@replit/river 0.3.1 → 0.5.0

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.
Files changed (74) hide show
  1. package/dist/__tests__/bandwidth.bench.d.ts +1 -0
  2. package/dist/__tests__/bandwidth.bench.d.ts.map +1 -0
  3. package/dist/__tests__/bandwidth.bench.js +12 -5
  4. package/dist/__tests__/e2e.test.d.ts +2 -0
  5. package/dist/__tests__/e2e.test.d.ts.map +1 -0
  6. package/dist/__tests__/e2e.test.js +153 -0
  7. package/dist/__tests__/fixtures.d.ts +155 -0
  8. package/dist/__tests__/fixtures.d.ts.map +1 -0
  9. package/dist/__tests__/fixtures.js +129 -0
  10. package/dist/__tests__/handler.test.d.ts +2 -0
  11. package/dist/__tests__/handler.test.d.ts.map +1 -0
  12. package/dist/__tests__/handler.test.js +71 -0
  13. package/dist/__tests__/serialize.test.d.ts +2 -0
  14. package/dist/__tests__/serialize.test.d.ts.map +1 -0
  15. package/dist/__tests__/serialize.test.js +135 -0
  16. package/dist/__tests__/typescript-stress.test.d.ts +736 -98
  17. package/dist/__tests__/typescript-stress.test.d.ts.map +1 -0
  18. package/dist/__tests__/typescript-stress.test.js +13 -1
  19. package/dist/codec/codec.test.d.ts +1 -0
  20. package/dist/codec/codec.test.d.ts.map +1 -0
  21. package/dist/codec/index.d.ts +1 -0
  22. package/dist/codec/index.d.ts.map +1 -0
  23. package/dist/codec/json.d.ts +5 -0
  24. package/dist/codec/json.d.ts.map +1 -0
  25. package/dist/codec/json.js +4 -0
  26. package/dist/codec/types.d.ts +15 -0
  27. package/dist/codec/types.d.ts.map +1 -0
  28. package/dist/logging/index.d.ts +13 -0
  29. package/dist/logging/index.d.ts.map +1 -0
  30. package/dist/logging/index.js +12 -0
  31. package/dist/router/builder.d.ts +91 -7
  32. package/dist/router/builder.d.ts.map +1 -0
  33. package/dist/router/builder.js +32 -0
  34. package/dist/router/client.d.ts +28 -3
  35. package/dist/router/client.d.ts.map +1 -0
  36. package/dist/router/client.js +37 -6
  37. package/dist/router/context.d.ts +2 -0
  38. package/dist/router/context.d.ts.map +1 -0
  39. package/dist/router/index.d.ts +1 -0
  40. package/dist/router/index.d.ts.map +1 -0
  41. package/dist/router/result.d.ts +25 -0
  42. package/dist/router/result.d.ts.map +1 -0
  43. package/dist/router/result.js +18 -0
  44. package/dist/router/server.d.ts +13 -0
  45. package/dist/router/server.d.ts.map +1 -0
  46. package/dist/router/server.js +85 -56
  47. package/dist/testUtils.d.ts +69 -2
  48. package/dist/testUtils.d.ts.map +1 -0
  49. package/dist/testUtils.js +91 -4
  50. package/dist/transport/impls/stdio.d.ts +25 -0
  51. package/dist/transport/impls/stdio.d.ts.map +1 -0
  52. package/dist/transport/impls/stdio.js +24 -0
  53. package/dist/transport/impls/stdio.test.d.ts +1 -0
  54. package/dist/transport/impls/stdio.test.d.ts.map +1 -0
  55. package/dist/transport/impls/stdio.test.js +2 -8
  56. package/dist/transport/impls/ws.d.ts +40 -1
  57. package/dist/transport/impls/ws.d.ts.map +1 -0
  58. package/dist/transport/impls/ws.js +39 -2
  59. package/dist/transport/impls/ws.test.d.ts +1 -0
  60. package/dist/transport/impls/ws.test.d.ts.map +1 -0
  61. package/dist/transport/impls/ws.test.js +8 -20
  62. package/dist/transport/index.d.ts +9 -2
  63. package/dist/transport/index.d.ts.map +1 -0
  64. package/dist/transport/index.js +7 -1
  65. package/dist/transport/message.d.ts +94 -36
  66. package/dist/transport/message.d.ts.map +1 -0
  67. package/dist/transport/message.js +66 -19
  68. package/dist/transport/message.test.d.ts +1 -0
  69. package/dist/transport/message.test.d.ts.map +1 -0
  70. package/dist/transport/message.test.js +39 -6
  71. package/dist/transport/types.d.ts +38 -2
  72. package/dist/transport/types.d.ts.map +1 -0
  73. package/dist/transport/types.js +44 -5
  74. package/package.json +1 -2
@@ -1,19 +1,7 @@
1
1
  import http from 'http';
2
2
  import { describe, test, expect, afterAll } from 'vitest';
3
- import { createWebSocketServer, createWsTransports, onServerReady, } from '../../testUtils';
4
- import { nanoid } from 'nanoid';
3
+ import { createWebSocketServer, createWsTransports, createDummyTransportMessage, onServerReady, } from '../../testUtils';
5
4
  import { waitForMessage } from '..';
6
- const getMsg = () => ({
7
- id: nanoid(),
8
- from: 'client',
9
- to: 'SERVER',
10
- serviceName: 'test',
11
- procedureName: 'test',
12
- payload: {
13
- msg: 'cool',
14
- test: Math.random(),
15
- },
16
- });
17
5
  describe('sending and receiving across websockets works', async () => {
18
6
  const server = http.createServer();
19
7
  const port = await onServerReady(server);
@@ -26,7 +14,7 @@ describe('sending and receiving across websockets works', async () => {
26
14
  });
27
15
  test('basic send/receive', async () => {
28
16
  const [clientTransport, serverTransport] = createWsTransports(port, wss);
29
- const msg = getMsg();
17
+ const msg = createDummyTransportMessage();
30
18
  clientTransport.send(msg);
31
19
  return expect(waitForMessage(serverTransport, (recv) => recv.id === msg.id)).resolves.toStrictEqual(msg.payload);
32
20
  });
@@ -46,8 +34,8 @@ describe('retry logic', async () => {
46
34
  // not going to worry about this rn but for future
47
35
  test('ws transport is recreated after clean disconnect', async () => {
48
36
  const [clientTransport, serverTransport] = createWsTransports(port, wss);
49
- const msg1 = getMsg();
50
- const msg2 = getMsg();
37
+ const msg1 = createDummyTransportMessage();
38
+ const msg2 = createDummyTransportMessage();
51
39
  clientTransport.send(msg1);
52
40
  await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
53
41
  clientTransport.ws?.close();
@@ -56,8 +44,8 @@ describe('retry logic', async () => {
56
44
  });
57
45
  test('ws transport is recreated after unclean disconnect', async () => {
58
46
  const [clientTransport, serverTransport] = createWsTransports(port, wss);
59
- const msg1 = getMsg();
60
- const msg2 = getMsg();
47
+ const msg1 = createDummyTransportMessage();
48
+ const msg2 = createDummyTransportMessage();
61
49
  clientTransport.send(msg1);
62
50
  await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
63
51
  clientTransport.ws?.terminate();
@@ -66,8 +54,8 @@ describe('retry logic', async () => {
66
54
  });
67
55
  test('ws transport is not recreated after manually closing', async () => {
68
56
  const [clientTransport, serverTransport] = createWsTransports(port, wss);
69
- const msg1 = getMsg();
70
- const msg2 = getMsg();
57
+ const msg1 = createDummyTransportMessage();
58
+ const msg2 = createDummyTransportMessage();
71
59
  clientTransport.send(msg1);
72
60
  await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
73
61
  clientTransport.close();
@@ -1,6 +1,13 @@
1
1
  import { OpaqueTransportMessage } from './message';
2
2
  import { Transport } from './types';
3
3
  export { Transport } from './types';
4
- export { TransportMessageSchema, OpaqueTransportMessageSchema, TransportAckSchema, msg, payloadToTransportMessage, ack, reply, } from './message';
5
- export type { TransportMessage, MessageId, OpaqueTransportMessage, TransportClientId, TransportMessageAck, } from './message';
4
+ export { TransportMessageSchema, OpaqueTransportMessageSchema, msg, reply, } from './message';
5
+ export type { TransportMessage, MessageId, OpaqueTransportMessage, TransportClientId, } from './message';
6
+ /**
7
+ * Waits for a message from the transport.
8
+ * @param {Transport} t - The transport to listen to.
9
+ * @param filter - An optional filter function to apply to the received messages.
10
+ * @returns A promise that resolves with the payload of the first message that passes the filter.
11
+ */
6
12
  export declare function waitForMessage(t: Transport, filter?: (msg: OpaqueTransportMessage) => boolean): Promise<unknown>;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../transport/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGpC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,GAAG,EACH,KAAK,GACN,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AAEnB;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,CAAC,EAAE,SAAS,EACZ,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,OAAO,oBAYlD"}
@@ -1,6 +1,12 @@
1
1
  // re-export
2
2
  export { Transport } from './types';
3
- export { TransportMessageSchema, OpaqueTransportMessageSchema, TransportAckSchema, msg, payloadToTransportMessage, ack, reply, } from './message';
3
+ export { TransportMessageSchema, OpaqueTransportMessageSchema, msg, reply, } from './message';
4
+ /**
5
+ * Waits for a message from the transport.
6
+ * @param {Transport} t - The transport to listen to.
7
+ * @param filter - An optional filter function to apply to the received messages.
8
+ * @returns A promise that resolves with the payload of the first message that passes the filter.
9
+ */
4
10
  export async function waitForMessage(t, filter) {
5
11
  return new Promise((resolve, _reject) => {
6
12
  function onMessage(msg) {
@@ -1,63 +1,121 @@
1
- import { type Static, TSchema } from '@sinclair/typebox';
1
+ import { TSchema } from '@sinclair/typebox';
2
+ /**
3
+ * Control flags for transport messages.
4
+ * An RPC message is coded with StreamOpenBit | StreamClosedBit.
5
+ * Streams are expected to start with StreamOpenBit sent and the client SHOULD send an empty
6
+ * message with StreamClosedBit to close the stream handler on the server, indicating that
7
+ * it will not be using the stream anymore.
8
+ */
9
+ export declare const enum ControlFlags {
10
+ AckBit = 1,
11
+ StreamOpenBit = 2,
12
+ StreamClosedBit = 4
13
+ }
14
+ /**
15
+ * Generic Typebox schema for a transport message.
16
+ * @template T The type of the payload.
17
+ * @param {T} t The payload schema.
18
+ * @returns The transport message schema.
19
+ */
2
20
  export declare const TransportMessageSchema: <T extends TSchema>(t: T) => import("@sinclair/typebox").TObject<{
3
21
  id: import("@sinclair/typebox").TString;
4
- replyTo: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
5
22
  from: import("@sinclair/typebox").TString;
6
23
  to: import("@sinclair/typebox").TString;
7
24
  serviceName: import("@sinclair/typebox").TString;
8
25
  procedureName: import("@sinclair/typebox").TString;
26
+ streamId: import("@sinclair/typebox").TString;
27
+ controlFlags: import("@sinclair/typebox").TInteger;
9
28
  payload: T;
10
29
  }>;
30
+ /**
31
+ * Defines the schema for a transport acknowledgement message. This is never constructed manually
32
+ * and is only used internally by the library for tracking inflight messages.
33
+ * @returns The transport message schema.
34
+ */
35
+ export declare const TransportAckSchema: import("@sinclair/typebox").TObject<{
36
+ id: import("@sinclair/typebox").TString;
37
+ from: import("@sinclair/typebox").TString;
38
+ to: import("@sinclair/typebox").TString;
39
+ serviceName: import("@sinclair/typebox").TString;
40
+ procedureName: import("@sinclair/typebox").TString;
41
+ streamId: import("@sinclair/typebox").TString;
42
+ controlFlags: import("@sinclair/typebox").TInteger;
43
+ payload: import("@sinclair/typebox").TObject<{
44
+ ack: import("@sinclair/typebox").TString;
45
+ }>;
46
+ }>;
47
+ /**
48
+ * Defines the schema for an opaque transport message that is agnostic to any
49
+ * procedure/service.
50
+ * @returns The transport message schema.
51
+ */
11
52
  export declare const OpaqueTransportMessageSchema: import("@sinclair/typebox").TObject<{
12
53
  id: import("@sinclair/typebox").TString;
13
- replyTo: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
14
54
  from: import("@sinclair/typebox").TString;
15
55
  to: import("@sinclair/typebox").TString;
16
56
  serviceName: import("@sinclair/typebox").TString;
17
57
  procedureName: import("@sinclair/typebox").TString;
58
+ streamId: import("@sinclair/typebox").TString;
59
+ controlFlags: import("@sinclair/typebox").TInteger;
18
60
  payload: import("@sinclair/typebox").TUnknown;
19
61
  }>;
62
+ /**
63
+ * Represents a transport message. This is the same type as {@link TransportMessageSchema} but
64
+ * we can't statically infer generics from generic Typebox schemas so we have to define it again here.
65
+ * @template Payload The type of the payload.
66
+ */
20
67
  export type TransportMessage<Payload extends Record<string, unknown> | unknown = Record<string, unknown>> = {
21
68
  id: string;
22
- replyTo?: string;
23
69
  from: string;
24
70
  to: string;
25
71
  serviceName: string;
26
72
  procedureName: string;
73
+ streamId: string;
74
+ controlFlags: number;
27
75
  payload: Payload;
28
76
  };
29
77
  export type MessageId = string;
78
+ /**
79
+ * A type alias for a transport message with an opaque payload.
80
+ * @template T - The type of the opaque payload.
81
+ */
30
82
  export type OpaqueTransportMessage = TransportMessage<unknown>;
31
83
  export type TransportClientId = 'SERVER' | string;
32
- export declare const TransportAckSchema: import("@sinclair/typebox").TObject<{
33
- id: import("@sinclair/typebox").TString;
34
- from: import("@sinclair/typebox").TString;
35
- ack: import("@sinclair/typebox").TString;
36
- }>;
37
- export type TransportMessageAck = Static<typeof TransportAckSchema>;
38
- export declare function msg<Payload extends object>(from: string, to: string, service: string, proc: string, payload: Payload): {
39
- id: string;
40
- to: string;
41
- from: string;
42
- serviceName: string;
43
- procedureName: string;
44
- payload: Payload;
45
- };
46
- export declare function payloadToTransportMessage<Payload extends object>(payload: Payload): {
47
- id: string;
48
- to: string;
49
- from: string;
50
- serviceName: string;
51
- procedureName: string;
52
- payload: Payload;
53
- };
54
- export declare function ack(msg: OpaqueTransportMessage): TransportMessageAck;
55
- export declare function reply<Payload extends object>(msg: OpaqueTransportMessage, response: Payload): {
56
- id: string;
57
- replyTo: string;
58
- to: string;
59
- from: string;
60
- payload: Payload;
61
- serviceName: string;
62
- procedureName: string;
63
- };
84
+ /**
85
+ * Creates a transport message with the given parameters. You shouldn't need to call this manually unless
86
+ * you're writing a test.
87
+ * @param from The sender of the message.
88
+ * @param to The intended recipient of the message.
89
+ * @param service The name of the service the message is intended for.
90
+ * @param proc The name of the procedure the message is intended for.
91
+ * @param stream The ID of the stream the message is intended for.
92
+ * @param payload The payload of the message.
93
+ * @returns A TransportMessage object with the given parameters.
94
+ */
95
+ export declare function msg<Payload extends object>(from: string, to: string, service: string, proc: string, stream: string, payload: Payload): TransportMessage<Payload>;
96
+ /**
97
+ * Creates a new transport message as a response to the given message.
98
+ * @param msg The original message to respond to.
99
+ * @param response The payload of the response message.
100
+ * @returns A new transport message with appropriate to, from, and payload fields
101
+ */
102
+ export declare function reply<Payload extends object>(msg: OpaqueTransportMessage, response: Payload): TransportMessage<Payload>;
103
+ /**
104
+ * Checks if the given control flag (usually found in msg.controlFlag) is an ack message.
105
+ * @param controlFlag - The control flag to check.
106
+ * @returns True if the control flag contains the AckBit, false otherwise.
107
+ */
108
+ export declare function isAck(controlFlag: number): boolean;
109
+ /**
110
+ * Checks if the given control flag (usually found in msg.controlFlag) is a stream open message.
111
+ * @param controlFlag - The control flag to check.
112
+ * @returns True if the control flag contains the StreamOpenBit, false otherwise.
113
+ */
114
+ export declare function isStreamOpen(controlFlag: number): boolean;
115
+ /**
116
+ * Checks if the given control flag (usually found in msg.controlFlag) is a stream close message.
117
+ * @param controlFlag - The control flag to check.
118
+ * @returns True if the control flag contains the StreamCloseBit, false otherwise.
119
+ */
120
+ export declare function isStreamClose(controlFlag: number): boolean;
121
+ //# sourceMappingURL=message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../transport/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAGlD;;;;;;GAMG;AACH,0BAAkB,YAAY;IAC5B,MAAM,IAAS;IACf,aAAa,IAAS;IACtB,eAAe,IAAS;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;EAU/B,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;EAI9B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;EAExC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,CAC1B,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACzE;IACF,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAE/B;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAC/D,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,MAAM,CAAC;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,GAAG,CAAC,OAAO,SAAS,MAAM,EACxC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,GACf,gBAAgB,CAAC,OAAO,CAAC,CAW3B;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,OAAO,SAAS,MAAM,EAC1C,GAAG,EAAE,sBAAsB,EAC3B,QAAQ,EAAE,OAAO,GAChB,gBAAgB,CAAC,OAAO,CAAC,CAQ3B;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAIzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAK1D"}
@@ -1,48 +1,95 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { nanoid } from 'nanoid';
3
- // look at https://github.com/websockets/ws#use-the-nodejs-streams-api for a duplex stream we can use
3
+ /**
4
+ * Generic Typebox schema for a transport message.
5
+ * @template T The type of the payload.
6
+ * @param {T} t The payload schema.
7
+ * @returns The transport message schema.
8
+ */
4
9
  export const TransportMessageSchema = (t) => Type.Object({
5
10
  id: Type.String(),
6
- replyTo: Type.Optional(Type.String()),
7
11
  from: Type.String(),
8
12
  to: Type.String(),
9
13
  serviceName: Type.String(),
10
14
  procedureName: Type.String(),
15
+ streamId: Type.String(),
16
+ controlFlags: Type.Integer(),
11
17
  payload: t,
12
18
  });
13
- export const OpaqueTransportMessageSchema = TransportMessageSchema(Type.Unknown());
14
- export const TransportAckSchema = Type.Object({
15
- id: Type.String(),
16
- from: Type.String(),
19
+ /**
20
+ * Defines the schema for a transport acknowledgement message. This is never constructed manually
21
+ * and is only used internally by the library for tracking inflight messages.
22
+ * @returns The transport message schema.
23
+ */
24
+ export const TransportAckSchema = TransportMessageSchema(Type.Object({
17
25
  ack: Type.String(),
18
- });
19
- export function msg(from, to, service, proc, payload) {
26
+ }));
27
+ /**
28
+ * Defines the schema for an opaque transport message that is agnostic to any
29
+ * procedure/service.
30
+ * @returns The transport message schema.
31
+ */
32
+ export const OpaqueTransportMessageSchema = TransportMessageSchema(Type.Unknown());
33
+ /**
34
+ * Creates a transport message with the given parameters. You shouldn't need to call this manually unless
35
+ * you're writing a test.
36
+ * @param from The sender of the message.
37
+ * @param to The intended recipient of the message.
38
+ * @param service The name of the service the message is intended for.
39
+ * @param proc The name of the procedure the message is intended for.
40
+ * @param stream The ID of the stream the message is intended for.
41
+ * @param payload The payload of the message.
42
+ * @returns A TransportMessage object with the given parameters.
43
+ */
44
+ export function msg(from, to, service, proc, stream, payload) {
20
45
  return {
21
46
  id: nanoid(),
22
47
  to,
23
48
  from,
24
49
  serviceName: service,
25
50
  procedureName: proc,
51
+ streamId: stream,
52
+ controlFlags: 0,
26
53
  payload,
27
54
  };
28
55
  }
29
- export function payloadToTransportMessage(payload) {
30
- return msg('client', 'SERVER', 'service', 'procedure', payload);
31
- }
32
- export function ack(msg) {
33
- return {
34
- id: nanoid(),
35
- from: msg.to,
36
- ack: msg.id,
37
- };
38
- }
56
+ /**
57
+ * Creates a new transport message as a response to the given message.
58
+ * @param msg The original message to respond to.
59
+ * @param response The payload of the response message.
60
+ * @returns A new transport message with appropriate to, from, and payload fields
61
+ */
39
62
  export function reply(msg, response) {
40
63
  return {
41
64
  ...msg,
42
65
  id: nanoid(),
43
- replyTo: msg.id,
44
66
  to: msg.from,
45
67
  from: msg.to,
46
68
  payload: response,
47
69
  };
48
70
  }
71
+ /**
72
+ * Checks if the given control flag (usually found in msg.controlFlag) is an ack message.
73
+ * @param controlFlag - The control flag to check.
74
+ * @returns True if the control flag contains the AckBit, false otherwise.
75
+ */
76
+ export function isAck(controlFlag) {
77
+ return (controlFlag & 1 /* ControlFlags.AckBit */) === 1 /* ControlFlags.AckBit */;
78
+ }
79
+ /**
80
+ * Checks if the given control flag (usually found in msg.controlFlag) is a stream open message.
81
+ * @param controlFlag - The control flag to check.
82
+ * @returns True if the control flag contains the StreamOpenBit, false otherwise.
83
+ */
84
+ export function isStreamOpen(controlFlag) {
85
+ return ((controlFlag & 2 /* ControlFlags.StreamOpenBit */) === 2 /* ControlFlags.StreamOpenBit */);
86
+ }
87
+ /**
88
+ * Checks if the given control flag (usually found in msg.controlFlag) is a stream close message.
89
+ * @param controlFlag - The control flag to check.
90
+ * @returns True if the control flag contains the StreamCloseBit, false otherwise.
91
+ */
92
+ export function isStreamClose(controlFlag) {
93
+ return ((controlFlag & 4 /* ControlFlags.StreamClosedBit */) ===
94
+ 4 /* ControlFlags.StreamClosedBit */);
95
+ }
@@ -1 +1,2 @@
1
1
  export {};
2
+ //# sourceMappingURL=message.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.test.d.ts","sourceRoot":"","sources":["../../transport/message.test.ts"],"names":[],"mappings":""}
@@ -1,14 +1,32 @@
1
- import { ack, msg, reply } from './message';
1
+ import { isAck, isStreamClose, isStreamOpen, msg, reply, } from './message';
2
2
  import { describe, test, expect } from 'vitest';
3
3
  describe('message helpers', () => {
4
4
  test('ack', () => {
5
- const m = msg('a', 'b', 'svc', 'proc', { test: 1 });
6
- const resp = ack(m);
7
- expect(resp.from).toBe('b');
8
- expect(resp).toHaveProperty('ack');
5
+ const m = msg('a', 'b', 'svc', 'proc', 'stream', { test: 1 });
6
+ m.controlFlags |= 1 /* ControlFlags.AckBit */;
7
+ expect(m).toHaveProperty('controlFlags');
8
+ expect(isAck(m.controlFlags)).toBe(true);
9
+ expect(isStreamOpen(m.controlFlags)).toBe(false);
10
+ expect(isStreamClose(m.controlFlags)).toBe(false);
11
+ });
12
+ test('streamOpen', () => {
13
+ const m = msg('a', 'b', 'svc', 'proc', 'stream', { test: 1 });
14
+ m.controlFlags |= 2 /* ControlFlags.StreamOpenBit */;
15
+ expect(m).toHaveProperty('controlFlags');
16
+ expect(isAck(m.controlFlags)).toBe(false);
17
+ expect(isStreamOpen(m.controlFlags)).toBe(true);
18
+ expect(isStreamClose(m.controlFlags)).toBe(false);
19
+ });
20
+ test('streamClose', () => {
21
+ const m = msg('a', 'b', 'svc', 'proc', 'stream', { test: 1 });
22
+ m.controlFlags |= 4 /* ControlFlags.StreamClosedBit */;
23
+ expect(m).toHaveProperty('controlFlags');
24
+ expect(isAck(m.controlFlags)).toBe(false);
25
+ expect(isStreamOpen(m.controlFlags)).toBe(false);
26
+ expect(isStreamClose(m.controlFlags)).toBe(true);
9
27
  });
10
28
  test('reply', () => {
11
- const m = msg('a', 'b', 'svc', 'proc', { test: 1 });
29
+ const m = msg('a', 'b', 'svc', 'proc', 'stream', { test: 1 });
12
30
  const payload = { cool: 2 };
13
31
  const resp = reply(m, payload);
14
32
  expect(resp.id).not.toBe(m.id);
@@ -16,4 +34,19 @@ describe('message helpers', () => {
16
34
  expect(resp.from).toBe('b');
17
35
  expect(resp.to).toBe('a');
18
36
  });
37
+ test('default message has no control flags set', () => {
38
+ const m = msg('a', 'b', 'svc', 'proc', 'stream', { test: 1 });
39
+ expect(isAck(m.controlFlags)).toBe(false);
40
+ expect(isStreamOpen(m.controlFlags)).toBe(false);
41
+ expect(isStreamClose(m.controlFlags)).toBe(false);
42
+ });
43
+ test('combining control flags works', () => {
44
+ const m = msg('a', 'b', 'svc', 'proc', 'stream', { test: 1 });
45
+ m.controlFlags |= 2 /* ControlFlags.StreamOpenBit */;
46
+ expect(isStreamOpen(m.controlFlags)).toBe(true);
47
+ expect(isStreamClose(m.controlFlags)).toBe(false);
48
+ m.controlFlags |= 4 /* ControlFlags.StreamClosedBit */;
49
+ expect(isStreamOpen(m.controlFlags)).toBe(true);
50
+ expect(isStreamClose(m.controlFlags)).toBe(true);
51
+ });
19
52
  });
@@ -1,14 +1,50 @@
1
1
  import { Codec } from '../codec/types';
2
- import { MessageId, OpaqueTransportMessage, TransportClientId, TransportMessageAck } from './message';
2
+ import { MessageId, OpaqueTransportMessage, TransportClientId } from './message';
3
+ /**
4
+ * Abstract base for a transport layer for communication between nodes in a River network.
5
+ * Any River transport methods need to implement this interface.
6
+ * @abstract
7
+ */
3
8
  export declare abstract class Transport {
9
+ /**
10
+ * The {@link Codec} used to encode and decode messages.
11
+ */
4
12
  codec: Codec;
13
+ /**
14
+ * The client ID of this transport.
15
+ */
5
16
  clientId: TransportClientId;
17
+ /**
18
+ * The set of message handlers registered with this transport.
19
+ */
6
20
  handlers: Set<(msg: OpaqueTransportMessage) => void>;
21
+ /**
22
+ * The buffer of messages that have been sent but not yet acknowledged.
23
+ */
7
24
  sendBuffer: Map<MessageId, OpaqueTransportMessage>;
25
+ /**
26
+ * Creates a new Transport instance.
27
+ * @param codec The codec used to encode and decode messages.
28
+ * @param clientId The client ID of this transport.
29
+ */
8
30
  constructor(codec: Codec, clientId: TransportClientId);
31
+ /**
32
+ * Called when a message is received by this transport.
33
+ * You generally shouldn't need to override this in downstream transport implementations.
34
+ * @param msg The received message.
35
+ */
9
36
  onMessage(msg: string): void;
37
+ /**
38
+ * Adds a message listener to this transport.
39
+ * @param handler The message handler to add.
40
+ */
10
41
  addMessageListener(handler: (msg: OpaqueTransportMessage) => void): void;
42
+ /**
43
+ * Removes a message listener from this transport.
44
+ * @param handler The message handler to remove.
45
+ */
11
46
  removeMessageListener(handler: (msg: OpaqueTransportMessage) => void): void;
12
- abstract send(msg: OpaqueTransportMessage | TransportMessageAck): MessageId;
47
+ abstract send(msg: OpaqueTransportMessage): MessageId;
13
48
  abstract close(): Promise<void>;
14
49
  }
50
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../transport/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,OAAO,EAEL,SAAS,EACT,sBAAsB,EAGtB,iBAAiB,EAGlB,MAAM,WAAW,CAAC;AAGnB;;;;GAIG;AACH,8BAAsB,SAAS;IAC7B;;OAEG;IACH,KAAK,EAAE,KAAK,CAAC;IAEb;;OAEG;IACH,QAAQ,EAAE,iBAAiB,CAAC;IAE5B;;OAEG;IACH,QAAQ,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,sBAAsB,KAAK,IAAI,CAAC,CAAC;IAGrD;;OAEG;IACH,UAAU,EAAE,GAAG,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAC;IAEnD;;;;OAIG;gBACS,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB;IAOrD;;;;OAIG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM;IAuCrB;;;OAGG;IACH,kBAAkB,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,IAAI,GAAG,IAAI;IAIxE;;;OAGG;IACH,qBAAqB,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,IAAI,GAAG,IAAI;IAI3E,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAsB,GAAG,SAAS;IACrD,QAAQ,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAChC"}
@@ -1,31 +1,61 @@
1
1
  import { Value } from '@sinclair/typebox/value';
2
- import { OpaqueTransportMessageSchema, TransportAckSchema, ack, } from './message';
2
+ import { OpaqueTransportMessageSchema, TransportAckSchema, isAck, reply, } from './message';
3
3
  import { log } from '../logging';
4
+ /**
5
+ * Abstract base for a transport layer for communication between nodes in a River network.
6
+ * Any River transport methods need to implement this interface.
7
+ * @abstract
8
+ */
4
9
  export class Transport {
10
+ /**
11
+ * The {@link Codec} used to encode and decode messages.
12
+ */
5
13
  codec;
14
+ /**
15
+ * The client ID of this transport.
16
+ */
6
17
  clientId;
18
+ /**
19
+ * The set of message handlers registered with this transport.
20
+ */
7
21
  handlers;
22
+ // TODO; we can do much better here on retry (maybe resending the sendBuffer on fixed interval)
23
+ /**
24
+ * The buffer of messages that have been sent but not yet acknowledged.
25
+ */
8
26
  sendBuffer;
27
+ /**
28
+ * Creates a new Transport instance.
29
+ * @param codec The codec used to encode and decode messages.
30
+ * @param clientId The client ID of this transport.
31
+ */
9
32
  constructor(codec, clientId) {
10
33
  this.handlers = new Set();
11
34
  this.sendBuffer = new Map();
12
35
  this.codec = codec;
13
36
  this.clientId = clientId;
14
37
  }
38
+ /**
39
+ * Called when a message is received by this transport.
40
+ * You generally shouldn't need to override this in downstream transport implementations.
41
+ * @param msg The received message.
42
+ */
15
43
  onMessage(msg) {
16
44
  const parsedMsg = this.codec.fromStringBuf(msg);
17
45
  if (parsedMsg === null) {
18
46
  log?.warn(`${this.clientId} -- received malformed msg: ${msg}`);
19
47
  return;
20
48
  }
21
- if (Value.Check(TransportAckSchema, parsedMsg)) {
49
+ if (Value.Check(TransportAckSchema, parsedMsg) &&
50
+ isAck(parsedMsg.controlFlags)) {
22
51
  // process ack
23
52
  log?.info(`${this.clientId} -- received ack: ${msg}`);
24
- if (this.sendBuffer.has(parsedMsg.ack)) {
25
- this.sendBuffer.delete(parsedMsg.ack);
53
+ if (this.sendBuffer.has(parsedMsg.payload.ack)) {
54
+ this.sendBuffer.delete(parsedMsg.payload.ack);
26
55
  }
27
56
  }
28
57
  else if (Value.Check(OpaqueTransportMessageSchema, parsedMsg)) {
58
+ // regular river message
29
59
  log?.info(`${this.clientId} -- received msg: ${msg}`);
30
60
  // ignore if not for us
31
61
  if (parsedMsg.to !== this.clientId && parsedMsg.to !== 'broadcast') {
@@ -35,7 +65,8 @@ export class Transport {
35
65
  for (const handler of this.handlers) {
36
66
  handler(parsedMsg);
37
67
  }
38
- const ackMsg = ack(parsedMsg);
68
+ const ackMsg = reply(parsedMsg, { ack: parsedMsg.id });
69
+ ackMsg.controlFlags = 1 /* ControlFlags.AckBit */;
39
70
  ackMsg.from = this.clientId;
40
71
  this.send(ackMsg);
41
72
  }
@@ -43,9 +74,17 @@ export class Transport {
43
74
  log?.warn(`${this.clientId} -- received invalid transport msg: ${msg}`);
44
75
  }
45
76
  }
77
+ /**
78
+ * Adds a message listener to this transport.
79
+ * @param handler The message handler to add.
80
+ */
46
81
  addMessageListener(handler) {
47
82
  this.handlers.add(handler);
48
83
  }
84
+ /**
85
+ * Removes a message listener from this transport.
86
+ * @param handler The message handler to remove.
87
+ */
49
88
  removeMessageListener(handler) {
50
89
  this.handlers.delete(handler);
51
90
  }
package/package.json CHANGED
@@ -2,14 +2,13 @@
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.3.1",
5
+ "version": "0.5.0",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./dist/router/index.js",
9
9
  "./logging": "./dist/logging/index.js",
10
10
  "./codec": "./dist/codec/index.js",
11
11
  "./test-util": "./dist/testUtils.js",
12
- "./package.json": "./package.json",
13
12
  "./transport": "./dist/transport/index.js",
14
13
  "./transport/ws": "./dist/transport/impls/ws.js",
15
14
  "./transport/stdio": "./dist/transport/impls/stdio.js"