diff --git a/gui/src/ipc/index.ts b/gui/src/ipc/index.ts deleted file mode 100644 index d19e762..0000000 --- a/gui/src/ipc/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { IPC } from './stdio'; -export type { Command } from './types'; diff --git a/gui/src/ipc/types.ts b/gui/src/ipc/types.ts deleted file mode 100644 index 1986489..0000000 --- a/gui/src/ipc/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type { Command, ExtractType } from '../../../src/ipc/types'; diff --git a/gui/src/jsonrpc/client.ts b/gui/src/jsonrpc/client.ts new file mode 100644 index 0000000..d82d182 --- /dev/null +++ b/gui/src/jsonrpc/client.ts @@ -0,0 +1,129 @@ +// A copy of `../../../src/jsonrpc/client.ts` +// deno-lint-ignore-file no-explicit-any +import { + ID, + Request, + Response, + ResponseError, + RPCResult, + Service, + Transport, +} from "./types"; + +export class JSONRPCClient { + protected nextId = 1; + protected transport: Transport; + protected requestMap: Map< + ID, + (result: RPCResult) => void + > = new Map(); + protected fatal: unknown = undefined; + protected task: Promise; + + constructor( + { transport }: { transport: Transport }, + ) { + this.transport = transport; + this.task = this.run(); + } + + protected setFatal(e: unknown) { + if (!this.fatal) { + this.fatal = e; + } + } + + protected handleResponse( + resp: Response, + ) { + const { id } = resp; + const callback = this.requestMap.get(id); + if (callback) { + this.requestMap.delete(id); + callback(resp); + } else { + this.setFatal(new Error("invalid response id: " + String(id))); + } + } + + // receive response from server + protected async run() { + try { + while (true) { + const data = await this.transport.recv(); + if (data === undefined) { + this.setFatal(new Error("transport closed")); + break; + } + const result = JSON.parse(data); + if (Array.isArray(result)) { + for (const resp of result) { + this.handleResponse(resp); + } + } else { + this.handleResponse(result); + } + } + } catch (e) { + this.setFatal(e); + } + } + + makeRequest< + K extends keyof S & string, + P extends Parameters, + >( + method: K, + params: P, + ): Request { + const req = { + jsonrpc: "2.0", + id: this.nextId, + method, + params, + } as const; + this.nextId += 1; + return req; + } + + async call< + K extends keyof S & string, + P extends Parameters, + R extends ReturnType, + >( + method: K, + ...params: P + ): Promise { + if (this.fatal) { + throw this.fatal; + } + const req = this.makeRequest(method, params); + await this.transport.send(JSON.stringify(req)); + + return new Promise((res, rej) => { + this.requestMap.set(req.id, (result) => { + if (result.error) { + rej(result.error); + } else { + res(result.result); + } + }); + }); + } + + getProxy(): S { + const proxy = new Proxy({}, { + get: (_, method: string) => { + return (...params: unknown[]) => { + return this.call(method, ...params as any); + }; + }, + }); + return proxy as S; + } + + async close() { + await this.transport.close(); + await this.task; + } +} diff --git a/gui/src/jsonrpc/index.ts b/gui/src/jsonrpc/index.ts new file mode 100644 index 0000000..939c66d --- /dev/null +++ b/gui/src/jsonrpc/index.ts @@ -0,0 +1,3 @@ +export type { S3SIService } from './types' +export { JSONRPCClient } from './client' +export { StdioTransport } from './stdio' diff --git a/gui/src/ipc/stdio.ts b/gui/src/jsonrpc/stdio.ts similarity index 51% rename from gui/src/ipc/stdio.ts rename to gui/src/jsonrpc/stdio.ts index d7d8eda..a27e4c8 100644 --- a/gui/src/ipc/stdio.ts +++ b/gui/src/jsonrpc/stdio.ts @@ -1,15 +1,14 @@ -import { ExtractType } from "./types"; import { Command, Child } from '@tauri-apps/api/shell' -export class IPC { - queue: T[] = []; - waiting: ((value: T) => void)[] = []; - callback = (data: unknown) => { +export class StdioTransport { + queue: string[] = []; + waiting: ((value: string | undefined) => void)[] = []; + callback = (data: string) => { const waiting = this.waiting.shift(); if (waiting) { - waiting(data as T); + waiting(data); } else { - this.queue.push(data as T); + this.queue.push(data); } }; child: Promise; @@ -19,7 +18,7 @@ export class IPC { ? new Command("deno", ["run", "-A", "../../src/daemon.ts"]) : Command.sidecar('../binaries/s3si'); command.stdout.on('data', line => { - this.callback(JSON.parse(line)) + this.callback(line) }) command.stderr.on('data', line => { console.error('daemon stderr', line) @@ -27,17 +26,8 @@ export class IPC { this.child = command.spawn() } - async recvType( - type: K, - ): Promise> { - const data = await this.recv(); - if (data.type !== type) { - throw new Error(`Unexpected type: ${data.type}`); - } - return data as ExtractType; - } - async recv(): Promise { - return new Promise((resolve) => { + async recv(): Promise { + return new Promise((resolve) => { const data = this.queue.shift(); if (data) { resolve(data); @@ -46,8 +36,12 @@ export class IPC { } }); } - async send(data: T) { + async send(data: string) { const child = await this.child; - await child.write(JSON.stringify(data) + "\n") + await child.write(data + "\n") + } + async close() { + const child = await this.child; + await child.kill() } } diff --git a/gui/src/jsonrpc/types.ts b/gui/src/jsonrpc/types.ts new file mode 100644 index 0000000..bbd45ba --- /dev/null +++ b/gui/src/jsonrpc/types.ts @@ -0,0 +1 @@ +export * from '../../../src/jsonrpc/types'; diff --git a/gui/src/pages/Home.tsx b/gui/src/pages/Home.tsx index 71c6540..f320709 100644 --- a/gui/src/pages/Home.tsx +++ b/gui/src/pages/Home.tsx @@ -1,26 +1,29 @@ import React from 'react' import { WebviewWindow } from '@tauri-apps/api/window' import { Loading } from 'components/Loading' -import { IPC, Command } from 'ipc'; +import { JSONRPCClient, S3SIService, StdioTransport } from 'jsonrpc'; -const ipc = new IPC(); +const client = new JSONRPCClient({ + transport: new StdioTransport() +}).getProxy(); export const Home: React.FC = ({ }) => { - const onClick = () => { - const webview = new WebviewWindow('theUniqueLabel', { - url: 'https://accounts.nintendo.com/', - resizable: false, - focus: true, - }) - }; const onHello = async () => { - await ipc.send({ type: 'hello', data: '1234' }); - const data = await ipc.recvType('hello'); - console.log(`hello`, data) + const result = await client.loginSteps(); + console.log(result) + if (result.error) { + throw new Error(result.error.message); + } + const webview = new WebviewWindow('login', { + url: 'https://accounts.nintendo.com/', + resizable: true, + focus: true, + }); + } return <> Hello world! - + } diff --git a/src/daemon.ts b/src/daemon.ts index 92fd8c7..8df4af8 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -1,9 +1,7 @@ -// deno-lint-ignore-file no-empty-interface - import { JSONRPCServer, - ResponseError, RPCResult, + S3SIService, Service, } from "./jsonrpc/mod.ts"; import { DenoIO } from "./jsonrpc/deno.ts"; @@ -11,32 +9,6 @@ import { loginSteps } from "./iksm.ts"; import { DEFAULT_ENV, Env } from "./env.ts"; import { Queue } from "./jsonrpc/channel.ts"; -export interface S3SINetworkError extends ResponseError<100> { -} - -export interface S3SIService { - loginSteps(): Promise< - RPCResult< - { - authCodeVerifier: string; - url: string; - }, - S3SINetworkError - > - >; - loginSteps(step2: { - authCodeVerifier: string; - login: string; - }): Promise< - RPCResult< - { - sessionToken: string; - }, - S3SINetworkError - > - >; -} - enum LoggerLevel { Debug = "debug", Log = "log", @@ -71,24 +43,16 @@ class S3SIServiceImplement implements S3SIService, Service { }; loginSteps(): Promise< - RPCResult< - { - authCodeVerifier: string; - url: string; - }, - S3SINetworkError - > + RPCResult<{ + authCodeVerifier: string; + url: string; + }> >; loginSteps(step2: { authCodeVerifier: string; login: string; }): Promise< - RPCResult< - { - sessionToken: string; - }, - S3SINetworkError - > + RPCResult<{ sessionToken: string }> >; async loginSteps(step2?: { authCodeVerifier: string; @@ -100,8 +64,7 @@ class S3SIServiceImplement implements S3SIService, Service { url: string; } | { sessionToken: string; - }, - S3SINetworkError + } > > { if (!step2) { diff --git a/src/jsonrpc/deno.ts b/src/jsonrpc/deno.ts index d984990..139200b 100644 --- a/src/jsonrpc/deno.ts +++ b/src/jsonrpc/deno.ts @@ -15,7 +15,7 @@ export class DenoIO implements Transport { const result = await this.lines.next(); if (!result.done) { - return JSON.parse(result.value); + return result.value; } return undefined; diff --git a/src/jsonrpc/jsonrpc.test.ts b/src/jsonrpc/jsonrpc.test.ts index b150b03..c2b67cb 100644 --- a/src/jsonrpc/jsonrpc.test.ts +++ b/src/jsonrpc/jsonrpc.test.ts @@ -13,6 +13,7 @@ export interface SimpleService { } class SimpleServiceImplement implements SimpleService, Service { + // deno-lint-ignore require-await async add(a: number, b: number): Promise> { return { result: a + b, diff --git a/src/jsonrpc/mod.ts b/src/jsonrpc/mod.ts index ccdf324..5468255 100644 --- a/src/jsonrpc/mod.ts +++ b/src/jsonrpc/mod.ts @@ -1,2 +1,4 @@ export * from "./types.ts"; export * from "./server.ts"; +export * from "./client.ts"; +export * from "./channel.ts"; diff --git a/src/jsonrpc/server.ts b/src/jsonrpc/server.ts index efdca86..c290519 100644 --- a/src/jsonrpc/server.ts +++ b/src/jsonrpc/server.ts @@ -47,7 +47,7 @@ export class JSONRPCServer { }; } - const result = await func(...params); + const result = await func.apply(this.service, params); return { ...res, @@ -81,7 +81,7 @@ export class JSONRPCServer { error: { code: 32000, message: "Internal error", - data: e, + data: String(e), }, }); diff --git a/src/jsonrpc/types.ts b/src/jsonrpc/types.ts index b8b58c9..6e7506c 100644 --- a/src/jsonrpc/types.ts +++ b/src/jsonrpc/types.ts @@ -64,3 +64,26 @@ export const ERROR_INTERNAL_ERROR: ResponseError<-32603> = { code: -32603, message: "Internal error", }; + +export interface S3SIService { + loginSteps(): Promise< + RPCResult< + { + authCodeVerifier: string; + url: string; + } + > + >; + loginSteps(step2: { + authCodeVerifier: string; + login: string; + }): Promise< + RPCResult< + { + sessionToken: string; + } + > + >; + // deno-lint-ignore no-explicit-any + [key: string]: any; +}