この記事を作った動機

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

動作の様子

TODOs

TODOs

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

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

  • バックエンドの実装
    • ターゲット層を明確にする
    • ネットワーク監視について実装を思いついた案に基づいて進める
    • バックエンドの設定についてフロントエンドにUIを設けたい
    • deleted.json に対して、ページUUIDも記録するようにし、過去に削除されたページと同じ階層構造で同じ名前を持つページが新規作成されても衝突しないようにする。
    • 削除されたことになっているが、まだ物理的にファイルシステム上からは消されていないページと同じ階層構造で同じ名前を持つページが新規作成された場合、重複としては扱わず、削除対象のページを退避する。というか、元から削除されたページは退避する場所(フォルダー)をノートブック内に専用に作る。
    • 全体的なページやノートブックのメタデータなどにおける時間の扱いを全部 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 に変更があったことを通知するようにする

やったことの詳細

deleted.json の形式を変更した

 正しくページを識別できるようにページのUUIDを項目に追加した。

旧来形態

{
    "newnote": [
        {
            "pageID": "default.md",
            "date": "2025/10/5"
        },
        {
            "pageID": "test/test/test/recursively/test.md",
            "date": "2025/10/14"
        },
        ...
    ]
}

新しい定義

{
    "newnote": [
        {
            "UUID": "18e01583-3b79-42de-a544-f2fec7da41d7",
            "pageID": "testa.md",
            "date": "2025/12/3"
        },
        {
            "UUID": "9d49383e-9bb1-46ec-a631-66e6730c602c",
            "pageID": "test.md",
            "date": "2025/12/3"
        },
        {
            "UUID": "a20d2903-43e7-47c7-9c73-e213839165c7",
            "pageID": "ttstdss.md",
            "date": "2025/12/3"
        }
        ...
    ]
}

deletePage コマンドの挙動を変更

 削除されたページを一時的に保持するために、各ノートブック内に、deletedという名前のフォルダを作成するようにした。そして、そこに[ページのUUID.ページの拡張子]という形で平たく階層構造は保持せずに削除されたページのファイルは移動するようにした。

├─deleted
│      0ed59523-7ec1-4f56-b1e0-17e936238a3e.md
│      a20d2903-43e7-47c7-9c73-e213839165c7.md
│      ...

deletePage コマンドのバグ修正

 ちょっと触ってみたところ以下のようなバグがあったので、修正をした。

  1. ページを作成
  2. 作ったページを削除
  3. 同じノートブック内で、削除されたページと同じ階層構造、名前、ページタイプを持つ新しいページを作成する
  4. 作ったページを削除しようとするが、ノートブックの整合性が取れていないというエラーになる
  5. 新たにページを作成しようとしても、古いページがcontentsフォルダに残ったままなので、削除されたページと同じノートブック内で、同じ階層構造、名前、ページタイプを持つ新しいページは、重複エラーとなり作成できなくなる。

 上記のことから、コードを探ると、deleted.json内部に記録されたPageIDを元に、すでに、削除されたページと同じ階層構造、名前、ページタイプを持つページが削除されていないか検証するコードがあり、それが新しいページとすでに削除されたページを区別できていないことが分かった。

 そのため、具体的な解決策として、PageIDを比較するのではなく、ページに付随している識別子であるUUIDを比較することで、問題が再現しないことを確認し、バグを修正したということにした。

修正前

    # Check the page has already been deleted or not.
    find = False
    for aPageInfo in targetDeletedInfo:
        if(aPageInfo["PageID"] == pagePathFromContentFolder):
            find = True
            break
    if(find):
        await errorResponse(
            websocket,
            request,
            "The page has already been deleted even the notebook metadata still has the ref to the page. The integrality may be corrupted.",
            [notebookName,pagePath,pagePathFromContentFolder,deleted]
        )
        return

修正後

    # NOTE: UUID is added to as identifier
    # Check the page has already been deleted or not.
    find = False
    for aPageInfo in targetDeletedInfo:
        if(aPageInfo["UUID"] == PageUUID):
            find = True
            break
    if(find):
        await errorResponse(
            websocket,
            request,
            "The page has already been deleted even the notebook metadata still has the ref to the page. The integrality may be corrupted.",
            [notebookName,pagePath,pagePathFromContentFolder,deleted]
        )
        return

fileAPI の案に進展があった

 以下は簡易的なメモ書き。

fileAPI として実装するのではなく、さらにHTTPサーバも立てて細かいファイルのやり取りはそれに任せるということを思いついた。それなら大きなファイルも楽に既存のプロトコルに乗せてやり取りできるのではないかと思った。

基本的にはファイルサーバは、
	http://serverIP:port/Files/ノートブック名  +階層構造を含めたファイルまでのパス
	http://serverIP:port/Files/ノートブックUUID +階層構造を含めたファイルまでのパス
というような形で、直感的にリソースを提供できるようにしたい。

またついでに考えると、HTTPプロトコルへのフォールバックとして、
	http://serverIP:port/Commands/[CommandName]
のようなエントリーポイントが考えられた。

卒論を書き進めた

  • 調査の章について、何を書くか見当をつけることを進めた。
  • X11やマインクラフトのサーバークライアントモデルの件について参考になりそうなリソースを探した。
  • データサーバについて記述を追加した。どのような役割があるのか、内部構造がどうなっているのかについて、概要を説明するようにした。また図解も作成した。
    DataServerArchitecture
  • フロントエンドについて章を立てて、図解を作っている途中までは行った。ただし、十分説明できているかは定かではない。
    frontendSend
    frontendReceive

参考にしたサイトとか

 今回は特になし。