feat: add jsonrpc interface

main
imspace 2023-03-06 01:56:07 +08:00
parent 8df1224ea9
commit 80c0e26b3e
13 changed files with 200 additions and 84 deletions

View File

@ -1,2 +0,0 @@
export { IPC } from './stdio';
export type { Command } from './types';

View File

@ -1 +0,0 @@
export type { Command, ExtractType } from '../../../src/ipc/types';

129
gui/src/jsonrpc/client.ts Normal file
View File

@ -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<S extends Service> {
protected nextId = 1;
protected transport: Transport;
protected requestMap: Map<
ID,
(result: RPCResult<any, ResponseError>) => void
> = new Map();
protected fatal: unknown = undefined;
protected task: Promise<void>;
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<unknown, ResponseError>,
) {
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<S[K]>,
>(
method: K,
params: P,
): Request<K, P> {
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<S[K]>,
R extends ReturnType<S[K]>,
>(
method: K,
...params: P
): Promise<R> {
if (this.fatal) {
throw this.fatal;
}
const req = this.makeRequest(method, params);
await this.transport.send(JSON.stringify(req));
return new Promise<R>((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;
}
}

3
gui/src/jsonrpc/index.ts Normal file
View File

@ -0,0 +1,3 @@
export type { S3SIService } from './types'
export { JSONRPCClient } from './client'
export { StdioTransport } from './stdio'

View File

@ -1,15 +1,14 @@
import { ExtractType } from "./types";
import { Command, Child } from '@tauri-apps/api/shell' import { Command, Child } from '@tauri-apps/api/shell'
export class IPC<T extends { type: string }> { export class StdioTransport {
queue: T[] = []; queue: string[] = [];
waiting: ((value: T) => void)[] = []; waiting: ((value: string | undefined) => void)[] = [];
callback = (data: unknown) => { callback = (data: string) => {
const waiting = this.waiting.shift(); const waiting = this.waiting.shift();
if (waiting) { if (waiting) {
waiting(data as T); waiting(data);
} else { } else {
this.queue.push(data as T); this.queue.push(data);
} }
}; };
child: Promise<Child>; child: Promise<Child>;
@ -19,7 +18,7 @@ export class IPC<T extends { type: string }> {
? new Command("deno", ["run", "-A", "../../src/daemon.ts"]) ? new Command("deno", ["run", "-A", "../../src/daemon.ts"])
: Command.sidecar('../binaries/s3si'); : Command.sidecar('../binaries/s3si');
command.stdout.on('data', line => { command.stdout.on('data', line => {
this.callback(JSON.parse(line)) this.callback(line)
}) })
command.stderr.on('data', line => { command.stderr.on('data', line => {
console.error('daemon stderr', line) console.error('daemon stderr', line)
@ -27,17 +26,8 @@ export class IPC<T extends { type: string }> {
this.child = command.spawn() this.child = command.spawn()
} }
async recvType<K extends T["type"]>( async recv(): Promise<string | undefined> {
type: K, return new Promise((resolve) => {
): Promise<ExtractType<T, K>> {
const data = await this.recv();
if (data.type !== type) {
throw new Error(`Unexpected type: ${data.type}`);
}
return data as ExtractType<T, K>;
}
async recv(): Promise<T> {
return new Promise<T>((resolve) => {
const data = this.queue.shift(); const data = this.queue.shift();
if (data) { if (data) {
resolve(data); resolve(data);
@ -46,8 +36,12 @@ export class IPC<T extends { type: string }> {
} }
}); });
} }
async send(data: T) { async send(data: string) {
const child = await this.child; 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()
} }
} }

1
gui/src/jsonrpc/types.ts Normal file
View File

@ -0,0 +1 @@
export * from '../../../src/jsonrpc/types';

View File

@ -1,26 +1,29 @@
import React from 'react' import React from 'react'
import { WebviewWindow } from '@tauri-apps/api/window' import { WebviewWindow } from '@tauri-apps/api/window'
import { Loading } from 'components/Loading' import { Loading } from 'components/Loading'
import { IPC, Command } from 'ipc'; import { JSONRPCClient, S3SIService, StdioTransport } from 'jsonrpc';
const ipc = new IPC<Command>(); const client = new JSONRPCClient<S3SIService>({
transport: new StdioTransport()
}).getProxy();
export const Home: React.FC = ({ }) => { export const Home: React.FC = ({ }) => {
const onClick = () => {
const webview = new WebviewWindow('theUniqueLabel', {
url: 'https://accounts.nintendo.com/',
resizable: false,
focus: true,
})
};
const onHello = async () => { const onHello = async () => {
await ipc.send({ type: 'hello', data: '1234' }); const result = await client.loginSteps();
const data = await ipc.recvType('hello'); console.log(result)
console.log(`hello`, data) if (result.error) {
throw new Error(result.error.message);
}
const webview = new WebviewWindow('login', {
url: 'https://accounts.nintendo.com/',
resizable: true,
focus: true,
});
} }
return <> return <>
Hello world! <Loading /> Hello world! <Loading />
<button onClick={onClick}>Open the window!</button>
<button onClick={onHello}>Hello</button> <button onClick={onHello}>Hello</button>
</> </>
} }

View File

@ -1,9 +1,7 @@
// deno-lint-ignore-file no-empty-interface
import { import {
JSONRPCServer, JSONRPCServer,
ResponseError,
RPCResult, RPCResult,
S3SIService,
Service, Service,
} from "./jsonrpc/mod.ts"; } from "./jsonrpc/mod.ts";
import { DenoIO } from "./jsonrpc/deno.ts"; import { DenoIO } from "./jsonrpc/deno.ts";
@ -11,32 +9,6 @@ import { loginSteps } from "./iksm.ts";
import { DEFAULT_ENV, Env } from "./env.ts"; import { DEFAULT_ENV, Env } from "./env.ts";
import { Queue } from "./jsonrpc/channel.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 { enum LoggerLevel {
Debug = "debug", Debug = "debug",
Log = "log", Log = "log",
@ -71,24 +43,16 @@ class S3SIServiceImplement implements S3SIService, Service {
}; };
loginSteps(): Promise< loginSteps(): Promise<
RPCResult< RPCResult<{
{
authCodeVerifier: string; authCodeVerifier: string;
url: string; url: string;
}, }>
S3SINetworkError
>
>; >;
loginSteps(step2: { loginSteps(step2: {
authCodeVerifier: string; authCodeVerifier: string;
login: string; login: string;
}): Promise< }): Promise<
RPCResult< RPCResult<{ sessionToken: string }>
{
sessionToken: string;
},
S3SINetworkError
>
>; >;
async loginSteps(step2?: { async loginSteps(step2?: {
authCodeVerifier: string; authCodeVerifier: string;
@ -100,8 +64,7 @@ class S3SIServiceImplement implements S3SIService, Service {
url: string; url: string;
} | { } | {
sessionToken: string; sessionToken: string;
}, }
S3SINetworkError
> >
> { > {
if (!step2) { if (!step2) {

View File

@ -15,7 +15,7 @@ export class DenoIO implements Transport {
const result = await this.lines.next(); const result = await this.lines.next();
if (!result.done) { if (!result.done) {
return JSON.parse(result.value); return result.value;
} }
return undefined; return undefined;

View File

@ -13,6 +13,7 @@ export interface SimpleService {
} }
class SimpleServiceImplement implements SimpleService, Service { class SimpleServiceImplement implements SimpleService, Service {
// deno-lint-ignore require-await
async add(a: number, b: number): Promise<RPCResult<number>> { async add(a: number, b: number): Promise<RPCResult<number>> {
return { return {
result: a + b, result: a + b,

View File

@ -1,2 +1,4 @@
export * from "./types.ts"; export * from "./types.ts";
export * from "./server.ts"; export * from "./server.ts";
export * from "./client.ts";
export * from "./channel.ts";

View File

@ -47,7 +47,7 @@ export class JSONRPCServer {
}; };
} }
const result = await func(...params); const result = await func.apply(this.service, params);
return { return {
...res, ...res,
@ -81,7 +81,7 @@ export class JSONRPCServer {
error: { error: {
code: 32000, code: 32000,
message: "Internal error", message: "Internal error",
data: e, data: String(e),
}, },
}); });

View File

@ -64,3 +64,26 @@ export const ERROR_INTERNAL_ERROR: ResponseError<-32603> = {
code: -32603, code: -32603,
message: "Internal error", 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;
}