@replit/river
Advanced tools
Comparing version 0.1.10 to 0.2.0
@@ -0,1 +1,2 @@ | ||
import { TransportMessage } from '../transport/message'; | ||
export declare const EchoRequest: import("@sinclair/typebox").TObject<{ | ||
@@ -23,5 +24,5 @@ msg: import("@sinclair/typebox").TString; | ||
count: number; | ||
}>, input: import("../transport/message").TransportMessage<{ | ||
}>, input: TransportMessage<{ | ||
n: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
}>) => Promise<TransportMessage<{ | ||
result: number; | ||
@@ -42,6 +43,6 @@ }>>; | ||
count: number; | ||
}>, input: AsyncIterable<import("../transport/message").TransportMessage<{ | ||
}>, input: AsyncIterable<TransportMessage<{ | ||
msg: string; | ||
ignore: boolean; | ||
}>>, output: import("it-pushable").Pushable<import("../transport/message").TransportMessage<{ | ||
}>>, output: import("it-pushable").Pushable<TransportMessage<{ | ||
response: string; | ||
@@ -48,0 +49,0 @@ }>, void, unknown>) => Promise<void>; |
@@ -5,3 +5,3 @@ import http from 'http'; | ||
import { reply } from '../transport/message'; | ||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'; | ||
import { afterAll, describe, expect, test } from 'vitest'; | ||
import { createWebSocketServer, createWsTransports, onServerReady, } from '../transport/util'; | ||
@@ -44,2 +44,25 @@ import { createServer } from '../router/server'; | ||
.finalize(); | ||
const OrderingServiceConstructor = () => ServiceBuilder.create('test') | ||
.initialState({ | ||
msgs: [], | ||
}) | ||
.defineProcedure('add', { | ||
type: 'rpc', | ||
input: Type.Object({ n: Type.Number() }), | ||
output: Type.Object({ ok: Type.Boolean() }), | ||
async handler(ctx, msg) { | ||
const { n } = msg.payload; | ||
ctx.state.msgs.push(n); | ||
return reply(msg, { ok: true }); | ||
}, | ||
}) | ||
.defineProcedure('getAll', { | ||
type: 'rpc', | ||
input: Type.Object({}), | ||
output: Type.Object({ msgs: Type.Array(Type.Number()) }), | ||
async handler(ctx, msg) { | ||
return reply(msg, { msgs: ctx.state.msgs }); | ||
}, | ||
}) | ||
.finalize(); | ||
test('serialize service to jsonschema', () => { | ||
@@ -101,26 +124,22 @@ const service = TestServiceConstructor(); | ||
test('stream basic', async () => { | ||
const [i, o] = asClientStream(initialState, service.procedures.echo); | ||
i.push({ msg: 'abc', ignore: false }); | ||
i.push({ msg: 'def', ignore: true }); | ||
i.push({ msg: 'ghi', ignore: false }); | ||
i.end(); | ||
await expect(o.next().then((res) => res.value)).resolves.toStrictEqual({ | ||
const [input, output] = asClientStream(initialState, service.procedures.echo); | ||
input.push({ msg: 'abc', ignore: false }); | ||
input.push({ msg: 'def', ignore: true }); | ||
input.push({ msg: 'ghi', ignore: false }); | ||
input.end(); | ||
await expect(output.next().then((res) => res.value)).resolves.toStrictEqual({ | ||
response: 'abc', | ||
}); | ||
await expect(o.next().then((res) => res.value)).resolves.toStrictEqual({ | ||
await expect(output.next().then((res) => res.value)).resolves.toStrictEqual({ | ||
response: 'ghi', | ||
}); | ||
expect(o.readableLength).toBe(0); | ||
expect(output.readableLength).toBe(0); | ||
}); | ||
}); | ||
const port = 4445; | ||
describe('client <-> server integration test', () => { | ||
describe('client <-> server integration test', async () => { | ||
const server = http.createServer(); | ||
let wss; | ||
beforeAll(async () => { | ||
await onServerReady(server, port); | ||
wss = await createWebSocketServer(server); | ||
}); | ||
const port = await onServerReady(server); | ||
const webSocketServer = await createWebSocketServer(server); | ||
afterAll(() => { | ||
wss.clients.forEach((socket) => { | ||
webSocketServer.clients.forEach((socket) => { | ||
socket.close(); | ||
@@ -131,6 +150,6 @@ }); | ||
test('rpc', async () => { | ||
const [ct, st] = await createWsTransports(port, wss); | ||
const [clientTransport, serverTransport] = createWsTransports(port, webSocketServer); | ||
const serviceDefs = { test: TestServiceConstructor() }; | ||
const server = await createServer(st, serviceDefs); | ||
const client = createClient(ct); | ||
const server = await createServer(serverTransport, serviceDefs); | ||
const client = createClient(clientTransport); | ||
await expect(client.test.add({ n: 3 })).resolves.toStrictEqual({ | ||
@@ -141,15 +160,15 @@ result: 3, | ||
test('stream', async () => { | ||
const [ct, st] = await createWsTransports(port, wss); | ||
const [clientTransport, serverTransport] = createWsTransports(port, webSocketServer); | ||
const serviceDefs = { test: TestServiceConstructor() }; | ||
const server = await createServer(st, serviceDefs); | ||
const client = createClient(ct); | ||
const [i, o, close] = await client.test.echo(); | ||
i.push({ msg: 'abc', ignore: false }); | ||
i.push({ msg: 'def', ignore: true }); | ||
i.push({ msg: 'ghi', ignore: false }); | ||
i.end(); | ||
await expect(o.next().then((res) => res.value)).resolves.toStrictEqual({ | ||
const server = await createServer(serverTransport, serviceDefs); | ||
const client = createClient(clientTransport); | ||
const [input, output, close] = await client.test.echo(); | ||
input.push({ msg: 'abc', ignore: false }); | ||
input.push({ msg: 'def', ignore: true }); | ||
input.push({ msg: 'ghi', ignore: false }); | ||
input.end(); | ||
await expect(output.next().then((res) => res.value)).resolves.toStrictEqual({ | ||
response: 'abc', | ||
}); | ||
await expect(o.next().then((res) => res.value)).resolves.toStrictEqual({ | ||
await expect(output.next().then((res) => res.value)).resolves.toStrictEqual({ | ||
response: 'ghi', | ||
@@ -159,2 +178,23 @@ }); | ||
}); | ||
test('message order is preserved in the face of disconnects', async () => { | ||
const [clientTransport, serverTransport] = createWsTransports(port, webSocketServer); | ||
const serviceDefs = { test: OrderingServiceConstructor() }; | ||
const server = await createServer(serverTransport, serviceDefs); | ||
const client = createClient(clientTransport); | ||
const expected = []; | ||
for (let i = 0; i < 50; i++) { | ||
expected.push(i); | ||
if (i == 10) { | ||
clientTransport.ws?.close(); | ||
} | ||
if (i == 42) { | ||
clientTransport.ws?.terminate(); | ||
} | ||
await client.test.add({ | ||
n: i, | ||
}); | ||
} | ||
const res = await client.test.getAll({}); | ||
return expect(res.msgs).toStrictEqual(expected); | ||
}); | ||
}); |
import { MessageId, OpaqueTransportMessage } from '../transport/message'; | ||
import { Transport } from '../transport/types'; | ||
export declare const StupidlyLargeService: () => { | ||
name: "test"; | ||
state: {}; | ||
procedures: { | ||
f1: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f2: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f3: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f4: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f5: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f6: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f7: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f8: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f9: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f10: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f11: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f12: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f13: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f14: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f15: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f16: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f17: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f18: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f19: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f20: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f21: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f22: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f23: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f24: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f25: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f26: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f27: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f28: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f29: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f30: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f31: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f32: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f33: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f34: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f35: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f36: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f37: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f38: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f39: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f40: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f41: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f42: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f43: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f44: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f45: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f46: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f47: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f48: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
} & { | ||
f49: { | ||
input: import("@sinclair/typebox").TObject<{ | ||
a: import("@sinclair/typebox").TNumber; | ||
}>; | ||
output: import("@sinclair/typebox").TObject<{ | ||
b: import("@sinclair/typebox").TNumber; | ||
}>; | ||
handler: (context: import("..").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{ | ||
a: number; | ||
}>) => Promise<import("../transport/message").TransportMessage<{ | ||
b: number; | ||
}>>; | ||
type: "rpc"; | ||
}; | ||
}; | ||
}; | ||
export declare class MockTransport extends Transport { | ||
@@ -4,0 +744,0 @@ constructor(clientId: string); |
@@ -9,6 +9,8 @@ import { describe, expect, test } from 'vitest'; | ||
import { createClient } from '../router/client'; | ||
const input = Type.Object({ a: Type.Number() }); | ||
const output = Type.Object({ b: Type.Number() }); | ||
const fnBody = { | ||
type: 'rpc', | ||
input: Type.Object({ a: Type.Number() }), | ||
output: Type.Object({ b: Type.Number() }), | ||
input, | ||
output, | ||
async handler(_state, msg) { | ||
@@ -20,3 +22,3 @@ return reply(msg, { b: msg.payload.a }); | ||
// see: https://github.com/microsoft/TypeScript/issues/33541 | ||
const svc = () => ServiceBuilder.create('test') | ||
export const StupidlyLargeService = () => ServiceBuilder.create('test') | ||
.defineProcedure('f1', fnBody) | ||
@@ -85,10 +87,10 @@ .defineProcedure('f2', fnBody) | ||
test('service with many procedures hits typescript limit', () => { | ||
expect(serializeService(svc())).toBeTruthy(); | ||
expect(serializeService(StupidlyLargeService())).toBeTruthy(); | ||
}); | ||
test('serverclient should support many services with many procedures', async () => { | ||
const listing = { | ||
a: svc(), | ||
b: svc(), | ||
c: svc(), | ||
d: svc(), | ||
a: StupidlyLargeService(), | ||
b: StupidlyLargeService(), | ||
c: StupidlyLargeService(), | ||
d: StupidlyLargeService(), | ||
}; | ||
@@ -95,0 +97,0 @@ const server = await createServer(new MockTransport('SERVER'), listing); |
@@ -14,2 +14,2 @@ export { serializeService, ServiceBuilder } from './router/builder'; | ||
export { WebSocketTransport } from './transport/ws'; | ||
export { createWebSocketServer, onServerReady, createWsTransports, waitForMessage, waitForSocketReady, createWebSocketClient, } from './transport/util'; | ||
export { createWebSocketServer, onServerReady, createWsTransports, waitForMessage, createLocalWebSocketClient, } from './transport/util'; |
@@ -9,2 +9,2 @@ export { serializeService, ServiceBuilder } from './router/builder'; | ||
export { WebSocketTransport } from './transport/ws'; | ||
export { createWebSocketServer, onServerReady, createWsTransports, waitForMessage, waitForSocketReady, createWebSocketClient, } from './transport/util'; | ||
export { createWebSocketServer, onServerReady, createWsTransports, waitForMessage, createLocalWebSocketClient, } from './transport/util'; |
@@ -26,15 +26,15 @@ import { pushable } from 'it-pushable'; | ||
// stream case | ||
const i = pushable({ objectMode: true }); | ||
const o = pushable({ objectMode: true }); | ||
// i -> transport | ||
const inputStream = pushable({ objectMode: true }); | ||
const outputStream = pushable({ objectMode: true }); | ||
// input -> transport | ||
// this gets cleaned up on i.end() which is called by closeHandler | ||
(async () => { | ||
for await (const rawIn of i) { | ||
for await (const rawIn of inputStream) { | ||
transport.send(msg(transport.clientId, 'SERVER', serviceName, procName, rawIn)); | ||
} | ||
})(); | ||
// transport -> o | ||
// transport -> output | ||
const listener = (msg) => { | ||
if (msg.serviceName === serviceName && msg.procedureName === procName) { | ||
o.push(msg.payload); | ||
outputStream.push(msg.payload); | ||
} | ||
@@ -44,7 +44,7 @@ }; | ||
const closeHandler = () => { | ||
i.end(); | ||
o.end(); | ||
inputStream.end(); | ||
outputStream.end(); | ||
transport.removeMessageListener(listener); | ||
}; | ||
return [i, o, closeHandler]; | ||
return [inputStream, outputStream, closeHandler]; | ||
} | ||
@@ -51,0 +51,0 @@ else { |
@@ -33,2 +33,3 @@ import { type Static, TSchema } from '@sinclair/typebox'; | ||
export declare const TransportAckSchema: import("@sinclair/typebox").TObject<{ | ||
id: import("@sinclair/typebox").TString; | ||
from: import("@sinclair/typebox").TString; | ||
@@ -35,0 +36,0 @@ ack: import("@sinclair/typebox").TString; |
@@ -15,2 +15,3 @@ import { Type } from '@sinclair/typebox'; | ||
export const TransportAckSchema = Type.Object({ | ||
id: Type.String(), | ||
from: Type.String(), | ||
@@ -34,2 +35,3 @@ ack: Type.String(), | ||
return { | ||
id: nanoid(), | ||
from: msg.to, | ||
@@ -36,0 +38,0 @@ ack: msg.id, |
@@ -15,2 +15,3 @@ import { Value } from '@sinclair/typebox/value'; | ||
onMessage(msg) { | ||
// TODO: try catch from string buf | ||
const parsedMsg = this.codec.fromStringBuf(msg.toString()); | ||
@@ -34,2 +35,5 @@ if (Value.Check(TransportAckSchema, parsedMsg)) { | ||
} | ||
else { | ||
// TODO: warn on malformed | ||
} | ||
} | ||
@@ -36,0 +40,0 @@ addMessageListener(handler) { |
@@ -6,8 +6,8 @@ /// <reference types="node" /> | ||
import { Transport } from './types'; | ||
import { WebSocketTransport } from './ws'; | ||
import { OpaqueTransportMessage } from './message'; | ||
export declare function createWebSocketServer(server: http.Server): Promise<WebSocket.Server<typeof WebSocket, typeof http.IncomingMessage>>; | ||
export declare function onServerReady(server: http.Server, port: number): Promise<void>; | ||
export declare function createWsTransports(port: number, wss: WebSocketServer): Promise<[Transport, Transport]>; | ||
export declare function waitForSocketReady(socket: WebSocket): Promise<void>; | ||
export declare function createWebSocketClient(port: number): Promise<WebSocket>; | ||
export declare function onServerReady(server: http.Server): Promise<number>; | ||
export declare function createLocalWebSocketClient(port: number): Promise<WebSocket>; | ||
export declare function createWsTransports(port: number, wss: WebSocketServer): [WebSocketTransport, WebSocketTransport]; | ||
export declare function waitForMessage(t: Transport, filter?: (msg: OpaqueTransportMessage) => boolean): Promise<unknown>; |
@@ -7,27 +7,32 @@ import WebSocket from 'isomorphic-ws'; | ||
} | ||
export async function onServerReady(server, port) { | ||
return new Promise((resolve) => { | ||
server.listen(port, resolve); | ||
}); | ||
} | ||
export async function createWsTransports(port, wss) { | ||
return new Promise((resolve) => { | ||
const clientSockPromise = createWebSocketClient(port); | ||
wss.on('connection', async (serverSock) => { | ||
resolve([ | ||
new WebSocketTransport(await clientSockPromise, 'client'), | ||
new WebSocketTransport(serverSock, 'SERVER'), | ||
]); | ||
export async function onServerReady(server) { | ||
return new Promise((resolve, reject) => { | ||
server.listen(() => { | ||
const addr = server.address(); | ||
if (typeof addr === 'object' && addr) { | ||
resolve(addr.port); | ||
} | ||
else { | ||
reject(new Error("couldn't find a port to allocate")); | ||
} | ||
}); | ||
}); | ||
} | ||
export async function waitForSocketReady(socket) { | ||
return new Promise((resolve) => { | ||
socket.addEventListener('open', () => resolve()); | ||
}); | ||
export async function createLocalWebSocketClient(port) { | ||
return new WebSocket(`ws://localhost:${port}`); | ||
} | ||
export async function createWebSocketClient(port) { | ||
const client = new WebSocket(`ws://localhost:${port}`); | ||
await waitForSocketReady(client); | ||
return client; | ||
export function createWsTransports(port, wss) { | ||
return [ | ||
new WebSocketTransport(async () => { | ||
return createLocalWebSocketClient(port); | ||
}, 'client'), | ||
new WebSocketTransport(async () => { | ||
return new Promise((resolve) => { | ||
wss.on('connection', async function onConnect(serverSock) { | ||
wss.removeListener('connection', onConnect); | ||
resolve(serverSock); | ||
}); | ||
}); | ||
}, 'SERVER'), | ||
]; | ||
} | ||
@@ -34,0 +39,0 @@ export async function waitForMessage(t, filter) { |
/// <reference types="ws" /> | ||
import type WebSocket from 'isomorphic-ws'; | ||
import WebSocket from 'isomorphic-ws'; | ||
import { Transport } from './types'; | ||
import { MessageId, OpaqueTransportMessage, TransportClientId } from './message'; | ||
interface Options { | ||
retryIntervalMs: number; | ||
} | ||
type WebSocketResult = { | ||
ws: WebSocket; | ||
} | { | ||
err: string; | ||
}; | ||
export declare class WebSocketTransport extends Transport { | ||
ws: WebSocket; | ||
constructor(ws: WebSocket, clientId: TransportClientId); | ||
wsGetter: () => Promise<WebSocket>; | ||
ws?: WebSocket; | ||
destroyed: boolean; | ||
reconnectPromise?: Promise<WebSocketResult>; | ||
options: Options; | ||
sendQueue: Array<MessageId>; | ||
constructor(wsGetter: () => Promise<WebSocket>, clientId: TransportClientId, options?: Partial<Options>); | ||
private tryConnect; | ||
send(msg: OpaqueTransportMessage): MessageId; | ||
close(): Promise<void>; | ||
close(): Promise<void | undefined>; | ||
} | ||
export {}; |
import { Transport } from './types'; | ||
import { NaiveJsonCodec } from '../codec/json'; | ||
// TODO should answer: | ||
// - how do we handle graceful client disconnects? (i.e. close tab) | ||
// - how do we handle graceful service disconnects (i.e. a fuck off message)? | ||
// - how do we handle forceful client disconnects? (i.e. broken connection, offline) | ||
// - how do we handle forceful service disconnects (i.e. a crash)? | ||
const defaultOptions = { | ||
retryIntervalMs: 250, | ||
}; | ||
export class WebSocketTransport extends Transport { | ||
wsGetter; | ||
ws; | ||
constructor(ws, clientId) { | ||
destroyed; | ||
reconnectPromise; | ||
options; | ||
sendQueue; | ||
constructor(wsGetter, clientId, options) { | ||
super(NaiveJsonCodec, clientId); | ||
this.ws = ws; | ||
ws.onmessage = (msg) => this.onMessage(msg.data.toString()); | ||
this.destroyed = false; | ||
this.wsGetter = wsGetter; | ||
this.options = { ...defaultOptions, ...options }; | ||
this.sendQueue = []; | ||
this.tryConnect(); | ||
} | ||
// postcondition: ws is concretely a WebSocket | ||
async tryConnect() { | ||
// wait until it's ready or we get an error | ||
this.reconnectPromise ??= new Promise(async (resolve) => { | ||
const ws = await this.wsGetter(); | ||
if (ws.readyState === ws.OPEN) { | ||
return resolve({ ws }); | ||
} | ||
if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) { | ||
return resolve({ err: 'ws is closing or closed' }); | ||
} | ||
ws.addEventListener('open', function onOpen() { | ||
ws.removeEventListener('open', onOpen); | ||
resolve({ ws }); | ||
}); | ||
ws.addEventListener('error', function onError(err) { | ||
ws.removeEventListener('error', onError); | ||
resolve({ err: err.message }); | ||
}); | ||
ws.addEventListener('close', function onClose(evt) { | ||
ws.removeEventListener('close', onClose); | ||
resolve({ err: evt.reason }); | ||
}); | ||
}); | ||
const res = await this.reconnectPromise; | ||
// only send if we resolved a valid websocket | ||
if ('ws' in res && res.ws.readyState === res.ws.OPEN) { | ||
this.ws = res.ws; | ||
this.ws.onmessage = (msg) => this.onMessage(msg.data.toString()); | ||
this.ws.onclose = () => { | ||
this.reconnectPromise = undefined; | ||
this.tryConnect().catch(); | ||
}; | ||
// send outstanding | ||
for (const id of this.sendQueue) { | ||
const msg = this.sendBuffer.get(id); | ||
if (!msg) { | ||
throw new Error('tried to resend a message we received an ack for'); | ||
} | ||
this.ws.send(this.codec.toStringBuf(msg)); | ||
} | ||
this.sendQueue = []; | ||
return; | ||
} | ||
// otherwise try and reconnect again | ||
this.reconnectPromise = undefined; | ||
setTimeout(() => this.tryConnect(), this.options.retryIntervalMs); | ||
} | ||
send(msg) { | ||
const id = msg.id; | ||
this.ws.send(this.codec.toStringBuf(msg)); | ||
if (this.destroyed) { | ||
throw new Error('ws is destroyed, cant send'); | ||
} | ||
this.sendBuffer.set(id, msg); | ||
if (this.ws && this.ws.readyState === this.ws.OPEN) { | ||
this.ws.send(this.codec.toStringBuf(msg)); | ||
} | ||
else { | ||
this.sendQueue.push(id); | ||
this.tryConnect().catch(); | ||
} | ||
return id; | ||
} | ||
async close() { | ||
return this.ws.close(); | ||
this.destroyed = true; | ||
return this.ws?.close(); | ||
} | ||
} |
import http from 'http'; | ||
import { WebSocketTransport } from './ws'; | ||
import { describe, test, expect, beforeAll, afterAll } from 'vitest'; | ||
import { createWebSocketClient, createWebSocketServer, onServerReady, waitForMessage, } from './util'; | ||
const port = 4444; | ||
describe('sending and receiving across websockets works', () => { | ||
import { describe, test, expect, afterAll } from 'vitest'; | ||
import { createWebSocketServer, createWsTransports, onServerReady, waitForMessage, } from './util'; | ||
import { nanoid } from 'nanoid'; | ||
const getMsg = () => ({ | ||
id: nanoid(), | ||
from: 'client', | ||
to: 'SERVER', | ||
serviceName: 'test', | ||
procedureName: 'test', | ||
payload: { | ||
msg: 'cool', | ||
test: Math.random(), | ||
}, | ||
}); | ||
describe('sending and receiving across websockets works', async () => { | ||
const server = http.createServer(); | ||
let wss; | ||
beforeAll(async () => { | ||
await onServerReady(server, port); | ||
wss = await createWebSocketServer(server); | ||
}); | ||
const port = await onServerReady(server); | ||
const wss = await createWebSocketServer(server); | ||
afterAll(() => { | ||
@@ -20,23 +27,50 @@ wss.clients.forEach((socket) => { | ||
test('basic send/receive', async () => { | ||
let serverTransport; | ||
wss.on('connection', (conn) => { | ||
serverTransport = new WebSocketTransport(conn, 'SERVER'); | ||
const [clientTransport, serverTransport] = createWsTransports(port, wss); | ||
const msg = getMsg(); | ||
clientTransport.send(msg); | ||
return expect(waitForMessage(serverTransport, (recv) => recv.id === msg.id)).resolves.toStrictEqual(msg.payload); | ||
}); | ||
}); | ||
describe('retry logic', async () => { | ||
const server = http.createServer(); | ||
const port = await onServerReady(server); | ||
const wss = await createWebSocketServer(server); | ||
afterAll(() => { | ||
wss.clients.forEach((socket) => { | ||
socket.close(); | ||
}); | ||
const clientSoc = await createWebSocketClient(port); | ||
const clientTransport = new WebSocketTransport(clientSoc, 'client'); | ||
const msg = { | ||
msg: 'cool', | ||
test: 123, | ||
}; | ||
clientTransport.send({ | ||
id: '1', | ||
from: 'client', | ||
to: 'SERVER', | ||
serviceName: 'test', | ||
procedureName: 'test', | ||
payload: msg, | ||
}); | ||
expect(serverTransport).toBeTruthy(); | ||
return expect(waitForMessage(serverTransport)).resolves.toStrictEqual(msg); | ||
server.close(); | ||
}); | ||
// TODO: right now, we only test client-side disconnects, we probably | ||
// need to also write tests for server-side crashes (but this involves clearing/restoring state) | ||
// not going to worry about this rn but for future | ||
test('ws transport is recreated after clean disconnect', async () => { | ||
const [clientTransport, serverTransport] = createWsTransports(port, wss); | ||
const msg1 = getMsg(); | ||
const msg2 = getMsg(); | ||
clientTransport.send(msg1); | ||
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload); | ||
clientTransport.ws?.close(); | ||
clientTransport.send(msg2); | ||
return expect(waitForMessage(serverTransport, (recv) => recv.id === msg2.id)).resolves.toStrictEqual(msg2.payload); | ||
}); | ||
test('ws transport is recreated after unclean disconnect', async () => { | ||
const [clientTransport, serverTransport] = createWsTransports(port, wss); | ||
const msg1 = getMsg(); | ||
const msg2 = getMsg(); | ||
clientTransport.send(msg1); | ||
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload); | ||
clientTransport.ws?.terminate(); | ||
clientTransport.send(msg2); | ||
return expect(waitForMessage(serverTransport, (recv) => recv.id === msg2.id)).resolves.toStrictEqual(msg2.payload); | ||
}); | ||
test('ws transport is not recreated after manually closing', async () => { | ||
const [clientTransport, serverTransport] = createWsTransports(port, wss); | ||
const msg1 = getMsg(); | ||
const msg2 = getMsg(); | ||
clientTransport.send(msg1); | ||
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload); | ||
clientTransport.close(); | ||
return expect(() => clientTransport.send(msg2)).toThrow(new Error('ws is destroyed, cant send')); | ||
}); | ||
}); |
@@ -5,3 +5,3 @@ { | ||
"description": "It's like tRPC but... with JSON Schema Support, duplex streaming and support for service multiplexing. Transport agnostic!", | ||
"version": "0.1.10", | ||
"version": "0.2.0", | ||
"type": "module", | ||
@@ -33,3 +33,4 @@ "main": "index.js", | ||
"release": "npm publish --access public", | ||
"test": "vitest" | ||
"test": "vitest", | ||
"bench": "vitest bench" | ||
}, | ||
@@ -36,0 +37,0 @@ "engines": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
86409
48
2176
3