Changes
25 changed files (+222/-141)
-
-
-
@@ -0,0 +1,8 @@# このディレクトリ特有の無視設定。 # # SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only # What: ビルドされた .js と .d.ts ファイル。 # Why: 編集するものではないため。 /lib
-
-
-
@@ -0,0 +1,25 @@{ "name": "@yamori/idb_backend", "private": true, "type": "module", "scripts": { "check": "tsc", "build": "tsc -p tsconfig.build.jsonc", "clean": "rm -rf lib" }, "exports": { ".": { "types": "./lib/lib.d.ts", "default": "./lib/lib.js" } }, "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@yamori/proto": "workspace:*", "idb": "^8.0.0" }, "devDependencies": { "@types/bun": "^1.1.14", "typescript": "^5.7.2" } }
-
-
-
@@ -0,0 +1,2 @@SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> SPDX-License-Identifier: AGPL-3.0-only
-
-
-
@@ -0,0 +1,38 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { openDB } from "idb"; import { CONTEXT } from "./symbols"; import { type Context, type RPCMessage } from "./types"; import { service } from "./yamori/service"; class IDBBackend { [CONTEXT]: Context; constructor(ctx: Context) { this[CONTEXT] = ctx; } handle(request: RPCMessage): Promise<Uint8Array> { return service(request, this[CONTEXT]); } } export async function idbBackend(): Promise<IDBBackend> { return new IDBBackend({ db: await openDB("yamori", 1, { upgrade(db, oldVersion, _newVersion, _transaction, _event) { if (oldVersion < 1) { const workspaces = db.createObjectStore("workspaces", { keyPath: "id", }); workspaces.createIndex("updatedAt", "updatedAt", { unique: false, }); } }, }), }); }
-
-
-
@@ -0,0 +1,4 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only export const CONTEXT = Symbol("IDBBackend.Context");
-
-
-
@@ -0,0 +1,19 @@// 外部とのやりとりやメソッドハンドラで共通するオブジェクトの定義。 // // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type IDBPDatabase } from "idb"; export interface Context { db: IDBPDatabase; } /** * RPC で授受されるリクエスト・レスポンス。 */ export interface RPCMessage { service: string; method: string; data: Uint8Array; }
-
-
packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/service.ts (new)
-
@@ -0,0 +1,27 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type Context, type RPCMessage } from "../../../../types"; import { create } from "./create"; import { deleteWorkspace } from "./delete"; import { list } from "./list"; import { update } from "./update"; export async function keyValueStorageBasedWorkspaceService( request: RPCMessage, ctx: Context, ): Promise<Uint8Array> { switch (request.method) { case "List": return list(request.data, ctx); case "Create": return create(request.data, ctx); case "Update": return update(request.data, ctx); case "Delete": return deleteWorkspace(request.data, ctx); default: throw new Error(`Unknown method "${request.method}" for ${request.service}`); } }
-
-
-
@@ -0,0 +1,14 @@// ビルドする際の設定。 // // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only { "extends": "./tsconfig.json", "compilerOptions": { "noEmit": false, "declaration": true, "outDir": "./lib" }, "include": ["src/**/*.ts"], "exclude": ["src/**/*.test.ts"] }
-
-
-
@@ -0,0 +1,10 @@{ "extends": "../../tsconfig.jsonc", "compilerOptions": { "allowImportingTsExtensions": false, "moduleResolution": "Bundler", "noEmit": true, "lib": ["ES2020", "WebWorker"] }, "include": ["src/**/*.ts"] }
-
-
-
@@ -0,0 +1,9 @@TypeScript のコンパイラ設定ファイル。 実際は JSON ではなく JSONC のためコメントを含められるが、 .jsonc にすると TypeScript 関連のツールがデフォルトで読まなくなる。逆に .json のまま コメントを含めるとフォーマッタといった TypeScript 以外の JSON を扱うツール でエラーになる。そのため拡張子と仕様に準拠している。 SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> SPDX-License-Identifier: AGPL-3.0-only
-
-
-
@@ -9,10 +9,8 @@ "build": "vite build","clean": "rm -rf dist" }, "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@yamori/proto": "workspace:*", "@yamori/idb_backend": "workspace:*", "@yamori/react_ui": "workspace:*", "idb": "^8.0.0", "react": "^19.0.0", "react-dom": "^19.0.0" },
-
-
packages/pwa/src/worker/context.ts (deleted)
-
@@ -1,10 +0,0 @@// 各メソッドハンドラに渡されるコンテキストの定義。 // // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type IDBPDatabase } from "idb"; export interface ServiceContext { db: IDBPDatabase; }
-
-
-
@@ -1,12 +1,12 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { isValidMessage } from "./message.ts"; import { setup } from "./setup.ts"; import { service } from "./yamori/service.ts"; import { idbBackend } from "@yamori/idb_backend"; import { isValidMessage, type Message } from "./message.ts"; async function main() { const ctx = await setup(); const backend = await idbBackend(); addEventListener("message", async (ev) => { if (!isValidMessage(ev.data)) {
-
@@ -14,11 +14,19 @@ console.warn("Invalid message sent from main thread.");return; } const resp = await service(ev.data, ctx); const resp = await backend.handle(ev.data); self.postMessage(resp, { transfer: [resp.data.buffer], }); self.postMessage( { id: ev.data.id, service: ev.data.service, method: ev.data.method, data: resp, } satisfies Message, { transfer: [resp.buffer], }, ); }); self.postMessage("ready");
-
-
packages/pwa/src/worker/setup.ts (deleted)
-
@@ -1,27 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { openDB } from "idb"; import { type ServiceContext } from "./context.ts"; /** * すぐに利用可能なコンテキストを生成・初期化して返す。 */ export async function setup(): Promise<ServiceContext> { return { db: await openDB("yamori", 1, { upgrade(db, oldVersion, _newVersion, _transaction, _event) { if (oldVersion < 1) { const workspaces = db.createObjectStore("workspaces", { keyPath: "id", }); workspaces.createIndex("updatedAt", "updatedAt", { unique: false, }); } }, }), }; }
-
-
-
@@ -1,12 +1,11 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type ServiceContext } from "../context.ts"; import { type Message } from "../message.ts"; import { type Context, type RPCMessage } from "../types"; import { keyValueStorageBasedWorkspaceService } from "./workspace/v1/key_value_storage_based_workspace_service/service.ts"; import { keyValueStorageBasedWorkspaceService } from "./workspace/v1/key_value_storage_based_workspace_service/service"; export async function service(request: Message, ctx: ServiceContext): Promise<Message> { export async function service(request: RPCMessage, ctx: Context): Promise<Uint8Array> { switch (request.service) { case "yamori.workspace.v1.KeyValueStorageBasedWorkspaceService": return keyValueStorageBasedWorkspaceService(request, ctx);
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/create.test.ts > packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/create.test.ts
-
@@ -14,10 +14,11 @@ import { KeySchema } from "@yamori/proto/yamori/idempotency/v1/key_pb.js";import { IDBFactory } from "fake-indexeddb"; import { openDB } from "idb"; import { setup } from "../../../../setup.ts"; import { idbBackend } from "../../../../lib"; import { CONTEXT } from "../../../../symbols"; import { create as method } from "./create.ts"; import { list } from "./list.ts"; import { create as method } from "./create"; import { list } from "./list"; beforeEach(() => { indexedDB = new IDBFactory();
-
@@ -85,7 +86,7 @@ expect(resp.result.value.code).toBe("IDB_ERROR");}); test("Should append a workspace", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const resp = fromBinary( CreateResponseSchema,
-
@@ -122,7 +123,7 @@ expect(listResp.result.value.workspaces[0]?.id?.value).toStartWith("ws-");}); test("Should allow inserting duplicating displayName", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const [resp1, resp2] = await Promise.all([ method(
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/create.ts > packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/create.ts
-
@@ -12,12 +12,9 @@ type Workspace,WorkspaceSchema, } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { type ServiceContext } from "../../../../context.ts"; import { type Context } from "../../../../types"; export async function create( data: Uint8Array, { db }: ServiceContext, ): Promise<Uint8Array> { export async function create(data: Uint8Array, { db }: Context): Promise<Uint8Array> { let req: CreateRequest; try {
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/delete.test.ts > packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/delete.test.ts
-
@@ -22,20 +22,21 @@ import { ListResponseSchema } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js";import { IDBFactory } from "fake-indexeddb"; import { openDB } from "idb"; import { setup } from "../../../../setup.ts"; import { type ServiceContext } from "../../../../context.ts"; import { idbBackend } from "../../../../lib"; import { CONTEXT } from "../../../../symbols"; import { type Context } from "../../../../types"; import { create as createHandler } from "./create.ts"; import { deleteWorkspace as deleteHandler } from "./delete.ts"; import { list } from "./list.ts"; import { create as createHandler } from "./create"; import { deleteWorkspace as deleteHandler } from "./delete"; import { list } from "./list"; function bind<RequestSchema extends DescMessage, ResponseSchema extends DescMessage>( requestSchema: RequestSchema, responseSchema: ResponseSchema, handler: (req: Uint8Array, ctx: ServiceContext) => Promise<Uint8Array>, handler: (req: Uint8Array, ctx: Context) => Promise<Uint8Array>, ): ( req: MessageInitShape<RequestSchema>, ctx: ServiceContext, ctx: Context, ) => Promise<MessageShape<ResponseSchema>> { return async (request, ctx) => fromBinary(
-
@@ -72,7 +73,7 @@ expect(resp.result.value.code).toBe("IDB_ERROR");}); test("Should return a missing field error if ID is nonexistent", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const created = await createWorkspace( {
-
@@ -109,7 +110,7 @@ expect(workspaces.result.value.workspaces).toHaveLength(1);}); test("Should return an error if ID field is absent", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const created = await createWorkspace( {
-
@@ -147,7 +148,7 @@ expect(workspaces.result.value.workspaces[0]?.displayName).toBe("Foo");}); test("Should delete a workspace", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const foo = await createWorkspace( {
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/delete.ts > packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/delete.ts
-
@@ -8,7 +8,7 @@ DeleteRequestSchema,} from "@yamori/proto/yamori/workspace/v1/delete_request_pb.js"; import { DeleteResponseSchema } from "@yamori/proto/yamori/workspace/v1/delete_response_pb.js"; import { type ServiceContext } from "../../../../context.ts"; import { type Context } from "../../../../types"; function respond(payload: MessageInitShape<typeof DeleteResponseSchema>): Uint8Array { return toBinary(DeleteResponseSchema, create(DeleteResponseSchema, payload));
-
@@ -19,7 +19,7 @@ * `delete` は予約後だからこれだけ目的語がついている。*/ export async function deleteWorkspace( data: Uint8Array, { db }: ServiceContext, { db }: Context, ): Promise<Uint8Array> { let req: DeleteRequest; try {
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/list.test.ts > packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/list.test.ts
-
@@ -13,10 +13,11 @@ import { ListResponseSchema } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js";import { IDBFactory } from "fake-indexeddb"; import { openDB } from "idb"; import { setup } from "../../../../setup.ts"; import { idbBackend } from "../../../../lib"; import { CONTEXT } from "../../../../symbols"; import { create as createHandler } from "./create.ts"; import { list } from "./list.ts"; import { create as createHandler } from "./create"; import { list } from "./list"; beforeEach(() => { indexedDB = new IDBFactory();
-
@@ -38,9 +39,11 @@ expect(resp.result.value.code).toBe("IDB_ERROR");}); test("Should return an empty workspaces on clean slate", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const resp = fromBinary( ListResponseSchema, await list(toBinary(ListRequestSchema, create(ListRequestSchema)), await setup()), await list(toBinary(ListRequestSchema, create(ListRequestSchema)), ctx), ); if (resp.result.case !== "ok") {
-
@@ -59,7 +62,7 @@ });} test("Should return workspaces in descending order", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const add = async (displayName: string) => { return fromBinary(
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/list.ts > packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/list.ts
-
@@ -5,12 +5,9 @@ import { create, toBinary } from "@bufbuild/protobuf";import { ListResponseSchema } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; import { WorkspaceSchema } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { type ServiceContext } from "../../../../context.ts"; import { type Context } from "../../../../types"; export async function list( _data: Uint8Array, { db }: ServiceContext, ): Promise<Uint8Array> { export async function list(_data: Uint8Array, { db }: Context): Promise<Uint8Array> { try { const tx = db.transaction("workspaces", "readonly"); const index = tx.store.index("updatedAt");
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/service.ts (deleted)
-
@@ -1,40 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type ServiceContext } from "../../../../context.ts"; import { type Message } from "../../../../message.ts"; import { create } from "./create.ts"; import { deleteWorkspace } from "./delete.ts"; import { list } from "./list.ts"; import { update } from "./update.ts"; export async function keyValueStorageBasedWorkspaceService( request: Message, ctx: ServiceContext, ): Promise<Message> { switch (request.method) { case "List": return { ...request, data: await list(request.data, ctx), }; case "Create": return { ...request, data: await create(request.data, ctx), }; case "Update": return { ...request, data: await update(request.data, ctx), }; case "Delete": return { ...request, data: await deleteWorkspace(request.data, ctx), }; default: throw new Error(`Unknown method "${request.method}" for ${request.service}`); } }
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/update.test.ts > packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/update.test.ts
-
@@ -22,12 +22,13 @@ import { UpdateResponseSchema } from "@yamori/proto/yamori/workspace/v1/update_response_pb.js";import { IDBFactory } from "fake-indexeddb"; import { openDB } from "idb"; import { setup } from "../../../../setup.ts"; import { type ServiceContext } from "../../../../context.ts"; import { idbBackend } from "../../../../lib"; import { CONTEXT } from "../../../../symbols"; import { type Context } from "../../../../types"; import { create as createHandler } from "./create.ts"; import { list } from "./list.ts"; import { update } from "./update.ts"; import { create as createHandler } from "./create"; import { list } from "./list"; import { update } from "./update"; beforeEach(() => { indexedDB = new IDBFactory();
-
@@ -36,10 +37,10 @@function bind<RequestSchema extends DescMessage, ResponseSchema extends DescMessage>( requestSchema: RequestSchema, responseSchema: ResponseSchema, handler: (req: Uint8Array, ctx: ServiceContext) => Promise<Uint8Array>, handler: (req: Uint8Array, ctx: Context) => Promise<Uint8Array>, ): ( req: MessageInitShape<RequestSchema>, ctx: ServiceContext, ctx: Context, ) => Promise<MessageShape<ResponseSchema>> { return async (request, ctx) => fromBinary(
-
@@ -76,7 +77,7 @@ expect(resp.result.value.code).toBe("IDB_ERROR");}); test("Should return a missing field error if ID is nonexistent", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const created = await createWorkspace( {
-
@@ -118,7 +119,7 @@ expect(workspaces.result.value.workspaces[0]?.displayName).toBe("Foo");}); test("Should return an error if ID field is absent", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const created = await createWorkspace( {
-
@@ -159,7 +160,7 @@ expect(workspaces.result.value.workspaces[0]?.displayName).toBe("Foo");}); test("Should return an error if display_name is in field_mask but missing", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const created = await createWorkspace( {
-
@@ -202,7 +203,7 @@ expect(workspaces.result.value.workspaces[0]?.displayName).toBe("Foo");}); test("Should do nothing if field_mask is empty", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const created = await createWorkspace( {
-
@@ -241,7 +242,7 @@ });} test("Should update a workspace", async () => { const ctx = await setup(); const { [CONTEXT]: ctx } = await idbBackend(); const foo = await createWorkspace( {
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/update.ts > packages/idb_backend/src/yamori/workspace/v1/key_value_storage_based_workspace_service/update.ts
-
@@ -8,16 +8,13 @@ UpdateRequestSchema,} from "@yamori/proto/yamori/workspace/v1/update_request_pb.js"; import { UpdateResponseSchema } from "@yamori/proto/yamori/workspace/v1/update_response_pb.js"; import { type ServiceContext } from "../../../../context.ts"; import { type Context } from "../../../../types"; function respond(payload: MessageInitShape<typeof UpdateResponseSchema>): Uint8Array { return toBinary(UpdateResponseSchema, create(UpdateResponseSchema, payload)); } export async function update( data: Uint8Array, { db }: ServiceContext, ): Promise<Uint8Array> { export async function update(data: Uint8Array, { db }: Context): Promise<Uint8Array> { let req: UpdateRequest; try { req = fromBinary(UpdateRequestSchema, data);
-