feat: add jsonrpc interface
parent
8df1224ea9
commit
80c0e26b3e
|
|
@ -1,2 +0,0 @@
|
|||
export { IPC } from './stdio';
|
||||
export type { Command } from './types';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type { Command, ExtractType } from '../../../src/ipc/types';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export type { S3SIService } from './types'
|
||||
export { JSONRPCClient } from './client'
|
||||
export { StdioTransport } from './stdio'
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
import { ExtractType } from "./types";
|
||||
import { Command, Child } from '@tauri-apps/api/shell'
|
||||
|
||||
export class IPC<T extends { type: string }> {
|
||||
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<Child>;
|
||||
|
|
@ -19,7 +18,7 @@ export class IPC<T extends { type: string }> {
|
|||
? 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<T extends { type: string }> {
|
|||
this.child = command.spawn()
|
||||
}
|
||||
|
||||
async recvType<K extends T["type"]>(
|
||||
type: K,
|
||||
): 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) => {
|
||||
async recv(): Promise<string | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const data = this.queue.shift();
|
||||
if (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;
|
||||
await child.write(JSON.stringify(data) + "\n")
|
||||
await child.write(data + "\n")
|
||||
}
|
||||
async close() {
|
||||
const child = await this.child;
|
||||
await child.kill()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from '../../../src/jsonrpc/types';
|
||||
|
|
@ -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<Command>();
|
||||
const client = new JSONRPCClient<S3SIService>({
|
||||
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! <Loading />
|
||||
<button onClick={onClick}>Open the window!</button>
|
||||
<button onClick={onHello}>Hello</button>
|
||||
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface SimpleService {
|
|||
}
|
||||
|
||||
class SimpleServiceImplement implements SimpleService, Service {
|
||||
// deno-lint-ignore require-await
|
||||
async add(a: number, b: number): Promise<RPCResult<number>> {
|
||||
return {
|
||||
result: a + b,
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
export * from "./types.ts";
|
||||
export * from "./server.ts";
|
||||
export * from "./client.ts";
|
||||
export * from "./channel.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),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue