@synnaxlabs/freighter 0.55.0 → 0.56.1

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/src/stream.ts CHANGED
@@ -16,15 +16,14 @@ import { type Transport } from "@/transport";
16
16
  */
17
17
  export interface StreamReceiver<RS extends z.ZodType> {
18
18
  /**
19
- * Receives a response from the stream. It's not safe to call receive
20
- * concurrently.
19
+ * Receives a response from the stream. It's not safe to call receive concurrently.
21
20
  *
22
- * @returns freighter.EOF: if the server closed the stream nominally.
23
- * @returns Error: if the server closed the stream abnormally,
24
- * returns the error the server returned.
25
- * @raises Error: if the transport fails.
21
+ * @returns the next response from the stream.
22
+ * @throws freighter.EOF: if the server closed the stream nominally.
23
+ * @throws Error: if the server closed the stream abnormally, throws the error the
24
+ * server returned, or a transport error if the transport itself failed.
26
25
  */
27
- receive: () => Promise<[z.infer<RS>, null] | [null, Error]>;
26
+ receive: () => Promise<z.infer<RS>>;
28
27
 
29
28
  /**
30
29
  * @returns true if the stream has received a response
@@ -37,17 +36,16 @@ export interface StreamReceiver<RS extends z.ZodType> {
37
36
  */
38
37
  export interface StreamSender<RQ extends z.ZodType> {
39
38
  /**
40
- * Sends a request to the stream. It is not safe to call send concurrently
41
- * with closeSend or send.
39
+ * Sends a request to the stream. It is not safe to call send concurrently with
40
+ * closeSend or send.
42
41
 
43
42
  * @param req - the request to send.
44
- * @returns freighter.EOF: if the server closed the stream. The caller
45
- * can discover the error returned by the server by calling receive().
46
- * @returns undefined: if the message was sent successfully.
47
- * @raises freighter.StreamClosed: if the client called close_send()
48
- * @raises Error: if the transport fails.
43
+ * @throws freighter.EOF: if the server closed the stream. The caller can discover the
44
+ * error returned by the server by calling receive().
45
+ * @throws freighter.StreamClosed: if the client called closeSend().
46
+ * @throws Error: if the transport fails.
49
47
  */
50
- send: (req: z.input<RQ> | z.infer<RQ>) => Error | null;
48
+ send: (req: z.input<RQ> | z.infer<RQ>) => void;
51
49
  }
52
50
 
53
51
  /**
@@ -56,13 +54,13 @@ export interface StreamSender<RQ extends z.ZodType> {
56
54
  */
57
55
  export interface StreamSenderCloser<RQ extends z.ZodType> extends StreamSender<RQ> {
58
56
  /**
59
- * Lets the server know no more messages will be sent. If the client attempts
60
- * to call send() after calling closeSend(), a freighter.StreamClosed
61
- * exception will be raised. close_send is idempotent. If the server has
62
- * already closed the stream, close_send will do nothing.
57
+ * Lets the server know no more messages will be sent. If the client attempts to call
58
+ * send() after calling closeSend(), a freighter.StreamClosed exception will be raised.
59
+ * close_send is idempotent. If the server has already closed the stream, close_send
60
+ * will do nothing.
63
61
 
64
- * After calling close_send, the client is responsible for calling receive()
65
- * to successfully receive the server's acknowledgement.
62
+ * After calling close_send, the client is responsible for calling receive() to
63
+ * successfully receive the server's acknowledgement.
66
64
  */
67
65
  closeSend: () => void;
68
66
  }
@@ -78,15 +76,15 @@ export interface Stream<RQ extends z.ZodType, RS extends z.ZodType = RQ>
78
76
  */
79
77
  export interface StreamClient extends Transport {
80
78
  /**
81
- * Dials the target and returns a stream that can be used to issue requests
82
- * and receive responses
79
+ * Dials the target and returns a stream that can be used to issue requests and
80
+ * receive responses
83
81
  *
84
- * @param target - The target to dial. In some implementations, this may be
85
- * an endpoint path, or in others, a complete hostname or URL.
86
- * @param reqSchema - The schema for the request type. This is used to
87
- * validate the request before sending it.
88
- * @param resSchema - The schema for the response type. This is used to
89
- * validate the response before returning it.
82
+ * @param target - The target to dial. In some implementations, this may be an
83
+ * endpoint path, or in others, a complete hostname or URL.
84
+ * @param reqSchema - The schema for the request type. This is used to validate the
85
+ * request before sending it.
86
+ * @param resSchema - The schema for the response type. This is used to validate the
87
+ * response before returning it.
90
88
  */
91
89
  stream: <RQ extends z.ZodType, RS extends z.ZodType = RQ>(
92
90
  target: string,
package/src/unary.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { breaker } from "@synnaxlabs/x";
10
+ import { breaker, errors } from "@synnaxlabs/x";
11
11
  import { type z } from "zod";
12
12
 
13
13
  import { Unreachable } from "@/errors";
@@ -15,8 +15,8 @@ import { type Middleware } from "@/middleware";
15
15
  import { type Transport } from "@/transport";
16
16
 
17
17
  /**
18
- * An interface for an entity that implements a simple request-response
19
- * transport between two entities.
18
+ * An interface for an entity that implements a simple request-response transport
19
+ * between two entities.
20
20
  */
21
21
  export interface UnaryClient extends Transport {
22
22
  /**
@@ -24,13 +24,15 @@ export interface UnaryClient extends Transport {
24
24
  * @param target - The target server to send the request to.
25
25
  * @param req - The request to send.
26
26
  * @param resSchema - The schema to validate the response against.
27
+ * @returns the decoded response.
28
+ * @throws Error: if the server returns an error or the transport fails.
27
29
  */
28
30
  send: <RQ extends z.ZodType, RS extends z.ZodType = RQ>(
29
31
  target: string,
30
32
  req: z.input<RQ> | z.infer<RQ>,
31
33
  reqSchema: RQ,
32
34
  resSchema: RS,
33
- ) => Promise<[z.infer<RS>, null] | [null, Error]>;
35
+ ) => Promise<z.infer<RS>>;
34
36
  }
35
37
 
36
38
  export const unaryWithBreaker = (
@@ -53,28 +55,19 @@ export const unaryWithBreaker = (
53
55
  req: z.input<RQ> | z.infer<RQ>,
54
56
  reqSchema: RQ,
55
57
  resSchema: RS,
56
- ): Promise<[z.infer<RS>, null] | [null, Error]> {
58
+ ): Promise<z.infer<RS>> {
57
59
  const brk = new breaker.Breaker(cfg);
58
- do {
59
- const [res, err] = await this.wrapped.send(target, req, reqSchema, resSchema);
60
- if (err == null) return [res, null];
61
- if (!Unreachable.matches(err)) return [null, err];
62
- console.warn(`[freighter] ${brk.retryMessage}`, err);
63
- if (!(await brk.wait())) return [res, err];
64
- } while (true);
60
+ do
61
+ try {
62
+ return await this.wrapped.send(target, req, reqSchema, resSchema);
63
+ } catch (err) {
64
+ const e = errors.fromUnknown(err);
65
+ if (!Unreachable.matches(e)) throw e;
66
+ console.warn(`[freighter] ${brk.retryMessage}`, e);
67
+ if (!(await brk.wait())) throw e;
68
+ }
69
+ while (true);
65
70
  }
66
71
  }
67
72
  return new WithBreaker(base);
68
73
  };
69
-
70
- export const sendRequired = async <RQ extends z.ZodType, RS extends z.ZodType = RQ>(
71
- client: UnaryClient,
72
- target: string,
73
- req: z.input<RQ> | z.infer<RQ>,
74
- reqSchema: RQ,
75
- resSchema: RS,
76
- ): Promise<z.infer<RS>> => {
77
- const [res, err] = await client.send(target, req, reqSchema, resSchema);
78
- if (err != null) throw err;
79
- return res;
80
- };
@@ -15,12 +15,9 @@ import { EOF } from "@/errors";
15
15
  import { type Context } from "@/middleware";
16
16
  import { WebSocketClient } from "@/websocket";
17
17
 
18
- const url = new URL({
19
- host: "127.0.0.1",
20
- port: 8080,
21
- });
18
+ const url = new URL({ host: "127.0.0.1", port: 8080 });
22
19
 
23
- const MessageSchema = z.object({
20
+ const messageSchema = z.object({
24
21
  id: z.number().optional(),
25
22
  message: z.string().optional(),
26
23
  });
@@ -47,73 +44,63 @@ const decodeTestError = (encoded: errors.Payload): errors.Typed | null => {
47
44
  return new MyCustomError(message, parseInt(code, 10));
48
45
  };
49
46
 
50
- errors.register({
51
- encode: encodeTestError,
52
- decode: decodeTestError,
53
- });
47
+ errors.register({ encode: encodeTestError, decode: decodeTestError });
54
48
 
55
49
  describe("websocket", () => {
56
50
  test("basic exchange", async () => {
57
- const stream = await client.stream("stream/echo", MessageSchema, MessageSchema);
51
+ const stream = await client.stream("stream/echo", messageSchema, messageSchema);
58
52
  for (let i = 0; i < 10; i++) {
59
53
  stream.send({ id: i, message: "hello" });
60
- const [response, error] = await stream.receive();
61
- expect(error).toBeNull();
62
- expect(response?.id).toEqual(i + 1);
63
- expect(response?.message).toEqual("hello");
54
+ const response = await stream.receive();
55
+ expect(response.id).toEqual(i + 1);
56
+ expect(response.message).toEqual("hello");
64
57
  }
65
58
  stream.closeSend();
66
- const [response, error] = await stream.receive();
67
- expect(EOF.matches(error)).toBeTruthy();
68
- expect(response).toBeNull();
59
+ await expect(stream.receive()).rejects.toThrow(EOF);
69
60
  });
70
61
 
71
62
  test("receive message after close", async () => {
72
63
  const stream = await client.stream(
73
64
  "stream/sendMessageAfterClientClose",
74
- MessageSchema,
75
- MessageSchema,
65
+ messageSchema,
66
+ messageSchema,
76
67
  );
77
68
  stream.closeSend();
78
- const [response, error] = await stream.receive();
79
- expect(error).toBeNull();
80
- expect(response?.id).toEqual(0);
81
- expect(response?.message).toEqual("Close Acknowledged");
82
- const [, recvError] = await stream.receive();
83
- expect(EOF.matches(recvError)).toBeTruthy();
69
+ const response = await stream.receive();
70
+ expect(response.id).toEqual(0);
71
+ expect(response.message).toEqual("Close Acknowledged");
72
+ await expect(stream.receive()).rejects.toThrow(EOF);
84
73
  });
85
74
 
86
75
  test("receive error", async () => {
87
76
  const stream = await client.stream(
88
77
  "stream/receiveAndExitWithErr",
89
- MessageSchema,
90
- MessageSchema,
78
+ messageSchema,
79
+ messageSchema,
91
80
  );
92
81
  stream.send({ id: 0, message: "hello" });
93
- const [response, error] = await stream.receive();
94
- expect(MyCustomError.matches(error)).toBeTruthy();
95
- expect(response).toBeNull();
82
+ await expect(stream.receive()).rejects.toThrow(MyCustomError);
96
83
  });
97
84
 
98
85
  describe("middleware", () => {
99
86
  test("receive middleware", async () => {
100
87
  const myClient = new WebSocketClient(url, new binary.JSONCodec());
101
88
  let c = 0;
102
- myClient.use(async (md, next): Promise<[Context, Error | null]> => {
89
+ myClient.use(async (md, next): Promise<Context> => {
103
90
  if (md.params !== undefined) {
104
91
  c++;
105
92
  md.params.Test = "test";
106
93
  }
107
94
  return await next(md);
108
95
  });
109
- await myClient.stream("stream/middlewareCheck", MessageSchema, MessageSchema);
96
+ await myClient.stream("stream/middlewareCheck", messageSchema, messageSchema);
110
97
  expect(c).toEqual(1);
111
98
  });
112
99
 
113
100
  test("middleware error on server", async () => {
114
101
  const myClient = new WebSocketClient(url, new binary.JSONCodec());
115
102
  await expect(
116
- myClient.stream("stream/middlewareCheck", MessageSchema, MessageSchema),
103
+ myClient.stream("stream/middlewareCheck", messageSchema, messageSchema),
117
104
  ).rejects.toThrow("test param not found");
118
105
  });
119
106
  });
package/src/websocket.ts CHANGED
@@ -56,34 +56,32 @@ class WebSocketStream<
56
56
  this.listenForMessages();
57
57
  }
58
58
 
59
- async receiveOpenAck(): Promise<Error | null> {
59
+ async receiveOpenAck(): Promise<void> {
60
60
  const msg = await this.receiveMsg();
61
- if (msg.type !== "open") {
62
- if (msg.error == null) throw new Error("Message error must be defined");
63
- return errors.decode(msg.error);
64
- }
65
- return null;
61
+ if (msg.type === "open") return;
62
+ if (msg.error == null) throw new Error("Message error must be defined");
63
+ const err = errors.decode(msg.error);
64
+ throw err ?? new Error(`Unexpected open-ack message type: ${msg.type}`);
66
65
  }
67
66
 
68
67
  /** Implements the Stream protocol */
69
- send(req: z.input<RQ> | z.infer<RQ>): Error | null {
70
- if (this.serverClosed != null) return new EOF();
68
+ send(req: z.input<RQ> | z.infer<RQ>): void {
69
+ if (this.serverClosed != null) throw new EOF();
71
70
  if (this.sendClosed) throw new StreamClosed();
72
71
  this.ws.send(this.codec.encode({ type: "data", payload: req }));
73
- return null;
74
72
  }
75
73
 
76
74
  /** Implements the Stream protocol */
77
- async receive(): Promise<[z.infer<RS>, null] | [null, Error]> {
78
- if (this.serverClosed != null) return [null, this.serverClosed];
75
+ async receive(): Promise<z.infer<RS>> {
76
+ if (this.serverClosed != null) throw this.serverClosed;
79
77
  const msg = await this.receiveMsg();
80
78
  if (msg.type === "close") {
81
79
  if (msg.error == null) throw new Error("Message error must be defined");
82
80
  this.serverClosed = errors.decode(msg.error);
83
81
  if (this.serverClosed == null) throw new Error("Message error must be defined");
84
- return [null, this.serverClosed];
82
+ throw this.serverClosed;
85
83
  }
86
- return [this.resSchema.parse(msg.payload), null];
84
+ return this.resSchema.parse(msg.payload);
87
85
  }
88
86
 
89
87
  /** Implements the Stream protocol */
@@ -177,19 +175,16 @@ export class WebSocketClient extends MiddlewareCollector implements StreamClient
177
175
  resSchema: RS,
178
176
  ): Promise<Stream<RQ, RS>> {
179
177
  let stream: Stream<RQ, RS> | undefined;
180
- const [, error] = await this.executeMiddleware(
178
+ await this.executeMiddleware(
181
179
  { target, protocol: "websocket", params: {}, role: "client" },
182
- async (ctx: Context): Promise<[Context, Error | null]> => {
180
+ async (ctx: Context): Promise<Context> => {
183
181
  const ws = new WebSocket(this.buildURL(target, ctx));
184
182
  const outCtx: Context = { ...ctx, params: {} };
185
183
  ws.binaryType = WebSocketClient.MESSAGE_TYPE;
186
- const streamOrErr = await this.wrapSocket(ws, reqSchema, resSchema);
187
- if (streamOrErr instanceof Error) return [outCtx, streamOrErr];
188
- stream = streamOrErr;
189
- return [outCtx, null];
184
+ stream = await this.wrapSocket(ws, reqSchema, resSchema);
185
+ return outCtx;
190
186
  },
191
187
  );
192
- if (error != null) throw error;
193
188
  return stream as Stream<RQ, RS>;
194
189
  }
195
190
 
@@ -208,21 +203,18 @@ export class WebSocketClient extends MiddlewareCollector implements StreamClient
208
203
  ws: WebSocket,
209
204
  reqSchema: RQ,
210
205
  resSchema: RS,
211
- ): Promise<WebSocketStream<RQ, RS> | Error> {
212
- return await new Promise((resolve) => {
206
+ ): Promise<WebSocketStream<RQ, RS>> {
207
+ return await new Promise((resolve, reject) => {
213
208
  ws.onopen = () => {
214
209
  const oWs = new WebSocketStream<RQ, RS>(ws, this.encoder, reqSchema, resSchema);
215
210
  oWs
216
211
  .receiveOpenAck()
217
- .then((err) => {
218
- if (err != null) resolve(err);
219
- else resolve(oWs);
220
- })
221
- .catch((err: Error) => resolve(err));
212
+ .then(() => resolve(oWs))
213
+ .catch((err: unknown) => reject(errors.fromUnknown(err)));
222
214
  };
223
215
  ws.onerror = (ev: Event) => {
224
216
  const ev_ = ev as ErrorEvent;
225
- resolve(new Error(ev_.message));
217
+ reject(new Error(ev_.message ?? "websocket error", { cause: ev_.error ?? ev }));
226
218
  };
227
219
  });
228
220
  }