@synnaxlabs/freighter 0.2.0 → 0.4.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/.eslintrc.cjs +18 -0
- package/LICENSE +4 -21
- package/dist/alamos.d.ts +3 -0
- package/{build/module/lib → dist}/errors.d.ts +35 -40
- package/dist/freighter.cjs.js +11828 -0
- package/dist/freighter.cjs.js.map +1 -0
- package/dist/freighter.es.js +11828 -0
- package/dist/freighter.es.js.map +1 -0
- package/dist/http.d.ts +20 -0
- package/dist/index.d.ts +8 -0
- package/{build/module/lib → dist}/middleware.d.ts +11 -8
- package/{build/main/lib → dist}/stream.d.ts +11 -11
- package/{build/main/lib → dist}/transport.d.ts +2 -2
- package/dist/unary.d.ts +16 -0
- package/dist/websocket.d.ts +24 -0
- package/package.json +30 -98
- package/src/alamos.ts +40 -0
- package/src/errors.spec.ts +94 -0
- package/src/errors.ts +205 -0
- package/src/http.spec.ts +67 -0
- package/src/http.ts +115 -0
- package/src/index.ts +21 -20
- package/src/{lib/middleware.ts → middleware.ts} +29 -19
- package/src/{lib/stream.ts → stream.ts} +23 -14
- package/src/transport.ts +23 -0
- package/src/unary.ts +44 -0
- package/src/websocket.spec.ts +119 -0
- package/src/websocket.ts +203 -0
- package/tsconfig.json +5 -42
- package/tsconfig.vite.json +4 -0
- package/vite.config.ts +16 -0
- package/.editorconfig +0 -15
- package/.eslintrc.json +0 -33
- package/.gitignore +0 -9
- package/.nyc_output/3238f10e-9572-49ec-ab9d-28cbcaa6152a.json +0 -1
- package/.nyc_output/4e78a5c9-c0ca-4664-aa04-f478522608eb.json +0 -1
- package/.nyc_output/6a2244f2-5aea-45c7-8eeb-e14b454f0096.json +0 -1
- package/.nyc_output/dd1075a0-827b-4154-a75e-9bc90a4e16d0.json +0 -1
- package/.nyc_output/f829ad27-9bcd-4604-ae57-aae8c6f28d51.json +0 -1
- package/.nyc_output/fabc60f1-8fc5-4a1e-bea0-dc1fbcc31c9c.json +0 -1
- package/.nyc_output/processinfo/3238f10e-9572-49ec-ab9d-28cbcaa6152a.json +0 -1
- package/.nyc_output/processinfo/4e78a5c9-c0ca-4664-aa04-f478522608eb.json +0 -1
- package/.nyc_output/processinfo/6a2244f2-5aea-45c7-8eeb-e14b454f0096.json +0 -1
- package/.nyc_output/processinfo/dd1075a0-827b-4154-a75e-9bc90a4e16d0.json +0 -1
- package/.nyc_output/processinfo/f829ad27-9bcd-4604-ae57-aae8c6f28d51.json +0 -1
- package/.nyc_output/processinfo/fabc60f1-8fc5-4a1e-bea0-dc1fbcc31c9c.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/.prettierignore +0 -2
- package/.prettierrc +0 -3
- package/.vscode/settings.json +0 -4
- package/build/main/index.d.ts +0 -9
- package/build/main/index.js +0 -29
- package/build/main/lib/caseconv.d.ts +0 -2
- package/build/main/lib/caseconv.js +0 -14
- package/build/main/lib/encoder.d.ts +0 -59
- package/build/main/lib/encoder.js +0 -57
- package/build/main/lib/encoder.spec.d.ts +0 -1
- package/build/main/lib/encoder.spec.js +0 -26
- package/build/main/lib/errors.d.ts +0 -87
- package/build/main/lib/errors.js +0 -189
- package/build/main/lib/errors.spec.js +0 -88
- package/build/main/lib/http.d.ts +0 -50
- package/build/main/lib/http.js +0 -114
- package/build/main/lib/http.spec.js +0 -59
- package/build/main/lib/middleware.d.ts +0 -45
- package/build/main/lib/middleware.js +0 -38
- package/build/main/lib/runtime.d.ts +0 -5
- package/build/main/lib/runtime.js +0 -24
- package/build/main/lib/stream.js +0 -3
- package/build/main/lib/transport.js +0 -3
- package/build/main/lib/unary.d.ts +0 -15
- package/build/main/lib/unary.js +0 -3
- package/build/main/lib/url.d.ts +0 -38
- package/build/main/lib/url.js +0 -65
- package/build/main/lib/url.spec.d.ts +0 -1
- package/build/main/lib/url.spec.js +0 -41
- package/build/main/lib/util/log.d.ts +0 -2
- package/build/main/lib/util/log.js +0 -15
- package/build/main/lib/websocket.d.ts +0 -25
- package/build/main/lib/websocket.js +0 -172
- package/build/main/lib/websocket.spec.js +0 -86
- package/build/main/lib/ws.spec.d.ts +0 -1
- package/build/main/lib/ws.spec.js +0 -115
- package/build/module/index.d.ts +0 -9
- package/build/module/index.js +0 -7
- package/build/module/lib/caseconv.d.ts +0 -2
- package/build/module/lib/caseconv.js +0 -12
- package/build/module/lib/encoder.d.ts +0 -59
- package/build/module/lib/encoder.js +0 -47
- package/build/module/lib/encoder.spec.d.ts +0 -1
- package/build/module/lib/encoder.spec.js +0 -21
- package/build/module/lib/errors.js +0 -164
- package/build/module/lib/errors.spec.d.ts +0 -1
- package/build/module/lib/errors.spec.js +0 -85
- package/build/module/lib/http.d.ts +0 -50
- package/build/module/lib/http.js +0 -108
- package/build/module/lib/http.spec.d.ts +0 -1
- package/build/module/lib/http.spec.js +0 -54
- package/build/module/lib/middleware.js +0 -32
- package/build/module/lib/runtime.d.ts +0 -5
- package/build/module/lib/runtime.js +0 -21
- package/build/module/lib/stream.d.ts +0 -76
- package/build/module/lib/stream.js +0 -2
- package/build/module/lib/transport.d.ts +0 -13
- package/build/module/lib/transport.js +0 -2
- package/build/module/lib/unary.d.ts +0 -15
- package/build/module/lib/unary.js +0 -2
- package/build/module/lib/url.d.ts +0 -38
- package/build/module/lib/url.js +0 -65
- package/build/module/lib/url.spec.d.ts +0 -1
- package/build/module/lib/url.spec.js +0 -34
- package/build/module/lib/util/log.d.ts +0 -2
- package/build/module/lib/util/log.js +0 -11
- package/build/module/lib/websocket.d.ts +0 -25
- package/build/module/lib/websocket.js +0 -181
- package/build/module/lib/websocket.spec.d.ts +0 -1
- package/build/module/lib/websocket.spec.js +0 -82
- package/build/module/lib/ws.spec.d.ts +0 -1
- package/build/module/lib/ws.spec.js +0 -94
- package/build/tsconfig.module.tsbuildinfo +0 -1
- package/build/tsconfig.tsbuildinfo +0 -1
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/caseconv.ts.html +0 -124
- package/coverage/encoder.ts.html +0 -403
- package/coverage/errors.ts.html +0 -727
- package/coverage/favicon.png +0 -0
- package/coverage/http.ts.html +0 -532
- package/coverage/index.html +0 -221
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/caseconv.ts.html +0 -124
- package/coverage/lcov-report/encoder.ts.html +0 -403
- package/coverage/lcov-report/errors.ts.html +0 -727
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/http.ts.html +0 -532
- package/coverage/lcov-report/index.html +0 -221
- package/coverage/lcov-report/middleware.ts.html +0 -286
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/runtime.ts.html +0 -154
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -196
- package/coverage/lcov-report/url.ts.html +0 -322
- package/coverage/lcov-report/websocket.ts.html +0 -760
- package/coverage/lcov.info +0 -552
- package/coverage/middleware.ts.html +0 -286
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/runtime.ts.html +0 -154
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -196
- package/coverage/url.ts.html +0 -322
- package/coverage/websocket.ts.html +0 -760
- package/src/lib/caseconv.ts +0 -13
- package/src/lib/encoder.spec.ts +0 -23
- package/src/lib/encoder.ts +0 -105
- package/src/lib/errors.spec.ts +0 -98
- package/src/lib/errors.ts +0 -214
- package/src/lib/http.spec.ts +0 -85
- package/src/lib/http.ts +0 -149
- package/src/lib/runtime.ts +0 -23
- package/src/lib/transport.ts +0 -14
- package/src/lib/unary.ts +0 -21
- package/src/lib/url.spec.ts +0 -37
- package/src/lib/url.ts +0 -79
- package/src/lib/util/log.ts +0 -12
- package/src/lib/websocket.spec.ts +0 -106
- package/src/lib/websocket.ts +0 -225
- package/src/types/example.d.ts +0 -24
- package/tsconfig.module.json +0 -9
- package/yarn.lock +0 -5878
- /package/{build/main/lib → dist}/errors.spec.d.ts +0 -0
- /package/{build/main/lib → dist}/http.spec.d.ts +0 -0
- /package/{build/main/lib → dist}/websocket.spec.d.ts +0 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Copyright 2023 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
9
|
+
|
|
10
|
+
import { URL } from "@synnaxlabs/x";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
export interface TypedError extends Error {
|
|
14
|
+
discriminator: "FreighterError";
|
|
15
|
+
/**
|
|
16
|
+
* @description Returns a unique type identifier for the error. Freighter uses this to
|
|
17
|
+
* determine the correct decoder to use on the other end of the freighter.
|
|
18
|
+
*/
|
|
19
|
+
type: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class BaseTypedError extends Error implements TypedError {
|
|
23
|
+
discriminator: "FreighterError" = "FreighterError";
|
|
24
|
+
type: string;
|
|
25
|
+
|
|
26
|
+
constructor(message: string, type: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.type = type;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ErrorDecoder = (encoded: ErrorPayload) => Error | null;
|
|
33
|
+
type ErrorEncoder = (error: TypedError) => ErrorPayload | null;
|
|
34
|
+
|
|
35
|
+
export const isTypedError = (error: unknown): error is TypedError => {
|
|
36
|
+
if (error == null || typeof error !== "object") return false;
|
|
37
|
+
const typedError = error as TypedError;
|
|
38
|
+
if (typedError.discriminator !== "FreighterError") return false;
|
|
39
|
+
if (!("type" in typedError))
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Freighter error is missing its type property: ${JSON.stringify(typedError)}`,
|
|
42
|
+
);
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const assertErrorType = <T>(type: string, error?: Error | null): T => {
|
|
47
|
+
if (error == null)
|
|
48
|
+
throw new Error(`Expected error of type ${type} but got nothing instead`);
|
|
49
|
+
if (!isTypedError(error))
|
|
50
|
+
throw new Error(`Expected a typed error, got: ${error.message}`);
|
|
51
|
+
if (error.type !== type)
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Expected error of type ${type}, got ${error.type}: ${error.message}`,
|
|
54
|
+
);
|
|
55
|
+
return error as unknown as T;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const UNKNOWN = "unknown";
|
|
59
|
+
export const NONE = "nil";
|
|
60
|
+
export const FREIGHTER = "freighter";
|
|
61
|
+
|
|
62
|
+
export const errorZ = z.object({ type: z.string(), data: z.string() });
|
|
63
|
+
|
|
64
|
+
export type ErrorPayload = z.infer<typeof errorZ>;
|
|
65
|
+
|
|
66
|
+
interface errorProvider {
|
|
67
|
+
encode: ErrorEncoder;
|
|
68
|
+
decode: ErrorDecoder;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class Registry {
|
|
72
|
+
private readonly providers: errorProvider[] = [];
|
|
73
|
+
|
|
74
|
+
register(provider: errorProvider): void {
|
|
75
|
+
this.providers.push(provider);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
encode(error: unknown): ErrorPayload {
|
|
79
|
+
if (error == null) return { type: NONE, data: "" };
|
|
80
|
+
if (isTypedError(error)) {
|
|
81
|
+
for (const provider of this.providers) {
|
|
82
|
+
const payload = provider.encode(error);
|
|
83
|
+
if (payload != null) return payload;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { type: UNKNOWN, data: JSON.stringify(error) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
decode(payload?: ErrorPayload | null): Error | null {
|
|
90
|
+
if (payload == null || payload.type === NONE) return null;
|
|
91
|
+
if (payload.type === UNKNOWN) return new UnknownError(payload.data);
|
|
92
|
+
for (const provider of this.providers) {
|
|
93
|
+
const error = provider.decode(payload);
|
|
94
|
+
if (error != null) return error;
|
|
95
|
+
}
|
|
96
|
+
return new UnknownError(payload.data);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const REGISTRY = new Registry();
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Registers a custom error type with the error registry, which allows it to be
|
|
104
|
+
* encoded/decoded and sent over the network.
|
|
105
|
+
*
|
|
106
|
+
* @param type - A unique string identifier for the error type.
|
|
107
|
+
* @param encode - A function that encodes the error into a string.
|
|
108
|
+
* @param decode - A function that decodes the error from a string.
|
|
109
|
+
*/
|
|
110
|
+
export const registerError = ({
|
|
111
|
+
encode,
|
|
112
|
+
decode,
|
|
113
|
+
}: {
|
|
114
|
+
encode: ErrorEncoder;
|
|
115
|
+
decode: ErrorDecoder;
|
|
116
|
+
}): void => REGISTRY.register({ encode, decode });
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Encodes an error into a payload that can be sent between a freighter server
|
|
120
|
+
* and client.
|
|
121
|
+
* @param error - The error to encode.
|
|
122
|
+
* @returns The encoded error.
|
|
123
|
+
*/
|
|
124
|
+
export const encodeError = (error: unknown): ErrorPayload => {
|
|
125
|
+
return REGISTRY.encode(error);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Decodes an error payload into an exception. If a custom decoder can be found
|
|
130
|
+
* for the error type, it will be used. Otherwise, a generic Error containing
|
|
131
|
+
* the error data is returned.
|
|
132
|
+
*
|
|
133
|
+
* @param payload - The encoded error payload.
|
|
134
|
+
* @returns The decoded error.
|
|
135
|
+
*/
|
|
136
|
+
export const decodeError = (payload: ErrorPayload): Error | null => {
|
|
137
|
+
return REGISTRY.decode(payload);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export class UnknownError extends BaseTypedError implements TypedError {
|
|
141
|
+
constructor(message: string) {
|
|
142
|
+
super(message, UNKNOWN);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Thrown/returned when a stream closed normally. */
|
|
147
|
+
export class EOF extends BaseTypedError implements TypedError {
|
|
148
|
+
constructor() {
|
|
149
|
+
super("EOF", FREIGHTER);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Thrown/returned when a stream is closed abnormally. */
|
|
154
|
+
export class StreamClosed extends BaseTypedError implements TypedError {
|
|
155
|
+
constructor() {
|
|
156
|
+
super("StreamClosed", FREIGHTER);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface UnreachableArgs {
|
|
161
|
+
message?: string;
|
|
162
|
+
url?: URL;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Thrown when a target is unreachable. */
|
|
166
|
+
export class Unreachable extends BaseTypedError implements TypedError {
|
|
167
|
+
url: URL;
|
|
168
|
+
|
|
169
|
+
constructor(args: UnreachableArgs = {}) {
|
|
170
|
+
const { message = "Unreachable", url = URL.UNKNOWN } = args;
|
|
171
|
+
super(message, FREIGHTER);
|
|
172
|
+
this.url = url;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const FREIGHTER_ERROR_TYPE = "freighter";
|
|
177
|
+
|
|
178
|
+
const freighterErrorEncoder: ErrorEncoder = (error: TypedError) => {
|
|
179
|
+
if (error.type !== FREIGHTER) return null;
|
|
180
|
+
if (error instanceof EOF) return { type: FREIGHTER_ERROR_TYPE, data: "EOF" };
|
|
181
|
+
if (error instanceof StreamClosed)
|
|
182
|
+
return { type: FREIGHTER_ERROR_TYPE, data: "StreamClosed" };
|
|
183
|
+
if (error instanceof Unreachable)
|
|
184
|
+
return { type: FREIGHTER_ERROR_TYPE, data: "Unreachable" };
|
|
185
|
+
throw new Error(`Unknown error type: ${error.type}: ${error.message}`);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const freighterErrorDecoder: ErrorDecoder = (encoded: ErrorPayload) => {
|
|
189
|
+
if (encoded.type !== FREIGHTER_ERROR_TYPE) return null;
|
|
190
|
+
switch (encoded.data) {
|
|
191
|
+
case "EOF":
|
|
192
|
+
return new EOF();
|
|
193
|
+
case "StreamClosed":
|
|
194
|
+
return new StreamClosed();
|
|
195
|
+
case "Unreachable":
|
|
196
|
+
return new Unreachable();
|
|
197
|
+
default:
|
|
198
|
+
throw new Error(`Unknown error type: ${encoded.data}`);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
registerError({
|
|
203
|
+
encode: freighterErrorEncoder,
|
|
204
|
+
decode: freighterErrorDecoder,
|
|
205
|
+
});
|
package/src/http.spec.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Copyright 2023 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
9
|
+
|
|
10
|
+
import { URL, binary } from "@synnaxlabs/x";
|
|
11
|
+
import { describe, expect, test } from "vitest";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
import { HTTPClient } from "@/http";
|
|
15
|
+
|
|
16
|
+
const ENDPOINT = new URL({
|
|
17
|
+
host: "127.0.0.1",
|
|
18
|
+
protocol: "http",
|
|
19
|
+
port: 8080,
|
|
20
|
+
pathPrefix: "unary",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const client = new HTTPClient(ENDPOINT, new binary.JSONEncoderDecoder());
|
|
24
|
+
|
|
25
|
+
const messageZ = z.object({
|
|
26
|
+
id: z.number().optional(),
|
|
27
|
+
message: z.string().optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("http", () => {
|
|
31
|
+
test("echo", async () => {
|
|
32
|
+
const [response, error] = await client.send<typeof messageZ>(
|
|
33
|
+
"/echo",
|
|
34
|
+
{
|
|
35
|
+
id: 1,
|
|
36
|
+
message: "hello",
|
|
37
|
+
},
|
|
38
|
+
messageZ,
|
|
39
|
+
);
|
|
40
|
+
expect(error).toBeNull();
|
|
41
|
+
expect(response).toEqual({ id: 2, message: "hello" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("not found", async () => {
|
|
45
|
+
const [response, error] = await client.send<typeof messageZ>(
|
|
46
|
+
"/not-found",
|
|
47
|
+
{},
|
|
48
|
+
messageZ,
|
|
49
|
+
);
|
|
50
|
+
expect(error?.message).toEqual("Not Found");
|
|
51
|
+
expect(response).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("middleware", async () => {
|
|
55
|
+
client.use(async (md, next) => {
|
|
56
|
+
md.params.Test = "test";
|
|
57
|
+
return await next(md);
|
|
58
|
+
});
|
|
59
|
+
const [response, error] = await client.send<typeof messageZ>(
|
|
60
|
+
"/middlewareCheck",
|
|
61
|
+
{},
|
|
62
|
+
messageZ,
|
|
63
|
+
);
|
|
64
|
+
expect(error).toBeNull();
|
|
65
|
+
expect(response?.message).toEqual("");
|
|
66
|
+
});
|
|
67
|
+
});
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
// Copyright 2023 Synnax Labs, Inc.
|
|
3
|
+
//
|
|
4
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
5
|
+
// licenses/BSL.txt.
|
|
6
|
+
//
|
|
7
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
8
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
9
|
+
// included in the file licenses/APL.txt.
|
|
10
|
+
|
|
11
|
+
import { runtime, type URL, type binary } from "@synnaxlabs/x";
|
|
12
|
+
import { type z } from "zod";
|
|
13
|
+
|
|
14
|
+
import { errorZ, decodeError, Unreachable } from "@/errors";
|
|
15
|
+
import { type Context, MiddlewareCollector } from "@/middleware";
|
|
16
|
+
import { type UnaryClient } from "@/unary";
|
|
17
|
+
|
|
18
|
+
export const CONTENT_TYPE_HEADER_KEY = "Content-Type";
|
|
19
|
+
|
|
20
|
+
const resolveFetchAPI = (protocol: "http" | "https"): typeof fetch => {
|
|
21
|
+
if (runtime.RUNTIME !== "node") return fetch;
|
|
22
|
+
const _fetch: typeof fetch = require("node-fetch");
|
|
23
|
+
if (protocol === "http") return _fetch;
|
|
24
|
+
const https = require("https");
|
|
25
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
26
|
+
// @ts-expect-error - TS doesn't know about qhis option
|
|
27
|
+
return async (info, init) => await _fetch(info, { ...init, agent });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* HTTPClientFactory provides a POST and GET implementation of the Unary
|
|
32
|
+
* protocol.
|
|
33
|
+
*
|
|
34
|
+
* @param url - The base URL of the API.
|
|
35
|
+
* @param encoder - The encoder/decoder to use for the request/response.
|
|
36
|
+
*/
|
|
37
|
+
export class HTTPClient extends MiddlewareCollector implements UnaryClient {
|
|
38
|
+
endpoint: URL;
|
|
39
|
+
encoder: binary.EncoderDecoder;
|
|
40
|
+
fetch: typeof fetch;
|
|
41
|
+
|
|
42
|
+
constructor(endpoint: URL, encoder: binary.EncoderDecoder, secure: boolean = false) {
|
|
43
|
+
super();
|
|
44
|
+
this.endpoint = endpoint.replace({ protocol: secure ? "https" : "http" });
|
|
45
|
+
this.encoder = encoder;
|
|
46
|
+
this.fetch = resolveFetchAPI(this.endpoint.protocol as "http" | "https");
|
|
47
|
+
|
|
48
|
+
return new Proxy(this, {
|
|
49
|
+
get: (target, prop, receiver) => {
|
|
50
|
+
if (prop === "endpoint") return this.endpoint;
|
|
51
|
+
return Reflect.get(target, prop, receiver);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get headers(): Record<string, string> {
|
|
57
|
+
return {
|
|
58
|
+
[CONTENT_TYPE_HEADER_KEY]: this.encoder.contentType,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async send<RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(
|
|
63
|
+
target: string,
|
|
64
|
+
req: z.input<RQ> | null,
|
|
65
|
+
resSchema: RS | null,
|
|
66
|
+
): Promise<[z.output<RS> | null, Error | null]> {
|
|
67
|
+
let rs: RS | null = null;
|
|
68
|
+
const url = this.endpoint.child(target);
|
|
69
|
+
const request: RequestInit = {};
|
|
70
|
+
request.method = "POST";
|
|
71
|
+
request.body = this.encoder.encode(req ?? {});
|
|
72
|
+
|
|
73
|
+
const [, err] = await this.executeMiddleware(
|
|
74
|
+
{ target: url.toString(), protocol: this.endpoint.protocol, params: {}, role: "client" },
|
|
75
|
+
async (ctx: Context): Promise<[Context, Error | null]> => {
|
|
76
|
+
const outCtx: Context = { ...ctx, params: {} };
|
|
77
|
+
request.headers = {
|
|
78
|
+
...this.headers,
|
|
79
|
+
...ctx.params,
|
|
80
|
+
};
|
|
81
|
+
let httpRes: Response;
|
|
82
|
+
try {
|
|
83
|
+
const f = resolveFetchAPI(ctx.protocol as "http" | "https");
|
|
84
|
+
httpRes = await f(ctx.target, request);
|
|
85
|
+
} catch (err_) {
|
|
86
|
+
let err = err_ as Error;
|
|
87
|
+
if (err.message === "Load failed") err = new Unreachable({ url });
|
|
88
|
+
return [outCtx, err];
|
|
89
|
+
}
|
|
90
|
+
const data = await httpRes.arrayBuffer();
|
|
91
|
+
if (httpRes?.ok) {
|
|
92
|
+
if (resSchema != null) rs = this.encoder.decode(data, resSchema);
|
|
93
|
+
return [outCtx, null];
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
if (httpRes.status !== 400) return [outCtx, new Error(httpRes.statusText)];
|
|
97
|
+
const err = this.encoder.decode(data, errorZ);
|
|
98
|
+
const decoded = decodeError(err);
|
|
99
|
+
return [outCtx, decoded];
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return [
|
|
102
|
+
outCtx,
|
|
103
|
+
new Error(
|
|
104
|
+
`[freighter] - failed to decode error: ${httpRes.statusText}: ${
|
|
105
|
+
(e as Error).message
|
|
106
|
+
}`,
|
|
107
|
+
),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return [rs, err];
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
// Copyright 2023 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
9
|
+
|
|
2
10
|
export {
|
|
3
|
-
MsgpackEncoderDecoder,
|
|
4
|
-
JSONEncoderDecoder,
|
|
5
|
-
registerCustomTypeEncoder,
|
|
6
|
-
ENCODERS,
|
|
7
|
-
} from './lib/encoder';
|
|
8
|
-
export { StreamClient, Stream } from './lib/stream';
|
|
9
|
-
export { UnaryClient } from './lib/unary';
|
|
10
|
-
export { HTTPClientFactory } from './lib/http';
|
|
11
|
-
export { default as URL } from './lib/url';
|
|
12
|
-
export {
|
|
13
|
-
encodeError,
|
|
14
|
-
decodeError,
|
|
15
|
-
registerError,
|
|
16
11
|
BaseTypedError,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ErrorPayloadSchema,
|
|
12
|
+
decodeError,
|
|
13
|
+
encodeError,
|
|
20
14
|
EOF,
|
|
15
|
+
errorZ,
|
|
16
|
+
registerError,
|
|
21
17
|
StreamClosed,
|
|
22
18
|
Unreachable,
|
|
23
|
-
} from
|
|
24
|
-
export {
|
|
25
|
-
export {
|
|
19
|
+
} from "@/errors";
|
|
20
|
+
export type { ErrorPayload as ErrorPayload, TypedError } from "@/errors";
|
|
21
|
+
export { HTTPClient } from "@/http";
|
|
22
|
+
export type { Context as MetaData, Middleware, Next } from "@/middleware";
|
|
23
|
+
export type { Stream, StreamClient } from "@/stream";
|
|
24
|
+
export type { UnaryClient } from "@/unary";
|
|
25
|
+
export { sendRequired } from "@/unary";
|
|
26
|
+
export { WebSocketClient } from "@/websocket";
|
|
@@ -1,34 +1,44 @@
|
|
|
1
|
+
// Copyright 2023 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
|
-
*
|
|
11
|
+
* Context is the metadata associated with a freighter transport request.
|
|
3
12
|
*
|
|
4
13
|
* @property target - The target the request is being issued to.
|
|
5
14
|
* @property protocol - The protocol used to issue the request.
|
|
6
15
|
* @property params - Arbitrary string parameters that can be set by client side
|
|
7
16
|
* middleware and read by server side middleware.
|
|
8
17
|
*/
|
|
9
|
-
export
|
|
18
|
+
export interface Context {
|
|
10
19
|
target: string;
|
|
20
|
+
role: Role;
|
|
11
21
|
protocol: string;
|
|
12
22
|
params: Record<string, string>;
|
|
13
|
-
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ROLES = ["client", "server"] as const;
|
|
26
|
+
export type Role = (typeof ROLES)[number];
|
|
14
27
|
|
|
15
28
|
/** Next executes the next middleware in the chain. */
|
|
16
|
-
export type Next = (
|
|
29
|
+
export type Next = (ctx: Context) => Promise<[Context, Error | null]>;
|
|
17
30
|
|
|
18
31
|
/**
|
|
19
32
|
* Middleware represents a general middleware function that can be used to
|
|
20
33
|
* parse/attach metadata to a request or alter its behavior.
|
|
21
34
|
*/
|
|
22
|
-
export type Middleware = (
|
|
23
|
-
md: MetaData,
|
|
24
|
-
next: Next
|
|
25
|
-
) => Promise<Error | undefined>;
|
|
35
|
+
export type Middleware = (ctx: Context, next: Next) => Promise<[Context, Error | null]>;
|
|
26
36
|
|
|
27
37
|
/**
|
|
28
38
|
* Finalizer is a middleware that is executed as the last step in the chain.
|
|
29
39
|
* Finalizer middleware should be used to execute the request.
|
|
30
40
|
*/
|
|
31
|
-
type Finalizer = (
|
|
41
|
+
type Finalizer = (ctx: Context) => Promise<[Context, Error | null]>;
|
|
32
42
|
|
|
33
43
|
/**
|
|
34
44
|
* MiddlewareCollector is a class that can be used to collect and execute
|
|
@@ -38,7 +48,7 @@ export class MiddlewareCollector {
|
|
|
38
48
|
middleware: Middleware[] = [];
|
|
39
49
|
|
|
40
50
|
/** Implements the Transport interface */
|
|
41
|
-
use(...mw: Middleware[]) {
|
|
51
|
+
use(...mw: Middleware[]): void {
|
|
42
52
|
this.middleware.push(...mw);
|
|
43
53
|
}
|
|
44
54
|
|
|
@@ -47,21 +57,21 @@ export class MiddlewareCollector {
|
|
|
47
57
|
* until the end of the chain is reached. It then calls the finalizer with the
|
|
48
58
|
* metadata.
|
|
49
59
|
*
|
|
50
|
-
* @param
|
|
60
|
+
* @param ctx - The context to pass to the middleware.
|
|
51
61
|
* @param finalizer - The finalizer to call with the metadata.
|
|
52
62
|
* @returns An error if one was encountered, otherwise undefined.
|
|
53
63
|
*/
|
|
54
|
-
executeMiddleware(
|
|
55
|
-
|
|
56
|
-
finalizer: Finalizer
|
|
57
|
-
): Promise<Error |
|
|
64
|
+
async executeMiddleware(
|
|
65
|
+
ctx: Context,
|
|
66
|
+
finalizer: Finalizer,
|
|
67
|
+
): Promise<[Context, Error | null]> {
|
|
58
68
|
let i = 0;
|
|
59
|
-
const next = (md:
|
|
60
|
-
if (i
|
|
69
|
+
const next = async (md: Context): Promise<[Context, Error | null]> => {
|
|
70
|
+
if (i === this.middleware.length) return await finalizer(md);
|
|
61
71
|
const _mw = this.middleware[i];
|
|
62
72
|
i++;
|
|
63
|
-
return _mw(md, next);
|
|
73
|
+
return await _mw(md, next);
|
|
64
74
|
};
|
|
65
|
-
return next(
|
|
75
|
+
return await next(ctx);
|
|
66
76
|
}
|
|
67
77
|
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
// Copyright 2023 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
2
9
|
|
|
3
|
-
import {
|
|
10
|
+
import { type z } from "zod";
|
|
11
|
+
|
|
12
|
+
import { type Transport } from "@/transport";
|
|
4
13
|
|
|
5
14
|
/**
|
|
6
15
|
* Interface for an entity that receives a stream of responses.
|
|
7
16
|
*/
|
|
8
|
-
export interface StreamReceiver<RS> {
|
|
17
|
+
export interface StreamReceiver<RS extends z.ZodTypeAny> {
|
|
9
18
|
/**
|
|
10
19
|
* Receives a response from the stream. It's not safe to call receive
|
|
11
20
|
* concurrently.
|
|
@@ -15,18 +24,18 @@ export interface StreamReceiver<RS> {
|
|
|
15
24
|
* returns the error the server returned.
|
|
16
25
|
* @raises Error: if the transport fails.
|
|
17
26
|
*/
|
|
18
|
-
receive()
|
|
27
|
+
receive: () => Promise<[z.output<RS>, null] | [null, Error]>;
|
|
19
28
|
|
|
20
29
|
/**
|
|
21
30
|
* @returns true if the stream has received a response
|
|
22
31
|
*/
|
|
23
|
-
received()
|
|
32
|
+
received: () => boolean;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
/**
|
|
27
36
|
* Interface for an entity that sends a stream of requests.
|
|
28
37
|
*/
|
|
29
|
-
export interface StreamSender<RQ> {
|
|
38
|
+
export interface StreamSender<RQ extends z.ZodTypeAny> {
|
|
30
39
|
/**
|
|
31
40
|
* Sends a request to the stream. It is not safe to call send concurrently
|
|
32
41
|
* with closeSend or send.
|
|
@@ -38,14 +47,14 @@ export interface StreamSender<RQ> {
|
|
|
38
47
|
* @raises freighter.StreamClosed: if the client called close_send()
|
|
39
48
|
* @raises Error: if the transport fails.
|
|
40
49
|
*/
|
|
41
|
-
send(req: RQ)
|
|
50
|
+
send: (req: z.input<RQ>) => Error | null;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
/**
|
|
45
54
|
* Extension of the StreamSender interface that allows the client to close the sending
|
|
46
55
|
* direction of the stream when finished issuing requrest.
|
|
47
56
|
*/
|
|
48
|
-
export interface StreamSenderCloser<RQ> extends StreamSender<RQ> {
|
|
57
|
+
export interface StreamSenderCloser<RQ extends z.ZodTypeAny> extends StreamSender<RQ> {
|
|
49
58
|
/**
|
|
50
59
|
* Lets the server know no more messages will be sent. If the client attempts
|
|
51
60
|
* to call send() after calling closeSend(), a freighter.StreamClosed
|
|
@@ -55,13 +64,13 @@ export interface StreamSenderCloser<RQ> extends StreamSender<RQ> {
|
|
|
55
64
|
* After calling close_send, the client is responsible for calling receive()
|
|
56
65
|
* to successfully receive the server's acknowledgement.
|
|
57
66
|
*/
|
|
58
|
-
closeSend()
|
|
67
|
+
closeSend: () => void;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
/**
|
|
62
71
|
* Interface for a bidirectional stream between a client and a server.
|
|
63
72
|
*/
|
|
64
|
-
export interface Stream<RQ, RS>
|
|
73
|
+
export interface Stream<RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>
|
|
65
74
|
extends StreamSenderCloser<RQ>,
|
|
66
75
|
StreamReceiver<RS> {}
|
|
67
76
|
|
|
@@ -80,9 +89,9 @@ export interface StreamClient extends Transport {
|
|
|
80
89
|
* @param resSchema - The schema for the response type. This is used to
|
|
81
90
|
* validate the response before returning it.
|
|
82
91
|
*/
|
|
83
|
-
stream<RQ, RS>(
|
|
92
|
+
stream: <RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(
|
|
84
93
|
target: string,
|
|
85
|
-
reqSchema:
|
|
86
|
-
resSchema:
|
|
87
|
-
)
|
|
94
|
+
reqSchema: RQ,
|
|
95
|
+
resSchema: RS,
|
|
96
|
+
) => Promise<Stream<RQ, RS>>;
|
|
88
97
|
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Copyright 2023 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
9
|
+
|
|
10
|
+
import { type Middleware } from "@/middleware";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Transport is a based interface that represents a general transport for
|
|
14
|
+
* exchanging messages between a client and a server.
|
|
15
|
+
*/
|
|
16
|
+
export interface Transport {
|
|
17
|
+
/**
|
|
18
|
+
* Use registers middleware that will be executed in order when the transport
|
|
19
|
+
*
|
|
20
|
+
* @param mw - The middleware to register.
|
|
21
|
+
*/
|
|
22
|
+
use: (...mw: Middleware[]) => void;
|
|
23
|
+
}
|
package/src/unary.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Copyright 2023 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
9
|
+
|
|
10
|
+
import { type z } from "zod";
|
|
11
|
+
|
|
12
|
+
import { type Transport } from "@/transport";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* An interface for an entity that implements a simple request-response
|
|
16
|
+
* transport between two entities.
|
|
17
|
+
*/
|
|
18
|
+
export interface UnaryClient extends Transport {
|
|
19
|
+
/**
|
|
20
|
+
* Sends a request to the target server and waits until a response is received.
|
|
21
|
+
* @param target - The target server to send the request to.
|
|
22
|
+
* @param req - The request to send.
|
|
23
|
+
* @param resSchema - The schema to validate the response against.
|
|
24
|
+
*/
|
|
25
|
+
send: <RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(
|
|
26
|
+
target: string,
|
|
27
|
+
req: z.input<RQ> | null,
|
|
28
|
+
resSchema: RS | null,
|
|
29
|
+
) => Promise<[z.output<RS>, null] | [null, Error]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const sendRequired = async <
|
|
33
|
+
RQ extends z.ZodTypeAny,
|
|
34
|
+
RS extends z.ZodTypeAny = RQ,
|
|
35
|
+
>(
|
|
36
|
+
client: UnaryClient,
|
|
37
|
+
target: string,
|
|
38
|
+
req: z.input<RQ>,
|
|
39
|
+
resSchema: RS | null,
|
|
40
|
+
): Promise<z.output<RS>> => {
|
|
41
|
+
const [res, err] = await client.send(target, req, resSchema);
|
|
42
|
+
if (err != null) throw err;
|
|
43
|
+
return res;
|
|
44
|
+
};
|