この記事を作った動機

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

動作の様子

TODOs

TODOs

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

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

  • バックエンドの実装
    • ターゲット層を明確にする
    • ネットワーク監視について実装を思いついた案に基づいて進める
    • バックエンドの設定についてフロントエンドにUIを設けたい
  • 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 に変更があったことを通知するようにする

やったことの詳細

Free ページがバグりまくるのを何とかする

 別のページを開くと前のページとアイテムが混じったりして地獄のようなことになっていたので修正しようと試みた。それで以下の点を試し、最終的に動いてそうな状況までもっていった。具体的には、Freeページを開いたらuseFreePageItemsStoreにアイテムを追加し、閉じたらuseFreePageItemsStoreからアイテムをなくし、初期化するはずだったが、それがうまくいかないという現象が起こった。別のFreeページに移動しても、うまくuseEffectでは、Freeページを閉じたときの処理を走らせることができず、前のFreeページのアイテムと、新しく開いたFreeページのアイテムが混ざる状態であった。

  1. Free ページコンポーネント内で、modified という状態変数を保持し、その状態変数が True となった時だけ、実際にデータサーバへセーブするようにリクエストを送る。アイテム追加、削除、やその他もろもろの編集機能は、modified状態変数を変更することで、変更を反映する。
  2. useFreePageItemsStore において、pageUUID をアイテムに紐づけて管理するようにした。しかし、あまり効果がなくバグを改善するにはこれは至らなかった気がする。またこの修正により、useAppStatecurrentPage の仕様を変更し、uuid についても管理するようにし、全体の実装もそれに合わせる変更をした。
  3. useEffectを使わず、useRefによって初期化を管理するようにした。useEffectを使ったコンポーネント初期化方法は一見するとクリーンアップも同時に実装でき便利なように見えるが、実は意図しないタイミングで発動することが分かり、それがバグを起こし、制御が難しかったことから、Free ページの初期化では使わないことにした。

以前の実装

...
    useEffect(() => {
        if(currentPage == null){
            showMessageBox({
                title: "Free Page",
                message: "Unable to find the opened page.",
                UUID: messageBoxUUID.current,
                type: "error"
            })
            return
        }
        console.log("Free page init")

        for(const item of jsondata.pageData.items){
            addItem(item,structuredClone(currentPage.uuid))
        }
        initComplete.current = true
        closed.current = false

        return () => {
            console.log("Free page end")
            initComplete.current = false
            closed.current = true
            cleanItem()
        }
    },[])
...

修正した実装

 あまり以下の実装におけるuseEffectの使い方はよくないっぽいが、現状だとそのままにしている。

...
    useEffect(() => {
        // if(currentPage == null){
        //     showMessageBox({
        //         title: "Free Page",
        //         message: "Unable to find the opened page.",
        //         UUID: messageBoxUUID.current,
        //         type: "error"
        //     })
        //     return
        // }
        // console.log("Free page init")

        // for(const item of jsondata.pageData.items){
        //     addItem(item,structuredClone(currentPage.uuid))
        // }
        // initComplete.current = true
        // closed.current = false

        return () => {
            console.log("Free page end")
            initComplete.current = false
            closed.current = true
            cleanItem()
        }
    },[])

    if(!initComplete.current){
        cleanItem()

        if(currentPage == null){
            showMessageBox({
                title: "Free Page",
                message: "Unable to find the opened page.",
                UUID: messageBoxUUID.current,
                type: "error"
            })
            return
        }
        console.log("Free page init")

        for(const item of jsondata.pageData.items){
            addItem(item,structuredClone(currentPage.uuid))
        }
        initComplete.current = true
        closed.current = false
    }
...

問題の様子と解決した様子

  • 問題の様子
  • 解決した様子

Commons を Free ページに実装した

 Free ページに存在するアイテムが共通でもつプロパティを編集できるUIを作った。まだ変更を正しく反映できないバグや、ActiveItemをUI操作中に維持できていない、色選択の表示がおかしいなどの問題がある。あとRaw DataZ-indexのボタンに一部未実装部分がある。

Commons

tag 機能について進めた

 以下のコマンドについて、dataserver側にまだ未実装でファイルだけ作っただけのコマンドと、具体的にどうするかある程度指針として決めるために、ドキュメントを軽く書いた。

  • addTag
     タグを特定のページに設定する。
  • createTag
     タグを新規作成する。
  • deleteTag
     タグを削除する。
  • getTagList
     既存のタグのリストを表示する。
  • queryTag
     タグに紐づいたページを検索する。
  • removeTag
     特定のタグを特定のページから切り離す。

フロントエンドのネットワークログの整理

 前の実装だと、いろんなところで websocketmessageイベントのデータを出力するようになっていて、コンソールが同じような出力で荒れるということがあったので、src\modules\helper\network.tsxに、何を送信して何が返ってきたか、表示するようにまとめてみた。

export function useDatabaseEffects() {
  const serverIP = useDatabaseStore((s) => s.serverIP);
  const setWebsocket = useDatabaseStore.setState;
  const getWebsocket = useDatabaseStore((s) => s.getWebsocket)
  // const init = useRef(true)
  
  const showMessage = (event:MessageEvent) => {
    console.log(event.data)
    try{
      console.log(JSON.parse(event.data))
    }catch(e) {
      console.log("This is not json data.")
    }
  }

  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("message",showMessage)
        // 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)
    websocket.addEventListener("message",showMessage)
    setWebsocket({ websocket: websocket });

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

ネットワーク接続状態の監視案について思いつく

 前フロントエンドのネットワーク再接続周りについて不完全な改善をしたことについては記録したが、その当時はWebsocketのブラウザの実装以外でネットワークの接続状況を確認する方法として、ポーリングし続けるというのがあった。しかし、それだとやはり無駄が多く、より良い正確に効率よく常態判定ができる方法はないか、考えたところ、「コマンドリクエストを送信して、一定時間経過したら、タイムアウト判定する」という方法を思いついた。

初期化時

 まず初期化としてデータサーバに接続するとき、一定時間経過後Ping的に一回コマンドを実行し、一定時間内に帰ってくるか見る。

接続時

 コマンドをリクエストした際に、同時にタイムアウトの監視を行う。

メリット

  • ポーリングしなくて済む
  • ブラウザの実装に依存せずに、自由にタイムアウト、接続の可否の判定ができる。

デメリット

  • 実装がちょいと煩雑になる
  • 接続後はコマンドが実行された後に初めて接続の可否が分かる仕様になっているため、UX的に良くないかもしれない。数十秒から数分単位で粗めにポーリングしつつ、コマンド送信時の応答時間の長さも判定に使うという方針がいいかもしれないとは思った。

Selector の実装をリファクタリングした

 Selector の実装が一つのスクリプトに詰まりすぎているのではないかということで、実装をいくつかに分けた。

│ selector.tsx
└─selector
        AnEntry.tsx
        CreateList.tsx
        helper.tsx
        init.tsx

selector.tsx

 Selector を表示するために、各selectorフォルダ内にあるコンポーネントを結び付け、総合的にUIをまとめて表示する親コンポーネントとしての役割がある。

AnEntry.tsx

 各ページや階層構造のエントリに関するUIパーツ一つ相当の実装がある。

CreateList.tsx

 各ノートブック内のページや階層構造などをリスト表示する実装がある。

helper.tsx

 現状では、updatePageInfoForSelectorという関数があり、notebookの情報を取得するコマンドを実行するためにネットワークrequestを出す実装がある。

init.tsx

 コンポーネント生成時に設定したいuseEffectをまとめたもの。

参考にしたサイトとか