この記事を作った動機

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

動作の様子

TODOs

TODOs

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

メイン(持ち越し、新規混合)

  • バックエンドの実装
    • ターゲット層を明確にする
    • ネットワーク監視について実装を思いついた案に基づいて進める
    • バックエンドの設定についてフロントエンドにUIを設けたい
    • 全体的なページやノートブックのメタデータなどにおける時間の扱いを全部 UnixTime の秒単位に置き換える
  • フロントエンドのネットワーク周り
    • useDatabaseStoreuseNetworkStoreに名前を変更する。
    • send関数をuseDatabaseStoreuseNetworkStore)内にあるものに置き換え、旧来のsend関数の実装を廃止する。
    • createPage.tsxの実装で、ページタイプを取得するとネットワーク周りがバグってリクエストした履歴が消えない結果、未接続の状態として判定されてしまうというバグを何とかする。
  • free ページの実装
    • フロントエンド
      • 共通のUIについて、使い方の説明について、ドキュメントを書いた。
        • overlayWindow
        • messagebox
        • toolsbar
        • page
      • 実際に変更があったら、変更内容をリアルタイムで保存するようにする。
        • 別のページを開いたときとかに、バグって変なタイミングでデータを保存しようとするのを止める
        • セーブする際に、ページのUUIDが一致するか確かめてからセーブする。
      • 書式設定に関する edit toggleable を作る。文章をどこに寄せるかとか、フォントサイズとか、フォントカラーとかそういう細かい設定ができるようにする。
      • アイテムが何もないページにおいて、アイテムの追加時に、どんなアイテムタイプが利用可能か取得するのに失敗するのをどうにかする。
      • リアルタイムセーブがバグることについて調査する。別のページを開いたとき、Freeページコンポーネント自体やその変数の中身が、どういう状態になっているのかを調べる。
      • Metadata を表示できるようにする
      • Free ページの要素について、基本要素の項目にアイテムの色をRGBAで設定できるようにする。
        • とりあえず、まだ色を変更するUIがないが、RGBAとしてCSSスタイルを設定してアイテムごとの色を反映するようにFreeページの実装を更新はした。
        • 色を設定するUIや項目へのアクセスボタンを実装する。 -> すべてのアイテム共通の設定項目であるため、汎用に設定項目を表示できるpropertiesとは独立して、commonsの一部として実装する。
        • 設定UIで色の一部が正しく表示できていないバグをなおす。 -> padStart
        • commonsが正しく色を更新できないバグを何とかする。
      • z-index をちゃんと更新するようにする
      • useFreePageItemsStore の items や activeItems の配列をページごとに分離して管理するようにする。
      • どうしてもzustandを使ったグローバルなアイテム管理が暴走する場合は、Freeコンポーネント内部でアイテムを管理するように実装を変更する。 -> とりあえず zustand を使った実装で続行。
      • 範囲選択の実装 -> activeItemsを操作する
  • 編集機能、データセーブ(更新)の実装
    • free ページにおいて、保存が成功するまでは、変更があってから保存されるまでの間に、バッファーを保持するようにする。
  • バックエンド割り込みの実装
    • updatePageの割り込みがあった時に、どのページがセーブされたかIDをUUIDをデータとして含める
    • updatePageがあったら差分を扱えるように、先にセーブされたデータをバッファーにためるようにして、diffをマージできるようにする
  • データサーバに接続されているかどうかなどの、ステータスインジゲーターを作る
  • データサーバのupdatePageコマンドのUUID検証ロジックについて正しく機能しているか調べる。
  • データサーバの実装
    • addTag deleteTag コマンドの実装
    • queryTag コマンドの実装

前回からの持ち越し

持ち越し分

  • 内部構造を説明する図とかドキュメントを作る

    • フロントエンド
      • drawio で説明する図を作る
        • 他人が構造の理解に必要な分を記録できたか
    • バックエンド(更新)
      • 新たに追加した、task の仕組みについて、ドキュメントを整備する。
      • フロントエンドを拡張するうえで重要そうな zustand のデータストアの仕様リストを作り、記録を行う。
  • OverlayWindow において、リサイズやサイズ制限をオプションで付ける。デフォルトでは無効にして、とりあえず selector においてそれが機能するようにする。

  • 全体的な未保存、保存、バッファーの管理について考える

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

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

  • 再接続機能が正しく機能しない場合を検証する

    • 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 を複数ページ選択に対応させる
  • タグ機能を機能するようにする
    • バックエンド側のコマンドを整備
    • フロントエンド側がコマンドを実行するようにする
    • 割り込みの実装を定義して tag に変更があったことを通知するようにする

やったことの詳細

データサーバのfindNotes helper 関数でノートブックがないときの挙動について修正した

 卒論のために記録を取るために、実装の違いによるバグを避けるため、ノートブックデータをまっさらにした。そのところ、findNotes関数がノートブックのルートフォルダに何もない場合、エラーを返していることが分かった。infocreateNotebookコマンドなどの挙動がエラーになったりして、それでは正常に動作しないことが分かったため、ノートブックがない場合について、実装を追加した。

 以下は追加した実装である。単にノートブックのフォルダ内に何もない場合、空のdictを返すようにした。これによって、まっさらな状態でノートブックを新規作成できないなどのバグが改善された。

    if(os.listdir(root).__len__() == 0):
        print("findNotes helper: There is no notebooks. This may not be critical.")
        return {}

フロントエンドのsend関数にタイムアウトの項目を追加

 要求が破棄されても致命的ではない要求については、一定時間後に要求の履歴を破棄するように設定できるようにした。また、逆に要求の履歴が破棄されるのが不都合なものに関しては、timeoutnullにすることにより、attempt回分だけ要求を再試行するまで要求の履歴が破棄されないこととした。

変更後の実装 (src\modules\helper\network.tsx)

  send: (request:string, attempt, timeout = null) => {
    if(attempt == null) attempt = 5

    const websocket = get().websocket 
    if(websocket == null){
      // retry
      setTimeout(() => { get().send(request,attempt - 1,timeout) },interval * 1000)
      return
    }


    const requestUUID = JSON.parse(request).UUID
    let findRequestHistory = false
    for(const history of requestHistory){
      if(history.UUID == requestUUID){
        findRequestHistory = true
        break
      }
    }
    const CurrentHistory:Arequest = {
      requestJSONstring: request,
      UUID: requestUUID,
      requestTimestamp: new Date,
      timeout: timeout
    }
    if(!findRequestHistory){
      requestHistory.push(CurrentHistory)
    }
    

    if(attempt == 0){
      console.log("on websocket send data, all attempts are failed. stop")
      console.log(request)
      return
    }
    if(
      websocket.readyState != WebSocket.CONNECTING  && 
      websocket.readyState != WebSocket.CLOSED      &&
      websocket.readyState != WebSocket.CLOSING
    ){
      console.log(request)
      websocket.send(request)
      
      // check the request on the timeout state
      setTimeout(() => {
        console.log(requestHistory)
        for(const history of requestHistory){
          if(history.UUID == CurrentHistory.UUID){
            // when failed request
            set({ isDisconnect:true })
            
            // retry
            setTimeout(() => { get().send(request,attempt - 1,timeout) },interval * 1000)
            break
          }
        }
      },timeoutInterval * 1000)
      return
    }

    // retry
    setTimeout(() => { get().send(request,attempt - 1,timeout) },interval * 1000)
  },
  const removeReceivedRequest = (event:MessageEvent) => {

    // remove history depend on the timeout
    try{
      const newHistory = []
      for(const oldHistory of requestHistory){
        const requestTime = oldHistory.requestTimestamp.getTime() / 1000.0 // sec
        const currentTime = new Date().getTime() / 1000.0 // sec
        const timeout     = oldHistory.timeout
        if(timeout == null) continue
        if(currentTime - requestTime > timeout) continue
        newHistory.push(oldHistory)
      }
      requestHistory = newHistory
    }catch(error){
      console.log("Unable to remove a request. (depend on timeout)")
      console.log(error)
      console.log(event.data)
      console.log(event)
    }

    // remove history depend on the received data from the dataserver
    console.log("removeReceivedRequest is working")
    try{
      const jsondata = JSON.parse(event.data)
      if(jsondata.responseType != "commandResponse") return
      const newHistory = []
      for(const oldHistory of requestHistory){
        if(oldHistory.UUID == jsondata.UUID) continue
        newHistory.push(oldHistory)
      }
      requestHistory = newHistory
      console.log(jsondata.UUID)
      console.log(newHistory)
    }catch (error){
      console.log("Unable to remove a request. (depend on received data)")
      console.log(error)
      console.log(event.data)
      console.log(event)
    }
  }

変更前の実装 (src\modules\helper\network.tsx)

  send: (request:string, attempt) => {
    if(attempt == null) attempt = 5

    const websocket = get().websocket 
    if(websocket == null){
      // retry
      setTimeout(() => { get().send(request,attempt - 1) },interval * 1000)
      return
    }


    const requestUUID = JSON.parse(request).UUID
    let findRequestHistory = false
    for(const history of requestHistory){
      if(history.UUID == requestUUID){
        findRequestHistory = true
        break
      }
    }
    const CurrentHistory:Arequest = {
      requestJSONstring: request,
      UUID: requestUUID,
      requestTimestamp: new Date
    }
    if(!findRequestHistory){
      requestHistory.push(CurrentHistory)
    }
    

    if(attempt == 0){
      console.log("on websocket send data, all attempts are failed. stop")
      console.log(request)
      return
    }
    if(
      websocket.readyState != WebSocket.CONNECTING  && 
      websocket.readyState != WebSocket.CLOSED      &&
      websocket.readyState != WebSocket.CLOSING
    ){
      console.log(request)
      websocket.send(request)
      
      // check the request on the timeout state
      setTimeout(() => {
        console.log(requestHistory)
        for(const history of requestHistory){
          if(history.UUID == CurrentHistory.UUID){
            // when failed request
            set({ isDisconnect:true })
            
            // retry
            setTimeout(() => { get().send(request,attempt - 1) },interval * 1000)
            break
          }
        }
      },timeoutInterval * 1000)
      return
    }

    // retry
    setTimeout(() => { get().send(request,attempt - 1) },interval * 1000)
  },
  const removeReceivedRequest = (event:MessageEvent) => {
    // remove history depend on the received data from the dataserver
    console.log("removeReceivedRequest is working")
    try{
      const jsondata = JSON.parse(event.data)
      if(jsondata.responseType != "commandResponse") return
      const newHistory = []
      for(const oldHistory of requestHistory){
        if(oldHistory.UUID == jsondata.UUID) continue
        newHistory.push(oldHistory)
      }
      requestHistory = newHistory
      console.log(jsondata.UUID)
      console.log(newHistory)
    }catch (error){
      console.log("Unable to remove a request. (depend on received data)")
      console.log(error)
      console.log(event.data)
      console.log(event)
    }
  }

フロントエンドの接続状態管理について変更

 データサーバからメッセージがあった場合に、データサーバに接続されているとみなして、isDisconnectfalse(接続状態に変更)するようにした。

変更後の実装 (src\modules\helper\network.tsx)

    // when a message is received from the dataserver, change network status
    if(isDisconnect)
      setWebsocket({isDisconnect:false})
  const removeReceivedRequest = (event:MessageEvent) => {

    // remove history depend on the timeout
    try{
      const newHistory = []
      for(const oldHistory of requestHistory){
        const requestTime = oldHistory.requestTimestamp.getTime() / 1000.0 // sec
        const currentTime = new Date().getTime() / 1000.0 // sec
        const timeout     = oldHistory.timeout
        if(timeout == null) continue
        if(currentTime - requestTime > timeout) continue
        newHistory.push(oldHistory)
      }
      requestHistory = newHistory
    }catch(error){
      console.log("Unable to remove a request. (depend on timeout)")
      console.log(error)
      console.log(event.data)
      console.log(event)
    }

    // remove history depend on the received data from the dataserver
    console.log("removeReceivedRequest is working")
    try{
      const jsondata = JSON.parse(event.data)
      if(jsondata.responseType != "commandResponse") return
      const newHistory = []
      for(const oldHistory of requestHistory){
        if(oldHistory.UUID == jsondata.UUID) continue
        newHistory.push(oldHistory)
      }
      requestHistory = newHistory
      console.log(jsondata.UUID)
      console.log(newHistory)
    }catch (error){
      console.log("Unable to remove a request. (depend on received data)")
      console.log(error)
      console.log(event.data)
      console.log(event)
    }

    // when a message is received from the dataserver, change network status
    if(isDisconnect)
      setWebsocket({isDisconnect:false})
  }

getPageTypeコマンドだけなぜか、フロントエンドの要求履歴から消えない理由が分かった

 フロントエンドでページを新規作成するUIを開くと、同時にどのタイプのページを作成できるかフロントエンドがデータサーバに対して、getPageTypeコマンドを要求することがある。しかし、そのコマンドが実装されたときだけ、返答が返ってきているにも関わらず、ネットワーク要求履歴から消えないということがあった。

 それで最終的には、データサーバのgetPageTypeコマンドの実装が、旧来のままになっており、responseTypeを返していないことが分かった。このためにフロントエンドから要求履歴を削除しようとしたときに、getPageTypeコマンドの要求だけ、コマンド応答ではないとして、無視されていたことが分かった。そのため、getPageTypeコマンドの実装にて、コマンド応答として、responseTypeを返すように改修した。

 今回のことから、応答するための共通のhelper関数を作って、それで応答に必要な項目が書けていないか、チェックした方がミスが防げて良いかもしれないということがあった。

修正前

# list pageType

from helper.common import NotImplementedResponse, dataKeyChecker
import type.pages.controller as controller
from helper import loadSettings 
import json

async def getPageType(request,websocket):
    responseString = json.dumps({
        # responseType がここにない
        "status"        : "ok",   
        "errorMessage"  : "nothing",
        "UUID"          : request["UUID"],
        "command"       : "getPageType",
        "data"          : list(controller.pages.keys())
    })
    
    await websocket.send(responseString)
    print(">>> " + responseString)

修正後

# list pageType

from helper.common import NotImplementedResponse, dataKeyChecker
import type.pages.controller as controller
from helper import loadSettings 
import json

async def getPageType(request,websocket):
    responseString = json.dumps({
        "responseType"  : "commandResponse", # コマンドレスポンスを返すようにしたので、フロントエンドでバグを起こさないようになった。
        "status"        : "ok",
        "errorMessage"  : "nothing",
        "UUID"          : request["UUID"],
        "command"       : "getPageType",
        "data"          : list(controller.pages.keys())
    })
    
    await websocket.send(responseString)
    print(">>> " + responseString)

卒論を書いていた

  • 結果のところについて、ページの表示と新規作成について書いた
  • 考察のところについて大体何を書くかについて検討をつけた。
  • はじめにの目的のところで、理想と研究内で取り扱うことの二つに分けて、何に向かっているか、何をしようとしているかについて書いた。

参考にしたサイトとか

今回は特になし。