Changes
30 changed files (+382/-812)
-
-
@@ -8,5 +8,5 @@ bun 1.1.45terraform 1.10.3 protoc-gen-connect-go 1.18.0 protoc-gen-go 1.36.5 go 1.24.1 go 1.24.2 zig 0.14.0
-
-
-
@@ -39,8 +39,11 @@ },"packages/pwa": { "name": "@yamori/pwa", "dependencies": { "@connectrpc/connect": "^2.0.2", "@connectrpc/connect-web": "^2.0.2", "@yamori/backend": "packages/backend", "@yamori/idb_backend": "packages/idb_backend", "@yamori/proto": "packages/proto", "@yamori/react_ui": "packages/react_ui", "react": "^19.0.0", "react-dom": "^19.0.0",
-
@@ -156,6 +159,10 @@"@bufbuild/protoc-gen-es": ["@bufbuild/protoc-gen-es@2.2.2", "", { "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@bufbuild/protoplugin": "2.2.2" }, "bin": { "protoc-gen-es": "bin/protoc-gen-es" } }, "sha512-dQNfX2c6srAevuT0NR+C5OrNp+dlebIhR2R/GQOhJykCMJB1GB0jBnniSEWC+6YIUGFGLXgSyYbtNPaxZGPM0Q=="], "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.2.2", "", { "dependencies": { "@bufbuild/protobuf": "2.2.2", "@typescript/vfs": "^1.5.2", "typescript": "5.4.5" } }, "sha512-EKKrjBsA/F2l502PPWfmvx//qRJpOXpyDhwFOYSbwRYfqzEUuuNSb1A+YGE7iDxoEqxwo+len/CJny29iP1ESg=="], "@connectrpc/connect": ["@connectrpc/connect@2.0.2", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-xZuylIUNvNlH52e/4eQsZvY4QZyDJRtEFEDnn/yBrv5Xi5ZZI/p8X+GAHH35ucVaBvv9u7OzHZo8+tEh1EFTxA=="], "@connectrpc/connect-web": ["@connectrpc/connect-web@2.0.2", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0", "@connectrpc/connect": "2.0.2" } }, "sha512-QANMFPiL2o66BdBEctg4TsQLe5ozsBLqcle3dCBp7BwGlNGTY6NnNnqmt+YRnpeMW88GgomJwWNMGCrRD9pRKA=="], "@date-fns/tz": ["@date-fns/tz@1.2.0", "", {}, "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="],
-
@@ -491,7 +498,7 @@ "@yamori/idb_backend": ["@yamori/idb_backend@workspace:packages/idb_backend", { "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@date-fns/tz": "^1.2.0", "@yamori/proto": "packages/proto", "date-fns": "^4.1.0", "idb": "^8.0.0" }, "devDependencies": { "@types/bun": "^1.1.14", "fake-indexeddb": "^6.0.0", "typescript": "^5.7.2" } }],"@yamori/proto": ["@yamori/proto@workspace:packages/proto", { "devDependencies": { "@bufbuild/protobuf": "^2.2.2", "@bufbuild/protoc-gen-es": "^2.2.2" }, "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }], "@yamori/pwa": ["@yamori/pwa@workspace:packages/pwa", { "dependencies": { "@yamori/backend": "packages/backend", "@yamori/idb_backend": "packages/idb_backend", "@yamori/react_ui": "packages/react_ui", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@types/bun": "^1.1.14", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "rollup-plugin-license": "^3.5.3", "typescript": "^5.7.2", "vite": "^6.0.2", "zod": "^3.24.1" } }], "@yamori/pwa": ["@yamori/pwa@workspace:packages/pwa", { "dependencies": { "@connectrpc/connect": "^2.0.2", "@connectrpc/connect-web": "^2.0.2", "@yamori/backend": "packages/backend", "@yamori/idb_backend": "packages/idb_backend", "@yamori/proto": "packages/proto", "@yamori/react_ui": "packages/react_ui", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@types/bun": "^1.1.14", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "rollup-plugin-license": "^3.5.3", "typescript": "^5.7.2", "vite": "^6.0.2", "zod": "^3.24.1" } }], "@yamori/react_ui": ["@yamori/react_ui@workspace:packages/react_ui", { "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@date-fns/tz": "^1.2.0", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/themes": "^3.1.6", "@tanstack/react-query": "^5.62.8", "@yamori/idb_backend": "packages/idb_backend", "@yamori/proto": "packages/proto", "date-fns": "^4.1.0", "react-hook-form": "^7.54.2", "urlpattern-polyfill": "^10.0.0" }, "devDependencies": { "@storybook/addon-essentials": "^8.4.7", "@storybook/addon-interactions": "^8.4.7", "@storybook/react": "^8.4.7", "@storybook/react-vite": "^8.4.7", "@storybook/test": "^8.4.7", "@types/bun": "^1.1.14", "@types/react": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "react": "19.x.x", "react-dom": "^19.0.0", "typescript": "^5.7.2", "vite": "^6.0.2" }, "peerDependencies": { "react": "19.x.x", "react-dom": "19.x.x" } }],
-
-
-
@@ -1,13 +1,11 @@github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/tmaxmax/go-sse v0.8.0/go.mod h1:HLoxqxdH+7oSUItjtnpxjzJedfr/+Rrm/dNWBcTxJFM= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
-
-
-
@@ -10,7 +10,7 @@ "clean": "bun run --filter '*' clean"}, "wireit": { "make": { "dependencies": ["./packages/pwa:make"], "dependencies": ["./packages/pwa:make", "./packages/backend:bin"], "packageLocks": ["bun.lockb"] }, "reuse-lint": {
-
-
-
@@ -3,14 +3,12 @@ ## SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only # What: Protobuf から自動生成されたソースコードの格納先。 # Why: `packages/proto/` ディレクトリ配下の .proto ファイルから自動生成されるため。 /src/proto # What: ビルドされた .js と .d.ts ファイル。 # Why: 編集するものではないため。 /lib # What: Go 製サーバの実行ファイル。 # Why: バイナリ。 /backend # What: ビルドされた WebAssembly ファイル。 /backend.wasm # What: ビルドされた WebAssembly に必要な Go の JS ランタイム。 /backend_prelude.js
-
-
packages/backend/build.zig (deleted)
-
@@ -1,49 +0,0 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only //! Zig のビルドスクリプト。 const std = @import("std"); const ProtoGenStep = @import("gremlin").ProtoGenStep; pub fn build(b: *std.Build) void { const proto_gen = ProtoGenStep.create(b, .{ .proto_sources = b.path("../proto/"), .target = b.path("src/proto"), }); // === WASM const wasm_target = b.resolveTargetQuery(.{ .os_tag = .freestanding, .cpu_arch = .wasm32, }); // ファイルサイズが大きくなってきたら ReleaseSmall に置き換えるが、 // アプリケーションであるため初回ダウンロードよりも継続的な実行速度 // を重視したビルド設定になっている。 const wasm_optimize: std.builtin.OptimizeMode = .ReleaseSmall; const wasm = b.addExecutable(.{ .name = "yamori_backend", .root_source_file = b.path("src/wasm.zig"), .target = wasm_target, .optimize = wasm_optimize, }); wasm.root_module.addImport( "gremlin", b.dependency("gremlin", .{ .target = wasm_target, .optimize = wasm_optimize, }).module("gremlin"), ); // WASM モジュールを出力するために必要。 wasm.rdynamic = true; wasm.entry = .disabled; wasm.step.dependOn(&proto_gen.step); b.installArtifact(wasm); }
-
-
packages/backend/build.zig.zon (deleted)
-
@@ -1,61 +0,0 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only // // === // // Zig の依存関係周りを定義するファイル。 // 基本的には zig のサブコマンドで自動的に書き込まれるため手動ではいじらない。 // デフォルトのコメントは有用なため残してある。 .{ // This is the default name used by packages depending on this one. For // example, when a user runs `zig fetch --save <url>`, this field is used // as the key in the `dependencies` table. Although the user can choose a // different name, most users will stick with this provided value. // // It is redundant to include "zig" in this name because it is already // within the Zig package namespace. .name = .backend_core, // This is a [Semantic Version](https://semver.org/). // In a future version of Zig it will be used for package deduplication. .version = "0.0.0", // Together with name, this represents a globally unique package // identifier. This field is generated by the Zig toolchain when the // package is first created, and then *never changes*. This allows // unambiguous detection of one package being an updated version of // another. // // When forking a Zig project, this id should be regenerated (delete the // field and run `zig build`) if the upstream project is still maintained. // Otherwise, the fork is *hostile*, attempting to take control over the // original project's identity. Thus it is recommended to leave the comment // on the following line intact, so that it shows up in code reviews that // modify the field. .fingerprint = 0xea6d0b6824b9f28f, // Changing this has security and trust implications. // Tracks the earliest Zig version that the package considers to be a // supported use case. .minimum_zig_version = "0.14.0", // This field is optional. // Each dependency must either provide a `url` and `hash`, or a `path`. // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. // Once all dependencies are fetched, `zig build` no longer requires // internet connectivity. .dependencies = .{ .gremlin = .{ .url = "https://github.com/octopus-foundation/gremlin.zig/archive/fccfe2659f24497199d86404e0ef7dbc79e033d2.tar.gz", .hash = "gremlin-0.0.0-E2s91bN4DgDYbb4QNxl4ZT5nEEhNBkk5ngfVasDbsnNF", }, }, .paths = .{ "build.zig", "build.zig.zon", "src", // For example... //"LICENSE", //"README.md", }, }
-
-
-
@@ -0,0 +1,10 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package core type Core struct{} func Init() (*Core, error) { return &Core{}, nil }
-
-
-
@@ -7,9 +7,13 @@ go 1.24.1require ( connectrpc.com/connect v1.18.1 github.com/tetratelabs/wazero v1.9.0 github.com/nlepage/go-wasm-http-server/v2 v2.2.1 golang.org/x/net v0.23.0 google.golang.org/protobuf v1.36.5 ) require golang.org/x/text v0.14.0 // indirect require ( github.com/hack-pad/safejs v0.1.1 // indirect github.com/nlepage/go-js-promise v1.0.0 // indirect golang.org/x/text v0.14.0 // indirect )
-
-
-
@@ -1,8 +1,15 @@connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/nlepage/go-js-promise v1.0.0 h1:K7OmJ3+0BgWJ2LfXchg2sI6RDr7AW/KWR8182epFwGQ= github.com/nlepage/go-js-promise v1.0.0/go.mod h1:bdOP0wObXu34euibyK39K1hoBCtlgTKXGc56AGflaRo= github.com/nlepage/go-wasm-http-server/v2 v2.2.1 h1:4tzhSb3HKQ3Ykt2TPfqEnmcPfw8n1E8agv4OzAyckr8= github.com/nlepage/go-wasm-http-server/v2 v2.2.1/go.mod h1:r8j7cEOeUqNp+c+C52sNuWaFTvvT/cNqIwBuEtA36HA= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
-
-
-
@@ -2,58 +2,53 @@ {"name": "@yamori/backend", "private": true, "type": "module", "main": "lib/worker.js", "main": "sw.js", "scripts": { "check": "wireit", "make": "wireit", "clean": "rm -rf zig-out .zig-cache lib" "clean": "rm backend backend.wasm backend_prelude.js" }, "wireit": { "make:wasm": { "command": "zig build", "files": ["src/**/*.zig", "build.zig", "build.zig.zon"], "clean": false, "output": ["zig-out/**"] "command": "go build -o backend.wasm pocka.jp/x/yamori/backend/wasm", "env": { "GOOS": "js", "GOARCH": "wasm" }, "files": ["**/*.go", "go.mod", "go.sum"], "dependencies": ["../proto:go"], "output": ["backend.wasm"], "packageLocks": [] }, "make:js": { "command": "tsc -p tsconfig.build.jsonc", "files": ["src/**/*.ts", "tsconfig.build.jsonc"], "clean": "if-file-deleted", "output": ["lib/**"], "dependencies": ["tsconfig"], "packageLocks": ["bun.lockb"] "make:prelude": { "command": "cp $(go env GOROOT)/lib/wasm/wasm_exec.js ./backend_prelude.js", "output": ["backend_prelude.js"] }, "make:bin": { "command": "go build", "command": "go build -o backend pocka.jp/x/yamori/backend/server", "files": ["**/*.go", "go.mod", "go.sum"], "dependencies": ["../proto:go"], "output": ["backend"], "dependencies": ["wasm"], "packageLocks": [] }, "make": { "dependencies": ["make:wasm", "make:js", "make:bin"] }, "check": { "command": "tsc", "files": ["src/*.ts", "package.json"], "output": [], "dependencies": ["tsconfig"], "packageLocks": ["bun.lockb"] }, "tsconfig": { "files": ["tsconfig.json", "../../tsconfig.jsonc"] "dependencies": ["make:wasm", "make:bin", "make:prelude"] }, "js": { "files": ["lib/**/*.js"], "files": ["backend_prelude.js"], "dependencies": [ { "script": "make:js", "script": "wasm", "cascade": false }, { "script": "make:prelude", "cascade": false } ] }, "wasm": { "files": ["zig-out/bin/*.wasm"], "files": ["backend.wasm"], "dependencies": [ { "script": "make:wasm",
-
-
packages/backend/server.go (deleted)
-
@@ -1,30 +0,0 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package main import ( "context" _ "embed" "log" "net/http" "pocka.jp/x/yamori/backend/src" "pocka.jp/x/yamori/backend/src/connect_go" ) //go:embed zig-out/bin/yamori_backend.wasm var coreWasm []byte func main() { ctx := context.Background() core, err := src.Init(ctx, coreWasm) if err != nil { log.Panicln(err) } defer core.Close(ctx) handler := connect_go.InitHandler(core) http.ListenAndServe("localhost:8765", handler) }
-
-
-
@@ -0,0 +1,26 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package main import ( _ "embed" "log" "net/http" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "pocka.jp/x/yamori/backend/core" "pocka.jp/x/yamori/backend/services" ) func main() { core, err := core.Init() if err != nil { log.Fatal(err) } mux := services.Mux(core) http.ListenAndServe("localhost:8765", h2c.NewHandler(mux, &http2.Server{})) }
-
-
-
@@ -0,0 +1,24 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package services import ( "net/http" "pocka.jp/x/yamori/backend/core" "pocka.jp/x/yamori/backend/services/meta" "pocka.jp/x/yamori/backend/services/workspace" ) func Mux(core *core.Core) *http.ServeMux { mux := http.NewServeMux() meta := meta.Service{} meta.Register(mux) workspace := workspace.Service{} workspace.Register(mux) return mux }
-
-
-
@@ -0,0 +1,111 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package workspace import ( "context" "net/http" "connectrpc.com/connect" "google.golang.org/protobuf/proto" "pocka.jp/x/yamori/backend/core" errorV1 "pocka.jp/x/yamori/proto/go/error/v1" workspaceV2 "pocka.jp/x/yamori/proto/go/workspace/v2" workspaceV2connect "pocka.jp/x/yamori/proto/go/workspace/v2/v2connect" ) type Service struct { core *core.Core } func (s *Service) Login( ctx context.Context, req *connect.Request[workspaceV2.LoginRequest], ) (*connect.Response[workspaceV2.LoginResponse], error) { if *req.Msg.Name == "Alice" { ok := workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_Ok{ Ok: &workspaceV2.User{ Name: proto.String("Alice"), }, }, } return connect.NewResponse(&ok), nil } res := workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), }, }, } return connect.NewResponse(&res), nil } func (s *Service) Logout( ctx context.Context, req *connect.Request[workspaceV2.LogoutRequest], ) (*connect.Response[workspaceV2.LogoutResponse], error) { res := workspaceV2.LogoutResponse{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), }, } return connect.NewResponse(&res), nil } func (s *Service) Get( ctx context.Context, req *connect.Request[workspaceV2.GetRequest], ) (*connect.Response[workspaceV2.GetResponse], error) { res := workspaceV2.GetResponse{ Result: &workspaceV2.GetResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), }, }, } return connect.NewResponse(&res), nil } func (s *Service) Update( ctx context.Context, req *connect.Request[workspaceV2.UpdateRequest], ) (*connect.Response[workspaceV2.UpdateResponse], error) { res := workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), }, }, } return connect.NewResponse(&res), nil } func (s *Service) CreateUser( ctx context.Context, req *connect.Request[workspaceV2.CreateUserRequest], ) (*connect.Response[workspaceV2.CreateUserResponse], error) { res := workspaceV2.CreateUserResponse{ Result: &workspaceV2.CreateUserResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), }, }, } return connect.NewResponse(&res), nil } func (s *Service) Register(mux *http.ServeMux) { path, handler := workspaceV2connect.NewWorkspaceServiceHandler(s) mux.Handle(path, handler) }
-
-
packages/backend/src/Core.zig (deleted)
-
@@ -1,56 +0,0 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only const std = @import("std"); const ping_request = @import("./proto/yamori/meta/v1/ping_request.proto.zig"); const ping_response = @import("./proto/yamori/meta/v1/ping_response.proto.zig"); const Core = @This(); allocator: std.mem.Allocator, pub const HandleError = error{ UnknownService, UnknownMethod, DecodeError, EncodeError, }; pub fn init(allocator: std.mem.Allocator) Core { var core: Core = undefined; core.populate(allocator); return core; } pub fn populate(core: *Core, allocator: std.mem.Allocator) void { core.allocator = allocator; } pub fn handle( self: *const Core, service: []const u8, method: []const u8, bytes: []const u8, ) HandleError![]const u8 { var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit(); const allocator = arena.allocator(); if (std.mem.eql(u8, service, "yamori.meta.v1.MetaService")) { if (std.mem.eql(u8, method, "Ping")) { _ = ping_request.PingRequestReader.init(allocator, bytes) catch { return HandleError.DecodeError; }; return ping_response.PingResponse.encode(&.{}, self.allocator) catch { return HandleError.EncodeError; }; } return HandleError.UnknownMethod; } return HandleError.UnknownService; }
-
-
-
@@ -1,21 +0,0 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package connect_go import ( "net/http" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "pocka.jp/x/yamori/backend/src" ) func InitHandler(core *src.Core) http.Handler { mux := http.NewServeMux() meta := &metaServer{core: core} meta.register(mux) return h2c.NewHandler(mux, &http2.Server{}) }
-
-
-
@@ -1,7 +1,7 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package connect_go package meta import ( "context"
-
@@ -10,38 +10,30 @@"connectrpc.com/connect" "google.golang.org/protobuf/proto" "pocka.jp/x/yamori/backend/src" "pocka.jp/x/yamori/backend/core" metaV1 "pocka.jp/x/yamori/proto/go/meta/v1" metaV1connect "pocka.jp/x/yamori/proto/go/meta/v1/v1connect" ) type metaServer struct { core *src.Core type Service struct { core *core.Core } func (s *metaServer) Ping( func (s *Service) Ping( ctx context.Context, req *connect.Request[metaV1.PingRequest], ) (*connect.Response[metaV1.PingResponse], error) { reqBinary, err := proto.Marshal(req.Msg) if err != nil { return nil, err } data, err := s.core.Handle(ctx, "yamori.meta.v1.MetaService", "Ping", reqBinary) _, err := proto.Marshal(req.Msg) if err != nil { return nil, err } res := metaV1.PingResponse{} if err := proto.Unmarshal(data, &res); err != nil { return nil, err } return connect.NewResponse(&res), nil } func (s *metaServer) register(mux *http.ServeMux) { func (s *Service) Register(mux *http.ServeMux) { path, handler := metaV1connect.NewMetaServiceHandler(s) mux.Handle(path, handler)
-
-
packages/backend/src/core.go (deleted)
-
@@ -1,158 +0,0 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package src import ( "bytes" "context" "fmt" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" ) // Core は WASM で実装されており、特にメモリアクセスでかなり煩雑になる。 // この構造体はそれらの操作を抽象化する。 // 内部実装は基本的に JavaScript 実装 (worker.ts) のポートとなっている。 type Core struct { wasmRuntime wazero.Runtime mod api.Module corePtr uint64 } func Init(ctx context.Context, wasm []byte) (*Core, error) { runtime := wazero.NewRuntime(ctx) mod, err := runtime.Instantiate(ctx, wasm) if err != nil { return nil, err } initCore := mod.ExportedFunction("init_core") initCoreResults, err := initCore.Call(ctx) if err != nil { return nil, err } return &Core{ wasmRuntime: runtime, mod: mod, corePtr: initCoreResults[0], }, nil } // allocate は WASM の "allocate_bytes" を呼び、確保されたアドレスを返す。 func (core Core) allocate(ctx context.Context, length uint32) (uint32, error) { fn := core.mod.ExportedFunction("allocate_bytes") results, err := fn.Call(ctx, uint64(length)) if err != nil { return 0, err } return uint32(results[0]), nil } // copy は受け取ったバイト列を WASM のメモリ上にコピーして WASM から読めるようにする。 // 確保したアドレスとバイト長を返す。 func (core Core) copy(ctx context.Context, bytes []byte) (uint32, uint32, error) { length := uint32(len(bytes)) ptr, err := core.allocate(ctx, length) if err != nil { return 0, 0, err } if length == 0 { return ptr, length, nil } if !core.mod.Memory().Write(ptr, bytes) { return 0, 0, fmt.Errorf("Failed to write to memory (%d bytes)", length) } return ptr, length, nil } // Handle は RPC のリクエストを受けてレスポンスを返す関数。 // サービス名もメソッド名も変更しないため、レスポンスのバイナリデータのみを返す。 func (core Core) Handle(ctx context.Context, service string, method string, data []byte) ([]byte, error) { freeBytes := core.mod.ExportedFunction("free_bytes") servicePtr, serviceLen, err := core.copy(ctx, []byte(service)) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(servicePtr), uint64(serviceLen)) methodPtr, methodLen, err := core.copy(ctx, []byte(method)) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(methodPtr), uint64(methodLen)) dataPtr, dataLen, err := core.copy(ctx, data) if err != nil { return nil, err } defer freeBytes.Call(ctx, uint64(dataPtr), uint64(dataLen)) destroyResponse := core.mod.ExportedFunction("destroy_response") createResponse := core.mod.ExportedFunction("create_response") createResponseResult, err := createResponse.Call(ctx) if err != nil { return nil, err } defer destroyResponse.Call(ctx, createResponseResult[0]) handleFn := core.mod.ExportedFunction("handle") handleResults, err := handleFn.Call( ctx, core.corePtr, uint64(servicePtr), uint64(serviceLen), uint64(methodPtr), uint64(methodLen), uint64(dataPtr), uint64(dataLen), createResponseResult[0], ) if err != nil { return nil, err } if handleResults[0] != 0 { return nil, fmt.Errorf("handler returned non-zero result: %d", handleResults[0]) } getResponsePtr := core.mod.ExportedFunction("get_response_ptr") getResponseLen := core.mod.ExportedFunction("get_response_len") responsePtr, err := getResponsePtr.Call(ctx, createResponseResult[0]) if err != nil { return nil, err } responseLen, err := getResponseLen.Call(ctx, createResponseResult[0]) if err != nil { return nil, err } if responseLen[0] == 0 { return []byte{}, nil } responseData, isRangeOK := core.mod.Memory().Read(uint32(responsePtr[0]), uint32(responseLen[0])) if !isRangeOK { return nil, fmt.Errorf("get_response_ptr/len returned out-of-range address or length") } // destroy_response によってメモリが解放されるため、返す前にクローンする // 必要がある。しないと read after free になってしまう。 return bytes.Clone(responseData), nil } // Close を呼ぶと以降は Core を使った操作はできなくなる。 // WASM ランタイムのリソースが解放される。 func (core *Core) Close(ctx context.Context) { core.mod.Close(ctx) core.wasmRuntime.Close(ctx) core = nil }
-
-
packages/backend/src/wasm.zig (deleted)
-
@@ -1,97 +0,0 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only const std = @import("std"); const Core = @import("Core.zig"); export fn init_core() *Core { // TODO: Handle OOM var core = std.heap.wasm_allocator.create(Core) catch unreachable; core.populate(std.heap.wasm_allocator); return core; } export fn deinit_core(core: *Core) void { std.heap.wasm_allocator.destroy(core); } export fn allocate_bytes(len: usize) [*]u8 { const slice = std.heap.wasm_allocator.alloc(u8, len) catch unreachable; return slice.ptr; } export fn free_bytes(ptr: [*]u8, len: usize) void { std.heap.wasm_allocator.free(ptr[0..len]); } const Response = extern struct { ptr: [*]const u8, len: usize, pub fn populate(self: *Response, slice: []const u8) void { const dst = std.heap.wasm_allocator.alloc(u8, slice.len) catch unreachable; @memcpy(dst, slice); self.ptr = dst.ptr; self.len = dst.len; } pub fn deinit(self: *Response) void { std.heap.wasm_allocator.free(self.ptr[0..self.len]); self.* = undefined; } }; export fn create_response() *Response { const response = std.heap.wasm_allocator.create(Response) catch unreachable; return response; } export fn get_response_ptr(self: *const Response) [*]const u8 { return self.ptr; } export fn get_response_len(self: *const Response) usize { return self.len; } export fn destroy_response(response: *Response) void { response.deinit(); std.heap.wasm_allocator.destroy(response); } const HandleResult = enum(u8) { ok = 0, unknown_service = 1, unknown_method = 2, decode_error = 3, encode_error = 4, }; export fn handle( core: *const Core, service_ptr: [*]const u8, service_len: usize, method_ptr: [*]const u8, method_len: usize, bytes_ptr: [*]const u8, bytes_len: usize, resp_ptr: *Response, ) HandleResult { const resp = core.handle( service_ptr[0..service_len], method_ptr[0..method_len], bytes_ptr[0..bytes_len], ) catch |err| return switch (err) { Core.HandleError.UnknownMethod => .unknown_method, Core.HandleError.UnknownService => .unknown_service, Core.HandleError.DecodeError => .decode_error, Core.HandleError.EncodeError => .encode_error, }; resp_ptr.populate(resp); return .ok; }
-
-
packages/backend/src/worker.ts (deleted)
-
@@ -1,197 +0,0 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only const wasmURL = new URL("../zig-out/bin/yamori_backend.wasm", import.meta.url); export interface IncomingMessage { type: "request"; id?: unknown; service: string; method: string; data: Uint8Array; } function isIncomingMessage(x: unknown): x is IncomingMessage { if (!x || typeof x !== "object") { return false; } if (!("type" in x && x.type === "request")) { return false; } return true; } export type OutgoingMessage = | { type: "response"; id?: unknown; service: string; method: string; data: Uint8Array; } | { type: "error"; id?: unknown; error: string; } | { type: "ready"; }; type CorePtr = number & { [type: symbol]: "Core" }; type ResponsePtr = number & { [type: symbol]: "Response" }; type BytesPtr = number & { [type: symbol]: "Bytes" }; type Uint8Slice = { ptr: BytesPtr; len: number }; interface WasmExports { memory: InstanceType<WebAssembly.Memory>; init_core(): CorePtr; deinit_core(ptr: CorePtr): void; allocate_bytes(len: number): BytesPtr; free_bytes(ptr: BytesPtr, len: number): void; create_response(): ResponsePtr; get_response_ptr(ptr: ResponsePtr): number; get_response_len(ptr: ResponsePtr): number; destroy_response(ptr: ResponsePtr): void; handle( core: CorePtr, service_ptr: BytesPtr, service_len: number, method_ptr: BytesPtr, method_len: number, bytes_ptr: BytesPtr, bytes_len: number, resp_ptr: ResponsePtr, ): number; } class Backend { #utf8enc: TextEncoder = new TextEncoder(); #exports: WasmExports; #core: CorePtr; constructor(instance: WebAssembly.Instance) { this.#exports = instance.exports as unknown as WasmExports; this.#core = this.#exports.init_core(); } static async init(): Promise<Backend> { const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmURL)); return new Backend(instance); } #allocateBytes(len: number): Uint8Slice { const ptr = this.#exports.allocate_bytes(len); return { ptr, len }; } #send(data: Uint8Array): Uint8Slice { const slice = this.#allocateBytes(data.length); if (slice.len === 0) { return slice; } const view = new Uint8Array(this.#exports.memory.buffer, slice.ptr, slice.len); view.set(data); return slice; } #sendUtf8(string: string): Uint8Slice { const bytes = this.#utf8enc.encode(string); return this.#send(bytes); } handle(service: string, method: string, data: Uint8Array): Uint8Array { const serviceSlice = this.#sendUtf8(service); const methodSlice = this.#sendUtf8(method); const dataSlice = this.#send(data); const resp = this.#exports.create_response(); const result = this.#exports.handle( this.#core, serviceSlice.ptr, serviceSlice.len, methodSlice.ptr, methodSlice.len, dataSlice.ptr, dataSlice.len, resp, ); try { if (result !== 0) { throw new Error("Handler returned non-zero result"); } const ptr = this.#exports.get_response_ptr(resp); const len = this.#exports.get_response_len(resp); if (len === 0) { return new Uint8Array([]); } const cloned = this.#exports.memory.buffer.slice(ptr, len); return new Uint8Array(cloned); } finally { this.#exports.destroy_response(resp); this.#exports.free_bytes(dataSlice.ptr, dataSlice.len); this.#exports.free_bytes(methodSlice.ptr, methodSlice.len); this.#exports.free_bytes(serviceSlice.ptr, serviceSlice.len); } } } Backend.init().then((backend) => { addEventListener("message", async (event) => { if (!isIncomingMessage(event.data)) { console.warn("Received invalid message: ", event); return; } const { id, service, method, data } = event.data; try { const resp = backend.handle(service, method, data); self.postMessage( { type: "response", id, service, method, data: resp, } satisfies OutgoingMessage, { transfer: [resp.buffer], }, ); } catch (error) { console.error(error); self.postMessage({ type: "error", id, error: String(error), } satisfies OutgoingMessage); } }); self.postMessage({ type: "ready", } satisfies OutgoingMessage); });
-
-
packages/backend/tsconfig.build.jsonc (deleted)
-
@@ -1,14 +0,0 @@// ビルドする際の設定。 // // SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only { "extends": "./tsconfig.json", "compilerOptions": { "noEmit": false, "declaration": true, "incremental": true, "outDir": "./lib" }, "include": ["src/**/*.ts"] }
-
-
packages/backend/tsconfig.json (deleted)
-
@@ -1,10 +0,0 @@{ "extends": "../../tsconfig.jsonc", "compilerOptions": { "allowImportingTsExtensions": false, "moduleResolution": "Bundler", "noEmit": true, "lib": ["ES2020", "WebWorker"] }, "include": ["src/**/*.ts"] }
-
-
packages/backend/tsconfig.json.license (deleted)
-
@@ -1,9 +0,0 @@TypeScript のコンパイラ設定ファイル。 実際は JSON ではなく JSONC のためコメントを含められるが、 .jsonc にすると TypeScript 関連のツールがデフォルトで読まなくなる。逆に .json のまま コメントを含めるとフォーマッタといった TypeScript 以外の JSON を扱うツール でエラーになる。そのため拡張子と仕様に準拠している。 SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> SPDX-License-Identifier: AGPL-3.0-only
-
-
-
@@ -0,0 +1,34 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only //go:build js && wasm package main import ( "log" wasmhttp "github.com/nlepage/go-wasm-http-server/v2" "pocka.jp/x/yamori/backend/core" "pocka.jp/x/yamori/backend/services" ) func main() { core, err := core.Init() if err != nil { log.Fatal(err) } mux := services.Mux(core) _, err = wasmhttp.Serve(mux) if err != nil { log.Fatal(err) } // サーバがすぐ終了するのを防ぐ。 go-wasm-http-server の README には // 書いていないがサンプル (何故か `docs/` にある) にはしれっと入ってる。 // https://github.com/norunners/vue/issues/40#issuecomment-1253916764 select {} }
-
-
-
@@ -0,0 +1,43 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only // // go-wasm-http-server の sw.js を WebWorker で動くようにしたもの。 // I/F を揃えているが、 Go の JS シムに触れているため、 Go や // go-wasm-http-server をアップグレードする際には入念に確認すること。 import "./backend_prelude.js"; const wasm = new URL("./backend.wasm", import.meta.url); const handler = new Promise((resolve) => { self.wasmhttp = { path: "/", setHandler: resolve, }; }); const go = new Go(); go.argv = [wasm]; WebAssembly.instantiateStreaming(fetch(wasm), go.importObject).then(({ instance }) => go.run(instance), ); self.addEventListener("message", async (event) => { const req = new Request(event.data.input, event.data.options); const resp = await (await handler)(req); const body = await resp.arrayBuffer(); self.postMessage( { id: event.data.id, body, headers: Object.fromEntries(resp.headers.entries()), }, { transfer: [body], }, ); });
-
-
-
@@ -56,9 +56,12 @@ "packageLocks": ["bun.lockb"]} }, "dependencies": { "@connectrpc/connect": "^2.0.2", "@connectrpc/connect-web": "^2.0.2", "@yamori/backend": "workspace:*", "@yamori/idb_backend": "workspace:*", "@yamori/proto": "workspace:*", "@yamori/react_ui": "workspace:*", "@yamori/backend": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0" },
-
-
-
@@ -1,6 +1,10 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createClient } from "@connectrpc/connect"; import { createConnectTransport } from "@connectrpc/connect-web"; import { MetaService } from "@yamori/proto/yamori/meta/v1/meta_service_pb.js"; import { WorkspaceService } from "@yamori/proto/yamori/workspace/v2/workspace_service_pb.js"; import { ProtoRPCProvider, type ProtoRPC,
-
@@ -13,23 +17,6 @@ import { createRoot } from "react-dom/client";import css from "./main.module.css"; import { Message, isValidMessage } from "./worker/message.ts"; import type { IncomingMessage, OutgoingMessage } from "@yamori/backend"; function isOutgoingMessage(x: unknown): x is OutgoingMessage { if (!x || typeof x !== "object" || !("type" in x)) { return false; } switch (x.type) { case "response": case "error": case "ready": return true; default: return false; } } async function readThirdPartyNotice() { const resp = await fetch("/third-party.txt");
-
@@ -91,38 +78,63 @@ const backend = new Worker(new URL("./worker/backend.ts", import.meta.url), {type: "module", }); backend.addEventListener( "message", (event) => { if (!isOutgoingMessage(event.data) || event.data.type !== "ready") { return; } const http = createConnectTransport({ baseUrl: "/api", }); backend.addEventListener("message", (event) => { if (!isOutgoingMessage(event.data)) { return; } const transport = createConnectTransport({ baseUrl: "/", useBinaryFormat: true, fetch(request, opts) { const id = crypto.randomUUID(); switch (event.data.type) { case "response": { console.dir(event.data); return; } case "error": { console.error(event.data.error); backend.postMessage( { id, input: request, options: { headers: Object.fromEntries(new Headers(opts?.headers).entries()), method: opts?.method, body: opts?.body, }, }, { transfer: [(opts?.body as Uint8Array).buffer], }, ); return new Promise((resolve) => { const handler = (event: MessageEvent) => { if (event.data.id !== id) { return; } } }); backend.postMessage({ type: "request", service: "yamori.meta.v1", method: "Ping", data: new Uint8Array([]), } satisfies IncomingMessage); }, { once: true, const body = event.data.body as ArrayBuffer; const headers = event.data.headers as Record<string, string>; console.log(event.data); resolve(new Response(body, { headers })); backend.removeEventListener("message", handler); }; backend.addEventListener("message", handler); }); }, ); }); const client = createClient(MetaService, transport); client.ping({}).then((pong) => { console.log(pong); }); const workspace = createClient(WorkspaceService, http); workspace .login({ name: "Alice", password: "Alice", }) .then((res) => { console.log(res); });
-
-
-
@@ -1,4 +1,4 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import "@yamori/backend"; import "@yamori/backend/worker.js";
-
-
-
@@ -11,6 +11,14 @@export default defineConfig({ root: new URL("./src", import.meta.url).pathname, publicDir: new URL("../assets", import.meta.url).pathname, server: { proxy: { "/api": { target: "http://localhost:8765", rewrite: (path) => path.replace(/^\/api/, ""), }, }, }, build: { emptyOutDir: true, outDir: "../dist",
-