この記事を作った動機

 2025/10/16 から、2025/10/23 に至るまでの間で OneNote 代替に関して、実装をするなど研究を進めているうちにわかったことなどを簡易的に記録する。

動作の様子 (フロントエンド)

TODOs

 2025/10/23 になる間やる項目たち。やりきれなかった分は次に持ち越し。

主な項目

  • 編集機能、データセーブ(更新)の実装

    • 別のページを開いても未保存のデータのバッファが管理されるように各ページタイプを実装する
  • 内部構造を説明する図とかドキュメントを作る

    • フロントエンド
      • drawio で説明する図を作る
        • 他人が構造の理解に必要な分を記録できたか
    • バックエンド(更新)
      • 新たに追加した、task の仕組みについて、ドキュメントを整備する。
  • free ページの実装

    • フロントエンド
      • 共通のUIについて、使い方の説明について、ドキュメントを書いた。
        • overlayWindow
        • messagebox
        • toolsbar
        • page
      • オブジェクト変形用に汎用的なコンポーネントを定義する。カーソルがオブジェクトをホバーしているときに出現するようにする。タッチでは、一回タップで出現。(サイズ変更、移動)
        • 移動について実装
        • リサイズ機能についての実装
          • 右下について実装 (とりあえずこれだけにするかも)
          • 下について実装
          • 右について実装
          • 左上について実装 (正しくitemを更新できないバグありで解消中)
          • 左について実装
          • 上について実装
          • 左下について実装
          • 右上について実装
      • 編集モードを各アイテムの要素の種類ごとに実装。汎用的には、ダブルクリックされた場合に編集モードに移行するようにする。タッチではダブルタッチで移行する.
      • フロントエンドを拡張するうえで重要そうな zustand のデータストアの仕様リストを作り、記録を行う。
    • バックエンド
  • selector の改善

    • Selector に未保存の表示を行う
  • OverlayWindow において、リサイズやサイズ制限をオプションで付ける。デフォルトでは無効にして、とりあえず selector においてそれが機能するようにする。

  • Free ページの要素について、基本要素の項目にアイテムの色をRGBAで設定できるようにする。

    • とりあえず、まだ色を変更するUIがないが、RGBAとしてCSSスタイルを設定してアイテムごとの色を反映するようにFreeページの実装を更新はした。
    • 色を設定するUIや項目へのアクセスボタンを実装する。
  • 全体的な未保存、保存、バッファーの管理について考える

  • pageType などは現状のままで、増やさず、free と markdown に専念する

  • tasks -> jobs で定期的に実行されるバックエンドの関数群において、pageCleaner.py を実装する。各ノートにある、参照が消され、deleted.json に記録されていて、一定期間たったページを時期が来たら本当にファイルシステム上から削除する実装をする。コード自体は、tasks.py の設定に依存し、現状では10秒ごとに消す時が来ていないかポーリングする簡易的な実装である。

  • バックエンド割り込みの実装

    • バックエンドからフロントエンドの方向の割り込みをすでにドキュメントである程度定義したとおりに実装する。例えば、ページ内容に変更があったら通知したりする。
    • helper.commonとして、sendIntteruptを実装する。
    • 割り込みが必要なコマンド群から、割り込みを送るようにする。また各コマンドのドキュメントに割り込みについて説明を書く。これに伴いテンプレートも割り込みについて記述するように、更新を行う。 -> 割り込みについては、専用のドキュメントページを作り、そこに記述する
    • フロントエンド側を割り込みを受けつけるように、改修していく。自動的に、フロントエンドが、バックエンドの割り込みによって連動して機能することを確認する。例えば、ページが作成されたとき、内容が変更されたとき、ノートブックが削除された時などに、データサーバが接続しているすべてのフロントエンドに、その通知が行く。各コンポーネントが、必要な割り込みイベントを、listen するような形をとる。
    • 割り込みの仕組みについてのドキュメントの記述が見つからない場合、新たに定義し、それをもとに実装を進める。
    • ドキュメントに書いた割り込みが実際に機能するよう、各コマンドに割り込みを送るように実装していく。
  • 再接続機能が正しく機能しない場合を検証する

    • WebSokcet が正常に閉じられなかった場合、自動的にそれを検出して再接続するようにフロントエンドを改修する。
    • WebSocket が正常に閉じられていないとき、誤作動を起こす件について、データサーバ側を改修することで、ある程度改善を行った。再接続機能改善の試みを詳細は参考にすること。
      • except asyncio.CancelledErrormainLoop 内で機能していない可能性について調べる。
      • 実はなんか思ってるのと違う WebSocket の閉じ方をしているかもしれないことについて調べる。
opening handshake failed
Traceback (most recent call last):
  File "C:\Users\username\AppData\Roaming\Python\Python312\site-packages\websockets\http11.py", line 138, in parse
    request_line = yield from parse_line(read_line)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\username\AppData\Roaming\Python\Python312\site-packages\websockets\http11.py", line 309, in parse_line
    line = yield from read_line(MAX_LINE_LENGTH)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\username\AppData\Roaming\Python\Python312\site-packages\websockets\streams.py", line 46, in read_line
    raise EOFError(f"stream ends after {p} bytes, before end of line")
EOFError: stream ends after 0 bytes, before end of line

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\username\AppData\Roaming\Python\Python312\site-packages\websockets\server.py", line 545, in parse
    request = yield from Request.parse(
              ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\username\AppData\Roaming\Python\Python312\site-packages\websockets\http11.py", line 140, in parse
    raise EOFError("connection closed while reading HTTP request line") from exc
EOFError: connection closed while reading HTTP request line

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\username\AppData\Roaming\Python\Python312\site-packages\websockets\asyncio\server.py", line 356, in conn_handler
    await connection.handshake(
  File "C:\Users\username\AppData\Roaming\Python\Python312\site-packages\websockets\asyncio\server.py", line 207, in handshake
    raise self.protocol.handshake_exc
websockets.exceptions.InvalidMessage: did not receive a valid HTTP request

他の事項

  • File API 周りの実装
  • マルチメディアの対応
  • selector を複数ページ選択に対応させる

やったことの詳細

dataserver -> frontend 方向の割り込みを考えた

 とりあえずどういうやり取りをするか、ある程度データ構造を決めてみた。

### dataserver to frontend
#### note in the frontend code
```
// dataserver -> frontend
// get an interrupt
// 
// {
//     "componentName"  : "Selector",
//     "command"        : "update",
//     "UUID"           : "UUID string",
//     "data"           : { }
// }
```
#### server interrupt reqest(JSON)
```json
{
    "componentName"  : "componentName",
    "interrupt"      : "update",
    "UUID"           : "UUID string",
    "data"           : { }
}
```

#### frontend response(JSON)
```json
{
    "status"        : "ok",
    "UUID"          : "UUID string",
    "interrupt"     : "update",
    "componentName" : "componentName",
    "errorMessage"  : "nothing",
    "data"          : { }    
}
```

sendInterrupt helper 関数を実装した

 とりあえずは、必要なキーがあることとを確認して、単にフロントエンドにデータを送信するようにした。フロントエンドから割り込みの結果も送るように現状定義はしているが、どうそれをバックエンド側で処理するかはまだ決めていない。現状の実装だと送りっぱなしの仕様になっている。

# TODO: test and use this
# TODO: write the document
# arg:
#   websocket       : the connection to the frontend via websocket
#   interrupt       : content of the interrupt
# return value
#   OK      : None
#   Error   : Error state does not exist.
async def sendInterrupt(websocket,interrupt:dict):
#   "componentName"  : "componentName",
#   "interrupt"      : "update",
#   "UUID"           : "UUID string",
#   "data"           : { }
    
    if(
        "componentName" in interrupt.keys() and
        "interrupt"     in interrupt.keys() and
        "UUID"          in interrupt.keys() and
        "data"          in interrupt.keys()
        ):
        await websocket.send(json.dumps(interrupt))
    else:
        print("sendInterrupt helper ERROR: Mandatory keys are missing")
        print("componentName,interrupt,UUID,data")
        print(interrupt)

dataserver -> frontend 方向の割り込みについて考え直してみた

 なんか割り込みイベントを受け取れるコンポーネントを直接指定していたりするのは、フロントエンド側を大規模にいじる必要があり、めんどくさく感じて、何か別の方法はないか考えていたところ、割り込みはイベント名を付与してそれをフロントエンドに送り付け、あとは各コンポーネントが自由にそれをさばくように作ればよさそうって思ったことがあった。

 それで割り込みは、データサーバからフロントエンドの一方通行で考え、以下のように変更した。

### dataserver to frontend
#### server interrupt reqest(JSON)
```json
{
    "event" : "EventName",
    "UUID"  : "UUID string",
    "data"  : { }
}
```
- event
 Interrupt event name. Any compornent are allowed to catch the interrupt.
- UUID  
 Identifier of the interrupt request
- data  
 Any data

 これでだいぶシンプルに実装しやすくなったように思う。また、sendInterrupt関数についても以下のように実装を変更した。

# TODO: test and use this
# TODO: write the document
# arg:
#   websocket       : the connection to the frontend via websocket
#   interrupt       : content of the interrupt
# return value
#   OK      : None
#   Error   : Error state does not exist.
async def sendInterrupt(websocket,interrupt:dict):
#   "componentName"  : "componentName",
#   "interrupt"      : "update",
#   "UUID"           : "UUID string",
#   "data"           : { }
    
    if(
        "event"         in interrupt.keys() and
        "UUID"          in interrupt.keys() and
        "data"          in interrupt.keys()
        ):
        await websocket.send(json.dumps(interrupt))
    else:
        print("sendInterrupt helper ERROR: Mandatory keys are missing")
        print("componentName,interrupt,UUID,data")
        print(interrupt)

 またどんな割り込みイベントがあって、なんのコマンドと関係あるかということについて、ドキュメントを作るようにした。以下はnewInfo割り込みイベントの例。

# newInfo
## Role
 Notifiy the frontend notebooks or pages are created or deleted.
 
## interrupt data 
```json
{
    "event" : "newInfo",
    "UUID"  : "UUID string",
    "data"  : { }
}
```

## related commands
- [createNotebook](../basicCommand/createNotebook.md)
- [deleteNotebook](../basicCommand/deleteNotebook.md)
- [createPage](../basicCommand/createPage.md)
- [deletePage](../basicCommand/deletePage.md)

再接続機能改善の試み

 フロントエンド側で一定周期でWebScoketreadyStateOPEN以外になっていないかを監視するという再接続コードを書いていたが、それだけだと不十分で、実際にはサーバーが落ちていても、ブラウザ側がまだWebSocketの接続が生きていると思うと、readyStateOPENのままになってうまく再接続できないことが分かった。

...
// 問題の実装
// TODO: Fix fllikering
// TODO: Reconnection Loop singleton, there are bug Reconnection Loop are appear mutiple times.
export function useDatabaseEffects() {
  const serverIP = useDatabaseStore((s) => s.serverIP);
  const setWebsocket = useDatabaseStore.setState;
  const getWebsocket = useDatabaseStore((s) => s.getWebsocket)
  // const init = useRef(true)

  useEffect(() => {
    if (!serverIP) return;

    const reconnectLoop = () => { 
      console.log("reconnect observer")
      const websocket = getWebsocket()
      if(websocket != null){
        // when there is no problem
        if(websocket.readyState == WebSocket.OPEN){
          // observe the connection
          setTimeout(() => { 
            reconnectLoop()
          },interval * 1000)
          return
        }
      }

      // when try to reconnect the dataserver 
      setTimeout(() => { 
        const websocket = new WebSocket(serverIP);
        // websocket.addEventListener("error",reconnectLoop)
        // websocket.addEventListener("open",reconnectLoop)
        setWebsocket({ websocket: websocket });
      },interval / 2 * 1000)
      setTimeout(() => { 
        reconnectLoop()
      },interval * 1000)
    } 
    const websocket = new WebSocket(serverIP);
    websocket.addEventListener("error",reconnectLoop)
    websocket.addEventListener("open",reconnectLoop)
    websocket.addEventListener("close",reconnectLoop)
    setWebsocket({ websocket: websocket });

    return () => {
      websocket.close();
    };
  }, [serverIP, setWebsocket]);
}

 また、WebScoketが実際には利用不可になっていて再接続が必要な状態になっていても、WebScoket.send()関数はバッファに送信内容をエンキューしたのちに、もし失敗した場合は”サイレント”に送信内容が破棄されるということが分かった。つまり、送信に失敗しても何もエラーも出てこずにそのまま動作し続けてしまうことが分かった。

以下は WebSocket: send() method - Web APIs | MDNの引用
The WebSocket.send() method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of bufferedAmount by the number of bytes needed to contain the data. If the data can’t be sent (for example, because it needs to be buffered but the buffer is full), the socket is closed automatically. The browser will throw an exception if you call send() when the connection is in the CONNECTING state. If you call send() when the connection is in the CLOSING or CLOSED states, the browser will silently discard the data.

 そこでまずできそうな改善点として、まずデータサーバが何かしらの理由で動作を停止するとき、既存の接続はちゃんときれいに例外をつかんで、きれいにWebSocketを全部閉じるように実装しようと試みた。

前の例外処理も全然してないコード (dataserver の main.py)

# main server scirpt
# serve dataserver function with websocket. 

import asyncio
import multiprocessing
# from modules.reqestParser import parser
from websockets.asyncio.server import serve
from helper import loadSettings
from helper.netwrok import receiveLoop 
import tasks
import controller

# 以下の mainLoop 関数では、サーバが何かしらの理由で終了されても、ちゃんと WebSocket を閉じてくれない。
async def mainLoop(websocket):
    await receiveLoop(websocket,controller.controller)    

async def dataserver():
    async with serve(mainLoop, loadSettings.settings["ip"], loadSettings.settings["port"]) as server:
        await server.serve_forever()

def dataserverThread():
    asyncio.run(dataserver())

def serviceThread():
    asyncio.run(tasks.taskController(loadSettings.settings["taskInterval"]))

if __name__ == "__main__":
    # 以下のコードでは、単に各プロセスをスタートさせて並列に動作させるだけになっており、
    # それぞれのプロセスが終了するのを待たないため、親プロセスにおいて、keyboardInterruptを全くつかめない。
    multiprocessing.Process(target=dataserverThread).start()
    multiprocessing.Process(target=serviceThread).start()

例外処理を追加したコード (dataserver の main.py)

 全部ではないが、とりあえずある程度、keyboardInterruptの例外をつかむようにして、かつ WebSocket はなるべく正常に閉じられるようにコードを書き換えた。

# main server scirpt
# serve dataserver function with websocket. 

import asyncio
import multiprocessing
# from modules.reqestParser import parser
from websockets.asyncio.server import serve
from helper import loadSettings
from helper.netwrok import receiveLoop 
import tasks
import controller
import sys

# 以下のコードでは、なるべく終了時に Websocket を閉じるようにした。
# ただし、except asyncio.CancelledError: はたぶん機能してなさそうなので要検証。
# dataserver() ですでに例外をつかんでしまっているからなのかは不明。
async def mainLoop(websocket):
    try:
        await receiveLoop(websocket,controller.controller)    
    except asyncio.CancelledError:
        print("The dataserver mainLoop is stopped.")
    finally:
        print("The dataserver mainLoop is closed.")
        await websocket.close()

async def dataserver():
    async with serve(mainLoop, loadSettings.settings["ip"], loadSettings.settings["port"]) as server:
        try:
            await server.serve_forever()
        except asyncio.CancelledError:
            print("The dataserver is stopped.")

def dataserverThread():
    asyncio.run(dataserver())

def serviceThread():
    try:
        asyncio.run(tasks.taskController(loadSettings.settings["taskInterval"]))
    except KeyboardInterrupt:
        print("The task controller is stopped.")

if __name__ == "__main__":
    print("This dataserver is started.")
    dataserverProcess = multiprocessing.Process(target=dataserverThread)
    serviceProcess = multiprocessing.Process(target=serviceThread)
    dataserverProcess.start()
    serviceProcess.start()

    # 例外をつかむために、各プロセスの終了を親プロセスで待つようにした。
    try:
        dataserverProcess.join()
        serviceProcess.join()
    except KeyboardInterrupt:
        print("This server will be stopped.")
        dataserverProcess.terminate()
        serviceProcess.terminate()
        print("All processes are terminated.")
        sys.exit()

フロントエンドとバックエンドの様子

  • 例外処理実装前

  • 例外処理実装後

ChatGPT

Open as a page
Open as a page

select 要素が Free Page だけで見えない

 全体で、onmousedownイベントをリッスンし、そのうえで、コールバック関数の中身に、event.preventDefault()を設定していると、select タグの中身が見えなくなることが分かった。

 具体的には、Free ページを開発していて、Add Item のフォームを設計していたところ、アイテム要素の種類を select タグから選べるはずが、全然ドロップダウンメニューが出てこず、右往左往したということがあった。それで、CSSスタイルやら疑わしいところを調べていたところ、Markdown ページでは症状が再現せず、Free ページのみに限って、select タグの中身が開けないということが、起こっていることが次はわかった。そして、マウス系のイベントで、全体的にリッスンしているコードを考えてみたところ、Free ページに固有で実装していた、右クリックメニューの実装の onmousedownのイベントのコールバックが怪しいなった。

 そこで、実際にそのコールバックを外してみると、Free ページを開いていても、select タグの内容が開けるようになることが分かった。コールバック関数を調べると、すべてのマウスの入力で、event.preventDefault() しており、select タグの内容を開くことまで prevent していることが分かった。

問題の実装と様子 (src\modules\pages\free\showMenu.tsx)

    useEffect(() => {
        const showAndHide = (event:MouseEvent) => {
            event.preventDefault() // これだとすべてのマウス入力で、preventDefault してしまう。
            if(event.button == 2){
                position.current.x = event.clientX
                position.current.y = event.clientY
                setVisible(true)
            }else{
                setVisible(false)
            }
        }
        addEventListener("mousedown",showAndHide)
        oncontextmenu = () => {return false}
        
        return () => {
            removeEventListener("mousedown",showAndHide)
        }
    },[])

解決とした実装と様子 (src\modules\pages\free\showMenu.tsx)

    useEffect(() => {
        const showAndHide = (event:MouseEvent) => {
            if(event.button == 2){
                event.preventDefault() // 右クリックのだけ、preventDefaultするようにすることで、通常の左クリックの操作を妨げないようにした。
                position.current.x = event.clientX
                position.current.y = event.clientY
                setVisible(true)
            }else{
                setVisible(false)
            }
        }
        addEventListener("mousedown",showAndHide)
        oncontextmenu = () => {return false}
        
        return () => {
            removeEventListener("mousedown",showAndHide)
        }
    },[])

Free ページでうまくアイテムを追加できない

 Free ページの実装として、アイテムを追加する機能を実装するというのがあり、作業していた。それで、AnItemのデータをテンプレートから作ろうとしたときに、変数の内容のコピーではなく、参照をコピーしてしまい、一回だけアイテムの追加に成功して、それ以降はアイテムを識別するための UUID がかぶってしまうため、アイテムが追加できないということが起こった。

 そこで、pythonの変数のディープコピー相当の機能はないかと調べていたところ、structuredClone が見つかったため、それを今回は実装で使うことにした。

問題のある実装

    function addItemToTheStore(){
        if(selectedType.current == null){
            showMessageBox({
                title: "Add Item",
                message: "Please select page type.",
                UUID: messageBoxUUID.current,
                type: "error"
            })
            return
        }

        // create an new item
        const targetElement = getElement(selectedType.current)       

        if(targetElement == null){
            showMessageBox({
                title: "Add Item",
                message: "Unable to find the page type.",
                UUID: messageBoxUUID.current,
                type: "error"
            })   
            return
        }

        // これでは、ディープコピーをしていないため、targetElement.defaultDataの参照をあちこちにばらまく結果となり、
        // アイテムを管理しているストアの中に、defaultData が登録されても、その登録された defaultData や targetElement.defaultData の ID が、
        // 全部 defaultData.ID = genUUID() で置き換わってしまうため、一つの独立したアイテムのデータを作るということができていなかった。
        const defaultData:AnItem = targetElement.defaultData
        defaultData.ID = genUUID()
        console.log(defaultData)

        // append the new item to the item store
        addItem(defaultData)

        const window = getWindow(OverlayWindowArg)
        if(window) closeWindow(window)
    }

解決とした実装

    function addItemToTheStore(){
        if(selectedType.current == null){
            showMessageBox({
                title: "Add Item",
                message: "Please select page type.",
                UUID: messageBoxUUID.current,
                type: "error"
            })
            return
        }

        // create an new item
        const targetElement = getElement(selectedType.current)       

        if(targetElement == null){
            showMessageBox({
                title: "Add Item",
                message: "Unable to find the page type.",
                UUID: messageBoxUUID.current,
                type: "error"
            })   
            return
        }

        // 以下では、targetElement.defaultData をディープコピーしてから、新しいアイテムのデータとして利用し、UUIDなどを設定しているため、
        // targetElement.defaultData の参照とは、新しいアイテムのデータは切り離され問題が解消されている。
        // deep copy is required
        // https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone
        const defaultData:AnItem = structuredClone(targetElement.defaultData)
        defaultData.ID = genUUID()
        console.log(defaultData)

        // append the new item to the item store
        addItem(defaultData)

        const window = getWindow(OverlayWindowArg)
        if(window) closeWindow(window)
    }

Free ページ アイテム追加の基本的なフロー 

 Free ページのアイテム管理の流れに変更点があったので、どうやってアイテムを追加するかのフローについて書いてみることにした。

  1. useFreePageElementStore から、どんなページタイプが利用可能かelementsを参照する。
  2. ユーザがアイテムタイプを選択肢して、作成ボタンを押す。
  3. useFreePageElementStore から、ユーザが取得したタイプのFreePageElementを取得する。
  4. 取得されたFreePageElementからAnItem型のテンプレートをディープコピーする。
  5. UUID を書き換え、新しい要素としてAnItem型のテンプレートデータを整形する。
  6. useFreePageItemsStoreに、テンプレートデータから作成した新しいアイテムを追加する。

 この流れの中で、新しく追加したものとしては、各アイテムタイプは、テンプレートデータを持ち、必要に応じて一部分のデータを変更していくということである。実際には以下のように実装した。

  • AnItem 自体のテンプレート
export const defaultItemData:AnItem = { 
    // need to be overwrite 
    data    : "",
    ID      : "",
    type    : "",

    // optional to overwrite
    position: {
        x: 100,
        y: 100,
        z: 1
    },
    size:{
        height: 100,
        width: 100
    },
    color:{
        a: 1,
        b: 100,
        g: 100,
        r: 100
    },
}
  • 各アイテムタイプのテンプレートの例(TextView)
// テンプレート作成の部分 -------
// テンプレート作成の部分 -------
const defaultItemDataForTextView:AnItem = defaultItemData
defaultItemDataForTextView.data = "Blank text item"
defaultItemDataForTextView.type = "text"
// テンプレート作成の部分 -------
// テンプレート作成の部分 -------

const element:FreePageElement = {
    name        : "text",
    element     : TextView,
    defaultData : defaultItemDataForTextView,
}
  • アイテム追加機能の実装の一部
        const defaultData:AnItem = structuredClone(targetElement.defaultData)
        defaultData.ID = genUUID()
        console.log(defaultData)

        // append the new item to the item store
        addItem(defaultData)

割り込みについて、さらに実装を追加した

 dataserver 側に一つ大きな割り込みの実装をした。具体的には以下の流れを実装した。

  1. interrupts.contoller にある、callInterrupt を各コマンドは必要に応じて呼ぶ。
  2. callInterrupt関数は、指定された割り込みが存在すればそれを呼ぶ
  3. 呼ばれた各種割り込みの実装の関数では、必要なデータが揃っていることを確認する。
  4. 問題なければ、helper.commonsendInterruptを使って割り込みをフロントエンドへ送信する。

controller.py の実装

 主なポイントは以下のとおりである。

  • intrruptListで利用可能な割り込みが保持されている。
  • callInterrupt関数では、存在しない割り込みを呼び出した時、エラーを吐く。
  • callInterrupt関数では、割り込みが存在した場合それを呼び出し、その実行の成否を戻り値として返す。
from newInfo    import newInfo
from updatePage import updatePage 

intrruptList = {
    "newInfo"       : newInfo,
    "updatePage"    : updatePage,
}

# return 
#  False -> OK
#  True  -> Something went worng
def callInterrupt(websocket,intrruptName:str,data:dict):
    for AnIntrrupt in intrruptList.keys():
        if(AnIntrrupt == intrruptName):
            # call intrrupt
            return intrruptList[AnIntrrupt](websocket,data)        
    
    print("callInterrupt ERROR: The intrrupt does not exist.")
    return True

各種割り込みの実装

 ここでは、newInfo割り込みの実装を例として挙げ、説明を試みる。

  • 必要な data の内容が揃っているかを確認する。
  • data の中身も必要に応じて検証する。
  • 問題なければ、responsedictを作成する。
  • sendInterrupt 関数で実際に割り込みを送信する。この時、送信の成否は戻り値として返されcallInterruptへ伝播される。
from ..helper.common import sendInterrupt
import uuid, copy, json

actionList = [
    "createNotebook",
    "deleteNotebook",
    "createPage",
    "deletePage",
]

def newInfo(websocket,data:dict):
    if(not "action" in data.keys()):
        print("newInfo interrupt ERROR: The mandatory key 'action' does not exist.")
        return True

    find = False
    for action in actionList:
        if(action == data["action"]):
            find = True
            break

    if(not find):
        print("newInfo interrupt ERROR: The action is invalid.")
        print(data)
        return True

    response = {
        "event" : "newInfo",
        "UUID"  : "UUID string",
        "data"  : data
    }

    return sendInterrupt(websocket,response)

実際にフロントエンドに割り込みを送るように実装した。

 まず以下の4つのコマンドについて、割り込みを送るように実装し、実際にそれがフロントエンドまで届くことを確認した。

Free ページの実装を進めた

 アイテムのリサイズ機能と、移動機能について、実装を進めた。移動機能については実装できたが、リサイズはまだ完全ではなく、右下からしか正常に相手もをリサイズできない状態が続いている。

 アイテム変形機能についてのフローとして、以下のような操作の形をとった。

  1. アイテムを一回クリックすると、移動機能が有効になる。
    移動機能が有効であるとき
  2. アイテムをダブルクリックすると、リサイズ機能が有効になる。
    リサイズ機能が有効な時
  3. アイテムをダブルクリックすると、編集機能が有効になる。
    編集機能が有効な時
  4. どのフェーズにおいても、アイテムの外を一回クリックすると、すべての機能が無効になり、最初の状態に戻る。
    何も有効でないとき

sendInterrupt helper 関数に関して、実装を変更した

 実際に、割り込みが機能しているか試したところ、データサーバが接続されているフロントエンドすべてにちゃんと割り込みを送信できる実装になっていなかった。割り込みを発生する要因となったフロントエンドにしか割り込みが来ていないことが分かり、修正を行った。

 問題となった実装では、単にコマンドがつかんでいるwebsocket接続の参照をそのままsendInterruptまで渡して、それを使って割り込みを送信してしまっているため、正しく接続されたすべてのフロントエンドに対して割り込みを送信できていなかった。

# arg:
#   websocket       : the connection to the frontend via websocket
#   interrupt       : content of the interrupt
# return value
#   OK      : False will be returned. It mean there are no problems
#   Error   : True will be returned. It mean there are something missing in dict keys of "interrupt" arg
async def sendInterrupt(websocket,interrupt:dict):
#   "evnet" : "eventName",
#   "UUID"  : "UUID string",
#   "data"  : { }
    
    if(
        "event"         in interrupt.keys() and
        "UUID"          in interrupt.keys() and
        "data"          in interrupt.keys()
        ):
        responseString = json.dumps(interrupt)
        print(">>> " + responseString)

        # これでは、コマンドを実行したフロントエンド以外には何も割り込みに通知がいかない
        await websocket.send(responseString)
        return False
    
    else:
        print("sendInterrupt helper ERROR: Mandatory keys are missing")
        print("componentName,interrupt,UUID,data")
        print(interrupt)
        return True

 具体的には、websocketの接続が確立されるごとに、websocketsという配列にその接続の参照を保持しておき、sendIntrrupt関数では、forループで全部のwebsocketの接続を回って割り込みを送信するように実装を変更した。

# arg:
#   websocket       : the connection to the frontend via websocket
#   interrupt       : content of the interrupt
# return value
#   OK      : False will be returned. It mean there are no problems
#   Error   : True will be returned. It mean there are something missing in dict keys of "interrupt" arg
async def sendInterrupt(websocket,interrupt:dict):
#   "evnet" : "eventName",
#   "UUID"  : "UUID string",
#   "data"  : { }
    if(
        "event"         in interrupt.keys() and
        "UUID"          in interrupt.keys() and
        "data"          in interrupt.keys()
        ):
        responseString = json.dumps(interrupt)
        print(">>> " + responseString)

        # 割り込みをすべてのフロントエンドに送信する実装
        for Awebsocket in websockets:
            try:
                await Awebsocket.send(responseString)            
            except Exception as e:
                print("sendInterrupt helper ERROR: Ignore the disconnected websocket.")
                print(e)
        # 割り込みをすべてのフロントエンドに送信する実装 終わり
        
        print(websockets)
        return False
    
    else:
        print("sendInterrupt helper ERROR: Mandatory keys are missing")
        print("componentName,interrupt,UUID,data")
        print(interrupt)
        return True

動作の様子

参考にしたサイトとか