この記事を作った動機
2025/11/6 から、2025/11/13 に至るまでの間で OneNote 代替に関して、実装をするなど研究を進めているうちにわかったことなどを簡易的に記録する。
動作の様子
TODOs
2025/11/13 になる間やる項目たち。やりきれなかった分は次に持ち越し。 内部構造を説明する図とかドキュメントを作る OverlayWindow において、リサイズやサイズ制限をオプションで付ける。デフォルトでは無効にして、とりあえず selector においてそれが機能するようにする。 全体的な未保存、保存、バッファーの管理について考える pageType などは現状のままで、増やさず、free と markdown に専念する tasks -> jobs で定期的に実行されるバックエンドの関数群において、pageCleaner.py を実装する。各ノートにある、参照が消され、deleted.json に記録されていて、一定期間たったページを時期が来たら本当にファイルシステム上から削除する実装をする。コード自体は、tasks.py の設定に依存し、現状では10秒ごとに消す時が来ていないかポーリングする簡易的な実装である。 再接続機能が正しく機能しない場合を検証するTODOs
メイン(持ち越し、新規混合)
useDatabaseStoreをuseNetworkStoreに名前を変更する。send関数をuseDatabaseStore(useNetworkStore)内にあるものに置き換え、旧来のsend関数の実装を廃止する。
propertiesとは独立して、commonsの一部として実装する。padStartcommonsが正しく色を更新できないバグを何とかする。
updatePageの割り込みがあった時に、どのページがセーブされたかIDをUUIDをデータとして含めるupdatePageがあったら差分を扱えるように、先にセーブされたデータをバッファーにためるようにして、diffをマージできるようにするupdatePageコマンドのUUID検証ロジックについて正しく機能しているか調べる。
前回からの持ち越し
持ち越し分
except asyncio.CancelledError が mainLoop 内で機能していない可能性について調べる。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
他の事項
やったことの詳細
保存される page や notebook の JSON データを human readable にする
もともとの実装だと、dataserver 側は単にデータを一行の JSON として保存しているだけで、可読性がひどいので、ちゃんと改行やインデントが入るように実装を変更した。
改修前のコード
import json
...
json.dumps(notebookMatadata)
...
改修後のコード
import json
...
json.dumps(notebookMatadata,indent=4)
...
改修前の Notebook metadata.json の例
{"name": "newnote", "createDate": "2025/9/26", "updateDate": "2025/11/6", "id": "5f3215a7-88f1-4065-9a01-98d630d99607", "pages": ["\u30c6\u30b9\u30c8.md", "\u3075\u3055\u304a.md", "test.md", "testa.md", "aaaaasdadaaaswe.md", "oneNoteStyle/husahusa.json", "oneNoteStyle/dssss.json"], "files": []}
改修後の Notebook metadata.json の例
{
"name": "newnote",
"createDate": "2025/9/26",
"updateDate": "2025/11/12",
"id": "5f3215a7-88f1-4065-9a01-98d630d99607",
"pages": [
"\u30c6\u30b9\u30c8.md",
"\u3075\u3055\u304a.md",
"test.md",
"testa.md",
"aaaaasdadaaaswe.md",
"oneNoteStyle/husahusa.json",
"oneNoteStyle/dssss.json"
],
"files": []
}
Free ページでメタデータを表示するように改修した
Free ページにおいて、メタデータを表示するように実装した。
src\modules\pages\free\main.tsx
...
useEffect(() => {
setMetadataToAppState({
createDate:jsondata.createDate,
files:jsondata.files,
tags:jsondata.tags,
updateDate:jsondata.updateDate,
UUID:jsondata.UUID
})
return () => {
setMetadataToAppState(null)
}
},[jsondata])
...
フロントエンドのネットワーク周りの改修
やったことリスト
- ユーザーに対し、背景を赤くすることで dataserver との接続が切れていることを表す。
- 再接続ループが常に
Websocketがオープンしたときに、ポーリングするようになっていたのを修正した。errorやcloseイベントが発火した時だけ、再接続を試みるように修正した。 useDatabaseEffectからuseNetworkEffectに名前を変更した。useDatabaseStore内に、send関数を実装した。useDatabaseStore内に、isDisconnectという項目をブール型で用意し、dataserver と接続されているか独立して状態を管理し、ほかのコンポーネントから参照できるようにした。requestHistoryという配列を用意し、Arequestという型をもってして送信した文字列や時間、コマンドリクエストのUUIDを管理するようにした。コマンドにdataserverから応答ががあった時は、requestHistoryの配列から送信した情報が削除され、正常にコマンドのやり取りができたことになる。useDatabaseStoreをuseNetworkStoreに名前を変更するTODOを立てた。
未接続状態の表示の実装例
export default function Window(){
...
const isDisconnect = useDatabaseStore((s) => s.isDisconnect)
let style = {
background: ""
}
if(isDisconnect){
style.background = "rgba(255,0,0,0.2)"
}
console.log(isDisconnect)
...
}
新しいsend関数
dataserver にリクエストを送る関数sendを、useDatabaseStore内に実装した。徐々にこの関数に実装を置き換えていく予定。
今までのsend関数の実装には問題があり、いちいちwebsocketをリクエストを行ったコンポーネントから引数として直接渡しているため、再接続などがあった時に、その新しいwebsocketを参照できずに、古い接続が途切れたwebsocketを使って再送信を試みるという問題が起こっていた。以下は今までのsend関数の流れである。
- 各コンポーネントが
websocketの参照をuseDatabaseStoreから持っている。 send関数が実行されたとき、useDatabaseStoreから持ってきたwebsocketの参照が"コピー"される。useDatabaseStoreのwebsocketの参照が変わっても、既に実行されているsend関数のwebsocketの参照が更新されない。send関数が送信失敗を検出し、再送信しようとするときに、既にとじらえていて利用不能なwebsocketの参照が利用される。- すべての再送信の試みにおいて失敗する。
そこで、websocketの参照が直接存在するuseDatabaseStore内にsend関数を移動することで、常に最新の接続されたweboscketの参照を使うようにしたいというのがあった。
- 旧来の実装
export function send(websocket:WebSocket, request:string, attempt=5){
// const addHistory = useNetworkRequestManager((s) => s.addRequest)
// const isHistoryExists = useNetworkRequestManager((s) => s.isRequestExist)
// const setWebsoketState = useDatabaseStore.setState
const CurrentHistory:Arequest = {
requestJSONstring: request,
UUID: JSON.parse(request).UUID,
requestTimestamp: new Date
}
// addHistory(history)
requestHistory.push(CurrentHistory)
if(attempt == 0){
console.log("on websocket send data, all attempts are failed. stop")
console.log(request)
return
}
// console.log(websocket.readyState)
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(() => {
for(const history of requestHistory){
if(history.UUID == CurrentHistory.UUID){
// when failed request
break
}
}
},timeoutInterval * 1000)
return
}
// 古い websocket を参照し続けながら、再試行回数上限を迎えるまで、永遠に再送信に失敗し続ける
setTimeout(() => { send(websocket,request,attempt - 1) },interval * 1000)
}
- 新しい実装
export const useDatabaseStore = create<Networks>((set, get) => ({
websocket: null,
serverIP: "ws://localhost:50097",
isDisconnect: true,
changeServer: (ip: string) => {
set({ serverIP: ip });
},
closeConnection: () => {
const ws = get().websocket;
ws?.close();
set({ websocket: null, isDisconnect: true });
},
getWebsocket: () => get().websocket,
send: (request:string, attempt) => {
if(attempt == null) attempt = 5
// 常に再送信時には最新の websocket を参照している
const websocket = get().websocket
if(websocket == null){
// retry
setTimeout(() => { get().send(request,attempt - 1) },interval * 1000)
return
}
const CurrentHistory:Arequest = {
requestJSONstring: request,
UUID: JSON.parse(request).UUID,
requestTimestamp: new Date
}
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)
},
}));
isDisconnect の実装の詳細
- true : 未接続
- false: dataserver に接続済み
isDisconnectはuseDatabaseStore内にあるブール型の変数で、dataserver とフロントエンドが接続されているか独立して状態を管理し、ほかのコンポーネントから参照できるようにしたものである。
isDisconnectは、websocketが閉じられたりエラーの時に、reconnectLoopに入ったり、send関数でタイムアウトが検出されたりすると、trueになり、何かしらの形でwebsocketの接続が復旧し、openイベントが発火したときにはfalseになる。
参考にしたサイトとか
- json — JSON encoder and decoder — Python 3.14.0 documentation
https://docs.python.org/3/library/json.html#basic-usage (2025年11月12日)