@replit/river 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,13 +6,3 @@ It's like tRPC but...
6
6
  - with full-duplex streaming
7
7
  - with support for service multiplexing
8
8
  - over WebSockets
9
-
10
- ## Levels of abstraction
11
-
12
- - Router
13
- - Service
14
- - Procedure
15
-
16
- ## TODO
17
-
18
- - support broadcast
@@ -1,4 +1,3 @@
1
- import { TransportMessage } from '../transport/message';
2
1
  export declare const EchoRequest: import("@sinclair/typebox").TObject<{
3
2
  msg: import("@sinclair/typebox").TString;
4
3
  ignore: import("@sinclair/typebox").TBoolean;
@@ -21,9 +20,9 @@ export declare const TestServiceConstructor: () => {
21
20
  }>;
22
21
  handler: (context: import("..").ServiceContextWithState<{
23
22
  count: number;
24
- }>, input: TransportMessage<{
23
+ }>, input: import("../transport/message").TransportMessage<{
25
24
  n: number;
26
- }>) => Promise<TransportMessage<{
25
+ }>) => Promise<import("../transport/message").TransportMessage<{
27
26
  result: number;
28
27
  }>>;
29
28
  type: "rpc";
@@ -39,10 +38,10 @@ export declare const TestServiceConstructor: () => {
39
38
  }>;
40
39
  handler: (context: import("..").ServiceContextWithState<{
41
40
  count: number;
42
- }>, input: AsyncIterable<TransportMessage<{
41
+ }>, input: AsyncIterable<import("../transport/message").TransportMessage<{
43
42
  msg: string;
44
43
  ignore: boolean;
45
- }>>, output: import("it-pushable").Pushable<TransportMessage<{
44
+ }>>, output: import("it-pushable").Pushable<import("../transport/message").TransportMessage<{
46
45
  response: string;
47
46
  }>, void, unknown>) => Promise<void>;
48
47
  type: "stream";
@@ -20,4 +20,10 @@ describe('naive json codec', () => {
20
20
  };
21
21
  expect(NaiveJsonCodec.fromStringBuf(NaiveJsonCodec.toStringBuf(msg))).toStrictEqual(msg);
22
22
  });
23
+ test('invalid json returns null', () => {
24
+ expect(NaiveJsonCodec.fromStringBuf('')).toBeNull();
25
+ expect(NaiveJsonCodec.fromStringBuf('[')).toBeNull();
26
+ expect(NaiveJsonCodec.fromStringBuf('[{}')).toBeNull();
27
+ expect(NaiveJsonCodec.fromStringBuf('{"a":1}[]')).toBeNull();
28
+ });
23
29
  });
@@ -1,4 +1,11 @@
1
1
  export const NaiveJsonCodec = {
2
2
  toStringBuf: JSON.stringify,
3
- fromStringBuf: JSON.parse,
3
+ fromStringBuf: (s) => {
4
+ try {
5
+ return JSON.parse(s);
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ },
4
11
  };
@@ -1,4 +1,4 @@
1
1
  export interface Codec {
2
2
  toStringBuf(obj: object): string;
3
- fromStringBuf(buf: string): object;
3
+ fromStringBuf(buf: string): object | null;
4
4
  }
@@ -0,0 +1,15 @@
1
+ declare const LoggingLevels: {
2
+ readonly info: 0;
3
+ readonly warn: 1;
4
+ readonly error: 2;
5
+ };
6
+ type LoggingLevel = keyof typeof LoggingLevels;
7
+ export type Logger = {
8
+ minLevel: LoggingLevel;
9
+ } & {
10
+ [key in LoggingLevel]: (msg: string) => void;
11
+ };
12
+ export declare let log: Logger | undefined;
13
+ export declare function bindLogger(write: (msg: string) => void, color?: boolean): void;
14
+ export declare function setLevel(level: LoggingLevel): void;
15
+ export {};
@@ -0,0 +1,29 @@
1
+ const LoggingLevels = {
2
+ info: 0,
3
+ warn: 1,
4
+ error: 2,
5
+ };
6
+ export let log;
7
+ const defaultLoggingLevel = 'warn';
8
+ export function bindLogger(write, color) {
9
+ const info = color ? '\u001b[37minfo\u001b[0m' : 'info';
10
+ const warn = color ? '\u001b[33mwarn\u001b[0m' : 'warn';
11
+ const error = color ? '\u001b[31merr\u001b[0m' : 'err';
12
+ log = {
13
+ info: (msg) => log &&
14
+ LoggingLevels[log.minLevel] <= 0 &&
15
+ write(`[river:${info}] ${msg}`),
16
+ warn: (msg) => log &&
17
+ LoggingLevels[log.minLevel] <= 1 &&
18
+ write(`[river:${warn}] ${msg}`),
19
+ error: (msg) => log &&
20
+ LoggingLevels[log.minLevel] <= 2 &&
21
+ write(`[river:${error}] ${msg}`),
22
+ minLevel: log?.minLevel ?? defaultLoggingLevel,
23
+ };
24
+ }
25
+ export function setLevel(level) {
26
+ if (log) {
27
+ log.minLevel = level;
28
+ }
29
+ }
@@ -1,12 +1,15 @@
1
1
  import { Value } from '@sinclair/typebox/value';
2
2
  import { pushable } from 'it-pushable';
3
+ import { log } from '../logging';
3
4
  export async function createServer(transport, services, extendedContext) {
4
5
  const contextMap = new Map();
5
6
  const streamMap = new Map();
6
7
  function getContext(service) {
7
8
  const context = contextMap.get(service);
8
9
  if (!context) {
9
- throw new Error(`No context found for ${service.name}`);
10
+ const err = `No context found for ${service.name}`;
11
+ log?.error(err);
12
+ throw new Error(err);
10
13
  }
11
14
  return context;
12
15
  }
@@ -38,7 +41,6 @@ export async function createServer(transport, services, extendedContext) {
38
41
  }
39
42
  }
40
43
  const handler = async (msg) => {
41
- // TODO: log msgs received
42
44
  if (msg.to !== 'SERVER') {
43
45
  return;
44
46
  }
@@ -66,10 +68,11 @@ export async function createServer(transport, services, extendedContext) {
66
68
  return;
67
69
  }
68
70
  else {
69
- // TODO: log invalid payload
71
+ log?.error(`${transport.clientId} -- procedure ${msg.serviceName}.${msg.procedureName} received invalid payload: ${inputMessage.payload}`);
70
72
  }
71
73
  }
72
74
  }
75
+ log?.warn(`${transport.clientId} -- couldn't find a matching procedure for ${msg.serviceName}.${msg.procedureName}`);
73
76
  };
74
77
  transport.addMessageListener(handler);
75
78
  return {
@@ -1,5 +1,6 @@
1
1
  import { Value } from '@sinclair/typebox/value';
2
2
  import { OpaqueTransportMessageSchema, TransportAckSchema, ack, } from './message';
3
+ import { log } from '../logging';
3
4
  export class Transport {
4
5
  codec;
5
6
  clientId;
@@ -12,15 +13,20 @@ export class Transport {
12
13
  this.clientId = clientId;
13
14
  }
14
15
  onMessage(msg) {
15
- // TODO: try catch from string buf
16
- const parsedMsg = this.codec.fromStringBuf(msg.toString());
16
+ const parsedMsg = this.codec.fromStringBuf(msg);
17
+ if (parsedMsg === null) {
18
+ log?.warn(`${this.clientId} -- received malformed msg: ${msg}`);
19
+ return;
20
+ }
17
21
  if (Value.Check(TransportAckSchema, parsedMsg)) {
18
22
  // process ack
23
+ log?.info(`${this.clientId} -- received ack: ${msg}`);
19
24
  if (this.sendBuffer.has(parsedMsg.ack)) {
20
25
  this.sendBuffer.delete(parsedMsg.ack);
21
26
  }
22
27
  }
23
28
  else if (Value.Check(OpaqueTransportMessageSchema, parsedMsg)) {
29
+ log?.info(`${this.clientId} -- received msg: ${msg}`);
24
30
  // ignore if not for us
25
31
  if (parsedMsg.to !== this.clientId && parsedMsg.to !== 'broadcast') {
26
32
  return;
@@ -29,10 +35,12 @@ export class Transport {
29
35
  for (const handler of this.handlers) {
30
36
  handler(parsedMsg);
31
37
  }
32
- this.send(ack(parsedMsg));
38
+ const ackMsg = ack(parsedMsg);
39
+ ackMsg.from = this.clientId;
40
+ this.send(ackMsg);
33
41
  }
34
42
  else {
35
- // TODO: warn on malformed
43
+ log?.warn(`${this.clientId} -- received invalid transport msg: ${msg}`);
36
44
  }
37
45
  }
38
46
  addMessageListener(handler) {
@@ -1,5 +1,6 @@
1
1
  import { Transport } from './types';
2
2
  import { NaiveJsonCodec } from '../codec/json';
3
+ import { log } from '../logging';
3
4
  const defaultOptions = {
4
5
  retryIntervalMs: 250,
5
6
  };
@@ -22,6 +23,7 @@ export class WebSocketTransport extends Transport {
22
23
  async tryConnect() {
23
24
  // wait until it's ready or we get an error
24
25
  this.reconnectPromise ??= new Promise(async (resolve) => {
26
+ log?.info(`${this.clientId} -- establishing a new websocket`);
25
27
  const ws = await this.wsGetter();
26
28
  if (ws.readyState === ws.OPEN) {
27
29
  return resolve({ ws });
@@ -45,6 +47,7 @@ export class WebSocketTransport extends Transport {
45
47
  const res = await this.reconnectPromise;
46
48
  // only send if we resolved a valid websocket
47
49
  if ('ws' in res && res.ws.readyState === res.ws.OPEN) {
50
+ log?.info(`${this.clientId} -- websocket ok`);
48
51
  this.ws = res.ws;
49
52
  this.ws.onmessage = (msg) => this.onMessage(msg.data.toString());
50
53
  this.ws.onclose = () => {
@@ -55,33 +58,42 @@ export class WebSocketTransport extends Transport {
55
58
  for (const id of this.sendQueue) {
56
59
  const msg = this.sendBuffer.get(id);
57
60
  if (!msg) {
58
- throw new Error('tried to resend a message we received an ack for');
61
+ const err = 'tried to resend a message we received an ack for';
62
+ log?.error(err);
63
+ throw new Error(err);
59
64
  }
65
+ log?.info(`${this.clientId} -- sending ${JSON.stringify(msg)}`);
60
66
  this.ws.send(this.codec.toStringBuf(msg));
61
67
  }
62
68
  this.sendQueue = [];
63
69
  return;
64
70
  }
65
71
  // otherwise try and reconnect again
72
+ log?.warn(`${this.clientId} -- websocket failed, trying again in ${this.options.retryIntervalMs}ms`);
66
73
  this.reconnectPromise = undefined;
67
74
  setTimeout(() => this.tryConnect(), this.options.retryIntervalMs);
68
75
  }
69
76
  send(msg) {
70
77
  const id = msg.id;
71
78
  if (this.destroyed) {
72
- throw new Error('ws is destroyed, cant send');
79
+ const err = 'ws is destroyed, cant send';
80
+ log?.error(err);
81
+ throw new Error(err);
73
82
  }
74
83
  this.sendBuffer.set(id, msg);
75
84
  if (this.ws && this.ws.readyState === this.ws.OPEN) {
85
+ log?.info(`${this.clientId} -- sending ${JSON.stringify(msg)}`);
76
86
  this.ws.send(this.codec.toStringBuf(msg));
77
87
  }
78
88
  else {
89
+ log?.info(`${this.clientId} -- transport not ready, queuing ${JSON.stringify(msg)}`);
79
90
  this.sendQueue.push(id);
80
91
  this.tryConnect().catch();
81
92
  }
82
93
  return id;
83
94
  }
84
95
  async close() {
96
+ log?.info('manually closed ws');
85
97
  this.destroyed = true;
86
98
  return this.ws?.close();
87
99
  }
package/package.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "name": "@replit/river",
3
3
  "sideEffects": false,
4
4
  "description": "It's like tRPC but... with JSON Schema Support, duplex streaming and support for service multiplexing. Transport agnostic!",
5
- "version": "0.2.0",
5
+ "version": "0.2.2",
6
6
  "type": "module",
7
- "main": "index.js",
8
- "types": "index.d.ts",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
9
  "files": [
10
10
  "dist"
11
11
  ],