Skip to content

Commit

Permalink
feat: SeedEngine implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
y9san9 committed Feb 2, 2025
1 parent 2fd1a76 commit e134de0
Show file tree
Hide file tree
Showing 10 changed files with 2,422 additions and 2,467 deletions.
4 changes: 3 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export default tseslint.config(
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/ no-unused-var': ['warn']
'semi': [2, 'always'],
"quotes": ["error", "double", { "avoidEscape": true }],
"comma-dangle": ["error", "always-multiline"],
},
},
)
4,367 changes: 1,973 additions & 2,394 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@types/react-dom": "^18.3.1",
"@types/throttle-debounce": "^5.0.2",
"@vitejs/plugin-react": "^4.3.3",
"@vitest/browser": "^2.1.5",
"@vitest/browser": "^3.0.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
Expand All @@ -62,6 +62,6 @@
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10",
"vite-plugin-pwa": "^0.21.1",
"vitest-browser-react": "^0.0.3"
"vitest": "^3.0.4"
}
}
26 changes: 26 additions & 0 deletions src/sdk-v2/seed-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createObservable, Observable } from "@/coroutines/observable";

export type SeedEvent = {
url: string;
type: string;
payload: unknown;
}

export interface SeedClient {
events: Observable<SeedEvent>;
send(): Promise<void>;
subscribe(): Promise<void>;
}

export function createSeedClient(): SeedClient {
const events: Observable<SeedEvent> = createObservable();

async function execute({ }: SeedRequest): Promise<unknown> {

Check failure on line 18 in src/sdk-v2/seed-client.ts

View workflow job for this annotation

GitHub Actions / Build

Cannot find name 'SeedRequest'.
return;
}

return {

Check failure on line 22 in src/sdk-v2/seed-client.ts

View workflow job for this annotation

GitHub Actions / Build

Type '{ events: Observable<SeedEvent>; }' is missing the following properties from type 'SeedClient': send, subscribe
events,
};
}

115 changes: 115 additions & 0 deletions src/sdk-v2/seed-engine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@

import { expect, test } from "vitest";
import { createSeedEngine, SeedEngine } from "./seed-engine";

test("seed-engine-open", async () => {
const engine = createSeedEngine("wss://meetacy.app/seed-kt");
engine.start();
await awaitConnected(engine);
});

test("seed-engine-close", async () => {
const engine = createSeedEngine("wss://meetacy.app/seed-kt");
engine.start();
await awaitConnected(engine);
engine.stop();
await awaitDisconnected(engine);
});

test("seed-engine-restore", async () => {
const engine = createSeedEngine("wss://meetacy.app/seed-kt");
engine.start();
await awaitConnected(engine);
engine.stop();
await awaitDisconnected(engine);
engine.start();
await awaitConnected(engine);
});

test("seed-engine-connect", async () => {
const engine = createSeedEngine("wss://meetacy.app/seed-kt");
engine.start();
await awaitConnected(engine);
await engine.connectUrl("wss://meetacy.app/seed-go");
});

test("seed-engine-forward", async () => {
const engine = createSeedEngine("wss://meetacy.app/seed-kt");
engine.start();
await awaitConnected(engine);
await engine.connectUrl("wss://meetacy.app/seed-go");
await engine.executeOrThrow({
url: "wss://meetacy.app/seed-go",
payload: {
type: "ping",
},
});
});

test("seed-engine-ping", async () => {
const engine = createSeedEngine("wss://meetacy.app/seed-kt");
engine.start();
await awaitConnected(engine);
await engine.connectUrl("wss://meetacy.app/seed-kt");
await engine.executeOrThrow({
url: "wss://meetacy.app/seed-kt",
payload: {
type: "ping",
},
});
});


test("seed-engine-reject-closed", async () => {
const engine = createSeedEngine("wss://meetacy.app/seed-kt");
let error = false;
try {
await engine.executeOrThrow({ url: "wss://meetacy.app/seed-kt", payload: { type: "ping" } });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
error = true;
}
expect(error, "Request must be rejected without open").toBe(true);
});

test("seed-engine-reject-disconnected", async () => {
const engine = createSeedEngine("wss://meetacy.app/seed-kt");
engine.start();
awaitConnected(engine);
let error = false;
try {
await engine.executeOrThrow({ url: "wss://meetacy.app/seed-kt", payload: { type: "ping" } });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
error = true;
}
expect(error, "Request must be rejected without open").toBe(true);
});

function awaitConnected(engine: SeedEngine): Promise<void> {
return new Promise<void>((resolve, reject) => {
const cancel = engine.connectedEvents.subscribe(connected => {
console.log("TEST", connected);
if (connected) {
resolve();
} else {
reject("Expected connected");
}
cancel();
});
});
}

function awaitDisconnected(engine: SeedEngine) {
return new Promise<void>((resolve, reject) => {
const cancel = engine.connectedEvents.subscribe(connected => {
if (!connected) {
resolve();
} else {
reject("Expected disconnected");
}
cancel();
});
});
}

228 changes: 228 additions & 0 deletions src/sdk-v2/seed-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { Cancellation } from "@/coroutines/cancellation";
import { createObservable, Observable } from "@/coroutines/observable";
import typia from "typia";

export type SeedEngineEvent = {
url: string;
payload: unknown;
}

export type SeedEngineExecuteOptions = {
url: string;
payload: unknown;
}

export type SeedEngineInitOptions = {
url: string;
fn: () => Promise<void>;
}

export class SeedEngineDisconnected extends Error {
constructor() {
super("Disconnected while executing request");
this.name = "SeedEngineDisconnected";
}
}

type ConnectRequest = {
type: "connect";
url: string;
}

type SeedEngineReceivedMessage =
SeedEngineForwardReceivedMessage |
SeedEngineRawReceivedMessage

type SeedEngineForwardReceivedMessage = {
type: "forward";
url: string;
forward: SeedEngineRawReceivedMessage;
}

type SeedEngineRawReceivedMessage = {
type: "response";
response: unknown;
} | {
type: "event";
event: unknown;
}

type SeedEngineSentMessage = unknown | {
type: "forward";
url: string;
request: unknown;
}

type SeedEnginePendingRequest = {
url: string;
resolve: (payload: unknown) => void;
reject: () => void;
}

export interface SeedEngine {
events: Observable<SeedEngineEvent>;

connectedEvents: Observable<boolean>;
getConnected(): void;

disconnectedUrlEvents: Observable<string>;
connectUrl(url: string): Promise<void>;

/**
* @throws SeedEngineDisconnected if request was not successful due to
* network conditions
*/
executeOrThrow(request: SeedEngineExecuteOptions): Promise<unknown>;

init(init: SeedEngineInitOptions): Cancellation;

start(): void;
stop(): void;
}

export function createSeedEngine(mainUrl: string): SeedEngine {
const events: Observable<SeedEngineEvent> = createObservable();

const connectedEvents: Observable<boolean> = createObservable();
let connected = false;

function setConnected(value: boolean) {
connected = value;
connectedEvents.emit(value);
}

const disconnectedUrlEvents: Observable<string> = createObservable();

let ws: WebSocket | undefined;
let requests: SeedEnginePendingRequest[] = [];
let connectedUrls: string[] = [];

async function connectUrl(url: string) {
// It is fine to call connectUrl with mainUrl,
// but it will just do nothing
if (url === mainUrl) {
connectedUrls.push(url);
return;
}
const payload: ConnectRequest = {
type: "connect",
url,
};

try {
await executeOrThrow({
url: mainUrl,
payload,
}, false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
disconnectedUrlEvents.emit(url);
}

connectedUrls.push(url);
}

async function executeOrThrow(
{ url, payload }: SeedEngineExecuteOptions,
checkConnection: boolean = true,
) {
console.log(`>> execute\nServer: ${url}\nRequest:`, payload);
return new Promise((resolve, reject) => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
reject(new SeedEngineDisconnected());
return;
}
if (checkConnection && !connectedUrls.includes(url)) {
reject(new SeedEngineDisconnected());
return;
}

requests.push({ url, resolve, reject });

let message: SeedEngineSentMessage;

if (url === mainUrl) {
message = payload;
} else {
message = {
type: "forward",
url,
request: payload,
};
}

ws.send(JSON.stringify(message));
});
}

function start() {
ws = new WebSocket(mainUrl);

ws.onopen = () => {
console.log("<< ws: onopen");
setConnected(true);
};

ws.onmessage = (message) => {
const data: SeedEngineReceivedMessage = JSON.parse(message.data);
if (!typia.is<SeedEngineReceivedMessage>(data)) {
return;
}
let forward: SeedEngineForwardReceivedMessage;
if (data.type === "forward") {
forward = data;
} else {
forward = {
type: "forward",
url: mainUrl,
forward: data,
};
}
console.log(`<< message\nServer: ${forward.url}\nPayload:`, forward.forward);
const index = requests.findIndex((request) => request.url === forward.url);
if (index === -1) {
console.warn("Got response without any request");
return;
}
const { resolve } = requests[index];
requests.splice(index, 1);
resolve(forward.forward);
};

ws.onclose = () => {
console.log("<< ws: onclose");
setConnected(false);
for (const { reject } of requests) {
reject();
}
requests = [];
for (const url of connectedUrls) {
disconnectedUrlEvents.emit(url);
}
connectedUrls = [];
};
}

function stop() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.warn("SeedEngine.close() called while socket is closed");
}
ws?.close();
}

return {

Check failure on line 213 in src/sdk-v2/seed-engine.ts

View workflow job for this annotation

GitHub Actions / Build

Property 'init' is missing in type '{ events: Observable<SeedEngineEvent>; connectedEvents: Observable<boolean>; getConnected: () => boolean; disconnectedUrlEvents: Observable<string>; connectUrl: (url: string) => Promise<...>; executeOrThrow: ({ url, payload }: SeedEngineExecuteOptions, checkConnection?: boolean) => Promise<...>; start: () => void; s...' but required in type 'SeedEngine'.
events,

connectedEvents,
getConnected: () => connected,

disconnectedUrlEvents,
connectUrl,

executeOrThrow,

start,
stop,
};
}

Loading

0 comments on commit e134de0

Please sign in to comment.