@synnaxlabs/freighter 0.41.0 → 0.42.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.
- package/dist/alamos.d.ts.map +1 -1
- package/dist/errors.d.ts +18 -87
- package/dist/errors.d.ts.map +1 -1
- package/dist/freighter.cjs +9 -9
- package/dist/freighter.js +2092 -976
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/unary.d.ts +2 -2
- package/dist/unary.d.ts.map +1 -1
- package/dist/websocket.d.ts +8 -1
- package/dist/websocket.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/alamos.ts +4 -3
- package/src/errors.spec.ts +6 -73
- package/src/errors.ts +20 -168
- package/src/http.ts +19 -12
- package/src/index.ts +2 -14
- package/src/unary.ts +7 -6
- package/src/websocket.spec.ts +9 -17
- package/src/websocket.ts +51 -29
package/dist/http.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ export declare class HTTPClient extends MiddlewareCollector implements UnaryClie
|
|
|
16
16
|
fetch: typeof fetch;
|
|
17
17
|
constructor(endpoint: URL, encoder: binary.Codec, secure?: boolean);
|
|
18
18
|
get headers(): Record<string, string>;
|
|
19
|
-
send<RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(target: string, req: z.input<RQ> | z.output<RQ>, reqSchema: RQ, resSchema: RS): Promise<[z.output<RS
|
|
19
|
+
send<RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(target: string, req: z.input<RQ> | z.output<RQ>, reqSchema: RQ, resSchema: RS): Promise<[z.output<RS>, null] | [null, Error]>;
|
|
20
20
|
}
|
|
21
21
|
//# sourceMappingURL=http.d.ts.map
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,MAAM,EAAmB,KAAK,GAAG,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B,OAAO,EAAgB,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAoBtD;;;;;;GAMG;AACH,qBAAa,UAAW,SAAQ,mBAAoB,YAAW,WAAW;IACxE,QAAQ,EAAE,GAAG,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;IACtB,KAAK,EAAE,OAAO,KAAK,CAAC;gBAER,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,GAAE,OAAe;IAczE,IAAI,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIpC;IAEK,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC,UAAU,EAAE,EAAE,SAAS,CAAC,CAAC,UAAU,GAAG,EAAE,EAC9D,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAC/B,SAAS,EAAE,EAAE,EACb,SAAS,EAAE,EAAE,GACZ,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;CAwDjD"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { EOF, StreamClosed, Unreachable } from './errors';
|
|
2
2
|
export { HTTPClient } from './http';
|
|
3
3
|
export { type Context, type Middleware, type Next } from './middleware';
|
|
4
4
|
export { type Stream, type StreamClient } from './stream';
|
|
5
5
|
export { sendRequired, type UnaryClient, unaryWithBreaker } from './unary';
|
|
6
6
|
export { WebSocketClient } from './websocket';
|
|
7
|
+
export { type WebsocketMessage } from './websocket';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/unary.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { breaker } from '@synnaxlabs/x';
|
|
1
|
+
import { breaker, errors } from '@synnaxlabs/x';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { Transport } from './transport';
|
|
4
4
|
/**
|
|
@@ -12,7 +12,7 @@ export interface UnaryClient extends Transport {
|
|
|
12
12
|
* @param req - The request to send.
|
|
13
13
|
* @param resSchema - The schema to validate the response against.
|
|
14
14
|
*/
|
|
15
|
-
send: <RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(target: string, req: z.input<RQ> | z.output<RQ>, reqSchema: RQ, resSchema: RS) => Promise<
|
|
15
|
+
send: <RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(target: string, req: z.input<RQ> | z.output<RQ>, reqSchema: RQ, resSchema: RS) => Promise<errors.Return<z.output<RS>>>;
|
|
16
16
|
}
|
|
17
17
|
export declare const unaryWithBreaker: (base: UnaryClient, cfg: breaker.Config) => UnaryClient;
|
|
18
18
|
export declare const sendRequired: <RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(client: UnaryClient, target: string, req: z.input<RQ> | z.output<RQ>, reqSchema: RQ, resSchema: RS) => Promise<z.output<RS>>;
|
package/dist/unary.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unary.d.ts","sourceRoot":"","sources":["../src/unary.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"unary.d.ts","sourceRoot":"","sources":["../src/unary.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,CAAC;AAI7B,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C;;;;;OAKG;IACH,IAAI,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,UAAU,EAAE,EAAE,SAAS,CAAC,CAAC,UAAU,GAAG,EAAE,EAC1D,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAC/B,SAAS,EAAE,EAAE,EACb,SAAS,EAAE,EAAE,KACV,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;CAC3C;AAED,eAAO,MAAM,gBAAgB,GAC3B,MAAM,WAAW,EACjB,KAAK,OAAO,CAAC,MAAM,KAClB,WA4BF,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,EAAE,SAAS,CAAC,CAAC,UAAU,EACvB,EAAE,SAAS,CAAC,CAAC,UAAU,GAAG,EAAE,EAE5B,QAAQ,WAAW,EACnB,QAAQ,MAAM,EACd,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAC/B,WAAW,EAAE,EACb,WAAW,EAAE,KACZ,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAItB,CAAC"}
|
package/dist/websocket.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { binary, URL } from '@synnaxlabs/x';
|
|
1
|
+
import { binary, errors, URL } from '@synnaxlabs/x';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { MiddlewareCollector } from './middleware';
|
|
4
4
|
import { Stream, StreamClient } from './stream';
|
|
5
|
+
export type WebsocketMessage<P = unknown> = {
|
|
6
|
+
type: "data" | "close" | "open";
|
|
7
|
+
error?: errors.Payload;
|
|
8
|
+
payload?: P;
|
|
9
|
+
};
|
|
5
10
|
export declare const FREIGHTER_METADATA_PREFIX = "freighterctx";
|
|
6
11
|
/**
|
|
7
12
|
* WebSocketClient is an implementation of StreamClient that is backed by
|
|
@@ -10,6 +15,7 @@ export declare const FREIGHTER_METADATA_PREFIX = "freighterctx";
|
|
|
10
15
|
export declare class WebSocketClient extends MiddlewareCollector implements StreamClient {
|
|
11
16
|
baseUrl: URL;
|
|
12
17
|
encoder: binary.Codec;
|
|
18
|
+
secure: boolean;
|
|
13
19
|
static readonly MESSAGE_TYPE = "arraybuffer";
|
|
14
20
|
/**
|
|
15
21
|
* @param encoder - The encoder to use for encoding messages and decoding
|
|
@@ -17,6 +23,7 @@ export declare class WebSocketClient extends MiddlewareCollector implements Stre
|
|
|
17
23
|
* @param baseEndpoint - A base url to use as a prefix for all requests.
|
|
18
24
|
*/
|
|
19
25
|
constructor(baseEndpoint: URL, encoder: binary.Codec, secure?: boolean);
|
|
26
|
+
withCodec(codec: binary.Codec): WebSocketClient;
|
|
20
27
|
/** Implements the StreamClient interface. */
|
|
21
28
|
stream<RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny = RQ>(target: string, reqSchema: RQ, resSchema: RS): Promise<Stream<RQ, RS>>;
|
|
22
29
|
private buildURL;
|
package/dist/websocket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AASA,OAAO,
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,MAAM,EAEX,MAAM,EAEN,KAAK,GAAG,EACT,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAgB,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAc1D,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,OAAO,IAAI;IAC1C,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,CAAC;CACb,CAAC;AA8GF,eAAO,MAAM,yBAAyB,iBAAiB,CAAC;AAIxD;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,mBAAoB,YAAW,YAAY;IAC9E,OAAO,EAAE,GAAG,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAEhB,MAAM,CAAC,QAAQ,CAAC,YAAY,iBAAiB;IAE7C;;;;OAIG;gBACS,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,UAAQ;IAOpE,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,GAAG,eAAe;IAM/C,6CAA6C;IACvC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,UAAU,EAAE,EAAE,SAAS,CAAC,CAAC,UAAU,GAAG,EAAE,EAChE,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,EAAE,EACb,SAAS,EAAE,EAAE,GACZ,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IAmB1B,OAAO,CAAC,QAAQ;YAWF,UAAU;CAsBzB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/freighter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "a modular transport abstraction",
|
|
6
6
|
"repository": "https://github.com/synnaxlabs/synnax/tree/main/freighter/ts",
|
|
@@ -16,19 +16,20 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"node-fetch": "^2.7.0",
|
|
18
18
|
"ws": "^8.18.1",
|
|
19
|
-
"zod": "^
|
|
20
|
-
"@synnaxlabs/alamos": "0.
|
|
21
|
-
"@synnaxlabs/x": "0.
|
|
19
|
+
"zod": "^4.0.0-beta",
|
|
20
|
+
"@synnaxlabs/alamos": "0.42.0",
|
|
21
|
+
"@synnaxlabs/x": "0.42.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^22.13.10",
|
|
25
|
+
"@vitest/coverage-v8": "^3.1.3",
|
|
25
26
|
"eslint": "^9.24.0",
|
|
26
27
|
"madge": "^8.0.0",
|
|
27
28
|
"typescript": "^5.8.3",
|
|
28
29
|
"vite": "^6.2.5",
|
|
29
30
|
"vitest": "^3.1.1",
|
|
30
|
-
"@synnaxlabs/vite-plugin": "0.0.1",
|
|
31
31
|
"@synnaxlabs/tsconfig": "0.0.2",
|
|
32
|
+
"@synnaxlabs/vite-plugin": "0.0.1",
|
|
32
33
|
"eslint-config-synnaxlabs": "0.0.1"
|
|
33
34
|
},
|
|
34
35
|
"main": "dist/freighter.cjs",
|
package/src/alamos.ts
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
import { type Instrumentation } from "@synnaxlabs/alamos";
|
|
11
11
|
|
|
12
|
-
import { type Context,type Middleware } from "@/middleware";
|
|
12
|
+
import { type Context, type Middleware } from "@/middleware";
|
|
13
13
|
|
|
14
14
|
export const middleware =
|
|
15
15
|
(instrumentation: Instrumentation): Middleware =>
|
|
16
16
|
async (context, next) => {
|
|
17
17
|
if (context.role === "client") instrumentation.T.propagate(context.params);
|
|
18
|
-
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
19
|
+
const [res, exc] = (await instrumentation.T.trace(
|
|
19
20
|
context.target,
|
|
20
21
|
"debug",
|
|
21
22
|
async (span): Promise<[Context, Error | null]> => {
|
|
@@ -23,7 +24,7 @@ export const middleware =
|
|
|
23
24
|
if (err != null) span.recordError(err);
|
|
24
25
|
return [ctx, err];
|
|
25
26
|
},
|
|
26
|
-
);
|
|
27
|
+
)) as [Context, Error | null];
|
|
27
28
|
log(context, instrumentation, exc);
|
|
28
29
|
return [res, exc];
|
|
29
30
|
};
|
package/src/errors.spec.ts
CHANGED
|
@@ -7,86 +7,19 @@
|
|
|
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 { errors } from "@synnaxlabs/x";
|
|
10
11
|
import { describe, expect, test } from "vitest";
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
-
assertErrorType,
|
|
14
|
-
BaseTypedError,
|
|
15
|
-
decodeError,
|
|
16
|
-
encodeError,
|
|
17
|
-
EOF,
|
|
18
|
-
type ErrorPayload,
|
|
19
|
-
FREIGHTER,
|
|
20
|
-
isTypedError,
|
|
21
|
-
NONE,
|
|
22
|
-
registerError,
|
|
23
|
-
StreamClosed,
|
|
24
|
-
type TypedError,
|
|
25
|
-
UNKNOWN,
|
|
26
|
-
UnknownError,
|
|
27
|
-
Unreachable,
|
|
28
|
-
} from "@/errors";
|
|
29
|
-
|
|
30
|
-
class MyCustomError extends BaseTypedError {
|
|
31
|
-
type = "MyCustomError";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const myCustomErrorEncoder = (error: MyCustomError): ErrorPayload | null => {
|
|
35
|
-
if (error.type !== "MyCustomError") return null;
|
|
36
|
-
return { type: "MyCustomError", data: error.message };
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const myCustomErrorDecoder = (encoded: ErrorPayload): TypedError =>
|
|
40
|
-
new MyCustomError(encoded.data);
|
|
13
|
+
import { EOF, FreighterError, StreamClosed, Unreachable } from "@/errors";
|
|
41
14
|
|
|
42
15
|
describe("errors", () => {
|
|
43
|
-
test("isTypedError", () => {
|
|
44
|
-
const error = new MyCustomError("test");
|
|
45
|
-
const fError = isTypedError(error);
|
|
46
|
-
expect(fError).toBeTruthy();
|
|
47
|
-
expect(error.type).toEqual("MyCustomError");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("encoding and decoding a custom error through registry", () => {
|
|
51
|
-
registerError({
|
|
52
|
-
encode: myCustomErrorEncoder,
|
|
53
|
-
decode: myCustomErrorDecoder,
|
|
54
|
-
});
|
|
55
|
-
const error = new MyCustomError("test");
|
|
56
|
-
const encoded = encodeError(error);
|
|
57
|
-
expect(encoded.type).toEqual("MyCustomError");
|
|
58
|
-
expect(encoded.data).toEqual("test");
|
|
59
|
-
const decoded = assertErrorType<MyCustomError>(
|
|
60
|
-
"MyCustomError",
|
|
61
|
-
decodeError(encoded),
|
|
62
|
-
);
|
|
63
|
-
expect(decoded.message).toEqual("test");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("encoding and decoding a null error", () => {
|
|
67
|
-
const encoded = encodeError(null);
|
|
68
|
-
expect(encoded.type).toEqual(NONE);
|
|
69
|
-
expect(encoded.data).toEqual("");
|
|
70
|
-
const decoded = decodeError(encoded);
|
|
71
|
-
expect(decoded).toBeNull();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("encoding and decoding an unrecognized error", () => {
|
|
75
|
-
const error = new Error("test");
|
|
76
|
-
const encoded = encodeError(error);
|
|
77
|
-
expect(encoded.type).toEqual(UNKNOWN);
|
|
78
|
-
expect(encoded.data).toEqual("{}");
|
|
79
|
-
const decoded = decodeError(encoded);
|
|
80
|
-
expect(decoded).toEqual(new UnknownError("{}"));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
16
|
test("encoding and decoding freighter errors", () => {
|
|
84
17
|
[new EOF(), new StreamClosed(), new Unreachable()].forEach((error) => {
|
|
85
|
-
const encoded =
|
|
86
|
-
expect(encoded.type.startsWith(
|
|
18
|
+
const encoded = errors.encode(error);
|
|
19
|
+
expect(encoded.type.startsWith(FreighterError.TYPE)).toBeTruthy();
|
|
87
20
|
expect(encoded.data).toEqual(error.message);
|
|
88
|
-
const decoded =
|
|
89
|
-
expect(decoded).
|
|
21
|
+
const decoded = errors.decode(encoded);
|
|
22
|
+
expect(error.matches(decoded)).toBeTruthy();
|
|
90
23
|
});
|
|
91
24
|
});
|
|
92
25
|
});
|
package/src/errors.ts
CHANGED
|
@@ -7,189 +7,41 @@
|
|
|
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 { URL } from "@synnaxlabs/x";
|
|
11
|
-
import { z } from "zod";
|
|
12
|
-
|
|
13
|
-
/** Basic interface for an error or error type that can be matched against a candidate error */
|
|
14
|
-
export interface MatchableErrorType {
|
|
15
|
-
/**
|
|
16
|
-
* @returns a function that matches errors of the given type. Returns true if
|
|
17
|
-
* the provided instance of Error or a string message contains the provided error type.
|
|
18
|
-
*/
|
|
19
|
-
matches: (e: string | Error | unknown) => boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface TypedError extends Error {
|
|
23
|
-
discriminator: "FreighterError";
|
|
24
|
-
/**
|
|
25
|
-
* @description Returns a unique type identifier for the error. Freighter uses this to
|
|
26
|
-
* determine the correct decoder to use on the other end of the freighter.
|
|
27
|
-
*/
|
|
28
|
-
type: string;
|
|
29
|
-
}
|
|
10
|
+
import { errors, URL } from "@synnaxlabs/x";
|
|
30
11
|
|
|
31
12
|
/**
|
|
32
|
-
* @
|
|
33
|
-
* @returns a function that matches errors of the given type. Returns true if
|
|
34
|
-
* the provided instance of Error or a string message contains the provided error type.
|
|
13
|
+
* @description Base class for all freighter-specific errors
|
|
35
14
|
*/
|
|
36
|
-
export
|
|
37
|
-
(type: string) =>
|
|
38
|
-
(e: string | Error | unknown): boolean => {
|
|
39
|
-
if (e != null && typeof e === "object" && "type" in e && typeof e.type === "string")
|
|
40
|
-
return e.type.includes(type);
|
|
41
|
-
if (e instanceof Error) return e.message.includes(type);
|
|
42
|
-
if (typeof e !== "string") return false;
|
|
43
|
-
return e.includes(type);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export class BaseTypedError extends Error implements TypedError {
|
|
47
|
-
readonly discriminator = "FreighterError";
|
|
48
|
-
type: string = "";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
type ErrorDecoder = (encoded: ErrorPayload) => Error | null;
|
|
52
|
-
type ErrorEncoder = (error: TypedError) => ErrorPayload | null;
|
|
53
|
-
|
|
54
|
-
export const isTypedError = (error: unknown): error is TypedError => {
|
|
55
|
-
if (error == null || typeof error !== "object") return false;
|
|
56
|
-
const typedError = error as TypedError;
|
|
57
|
-
if (typedError.discriminator !== "FreighterError") return false;
|
|
58
|
-
if (!("type" in typedError))
|
|
59
|
-
throw new Error(
|
|
60
|
-
`Freighter error is missing its type property: ${JSON.stringify(typedError)}`,
|
|
61
|
-
);
|
|
62
|
-
return true;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const assertErrorType = <T>(type: string, error?: Error | null): T => {
|
|
66
|
-
if (error == null)
|
|
67
|
-
throw new Error(`Expected error of type ${type} but got nothing instead`);
|
|
68
|
-
if (!isTypedError(error))
|
|
69
|
-
throw new Error(`Expected a typed error, got: ${error.message}`);
|
|
70
|
-
if (error.type !== type)
|
|
71
|
-
throw new Error(
|
|
72
|
-
`Expected error of type ${type}, got ${error.type}: ${error.message}`,
|
|
73
|
-
);
|
|
74
|
-
return error as T;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const UNKNOWN = "unknown";
|
|
78
|
-
export const NONE = "nil";
|
|
79
|
-
export const FREIGHTER = "freighter";
|
|
80
|
-
|
|
81
|
-
export const errorZ = z.object({ type: z.string(), data: z.string() });
|
|
82
|
-
|
|
83
|
-
export type ErrorPayload = z.infer<typeof errorZ>;
|
|
84
|
-
|
|
85
|
-
interface errorProvider {
|
|
86
|
-
encode: ErrorEncoder;
|
|
87
|
-
decode: ErrorDecoder;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
class Registry {
|
|
91
|
-
private readonly providers: errorProvider[] = [];
|
|
92
|
-
|
|
93
|
-
register(provider: errorProvider): void {
|
|
94
|
-
this.providers.push(provider);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
encode(error: unknown): ErrorPayload {
|
|
98
|
-
if (error == null) return { type: NONE, data: "" };
|
|
99
|
-
if (isTypedError(error))
|
|
100
|
-
for (const provider of this.providers) {
|
|
101
|
-
const payload = provider.encode(error);
|
|
102
|
-
if (payload != null) return payload;
|
|
103
|
-
}
|
|
104
|
-
return { type: UNKNOWN, data: JSON.stringify(error) };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
decode(payload?: ErrorPayload | null): Error | null {
|
|
108
|
-
if (payload == null || payload.type === NONE) return null;
|
|
109
|
-
if (payload.type === UNKNOWN) return new UnknownError(payload.data);
|
|
110
|
-
for (const provider of this.providers) {
|
|
111
|
-
const error = provider.decode(payload);
|
|
112
|
-
if (error != null) return error;
|
|
113
|
-
}
|
|
114
|
-
return new UnknownError(payload.data);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const REGISTRY = new Registry();
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Registers a custom error type with the error registry, which allows it to be
|
|
122
|
-
* encoded/decoded and sent over the network.
|
|
123
|
-
*
|
|
124
|
-
* @param type - A unique string identifier for the error type.
|
|
125
|
-
* @param encode - A function that encodes the error into a string.
|
|
126
|
-
* @param decode - A function that decodes the error from a string.
|
|
127
|
-
*/
|
|
128
|
-
export const registerError = ({
|
|
129
|
-
encode,
|
|
130
|
-
decode,
|
|
131
|
-
}: {
|
|
132
|
-
encode: ErrorEncoder;
|
|
133
|
-
decode: ErrorDecoder;
|
|
134
|
-
}): void => REGISTRY.register({ encode, decode });
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Encodes an error into a payload that can be sent between a freighter server
|
|
138
|
-
* and client.
|
|
139
|
-
* @param error - The error to encode.
|
|
140
|
-
* @returns The encoded error.
|
|
141
|
-
*/
|
|
142
|
-
export const encodeError = (error: unknown): ErrorPayload => REGISTRY.encode(error);
|
|
15
|
+
export class FreighterError extends errors.createTyped("freighter") {}
|
|
143
16
|
|
|
144
17
|
/**
|
|
145
|
-
*
|
|
146
|
-
* for the error type, it will be used. Otherwise, a generic Error containing
|
|
147
|
-
* the error data is returned.
|
|
148
|
-
*
|
|
149
|
-
* @param payload - The encoded error payload.
|
|
150
|
-
* @returns The decoded error.
|
|
18
|
+
* @description Error thrown when reaching the end of a file or stream
|
|
151
19
|
*/
|
|
152
|
-
export
|
|
153
|
-
REGISTRY.decode(payload);
|
|
154
|
-
|
|
155
|
-
export class UnknownError extends BaseTypedError implements TypedError {
|
|
156
|
-
type = "unknown";
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const FREIGHTER_ERROR_TYPE = "freighter.";
|
|
160
|
-
|
|
161
|
-
/** Thrown/returned when a stream closed normally. */
|
|
162
|
-
export class EOF extends BaseTypedError implements TypedError {
|
|
163
|
-
static readonly TYPE = `${FREIGHTER_ERROR_TYPE}eof`;
|
|
164
|
-
type = EOF.TYPE;
|
|
165
|
-
static readonly matches = errorMatcher(EOF.TYPE);
|
|
166
|
-
|
|
20
|
+
export class EOF extends FreighterError.sub("eof") {
|
|
167
21
|
constructor() {
|
|
168
22
|
super("EOF");
|
|
169
23
|
}
|
|
170
24
|
}
|
|
171
25
|
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
type = StreamClosed.TYPE;
|
|
177
|
-
|
|
26
|
+
/**
|
|
27
|
+
* @description Error thrown when attempting to operate on a closed stream
|
|
28
|
+
*/
|
|
29
|
+
export class StreamClosed extends FreighterError.sub("stream_closed") {
|
|
178
30
|
constructor() {
|
|
179
31
|
super("StreamClosed");
|
|
180
32
|
}
|
|
181
33
|
}
|
|
182
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @description Arguments for constructing an Unreachable error
|
|
37
|
+
*/
|
|
183
38
|
export interface UnreachableArgs {
|
|
184
39
|
message?: string;
|
|
185
40
|
url?: URL;
|
|
186
41
|
}
|
|
187
42
|
|
|
188
|
-
/** Thrown when a target is unreachable. */
|
|
189
|
-
export class Unreachable extends
|
|
190
|
-
static readonly TYPE = `${FREIGHTER_ERROR_TYPE}unreachable`;
|
|
191
|
-
type = Unreachable.TYPE;
|
|
192
|
-
static readonly matches = errorMatcher(Unreachable.TYPE);
|
|
43
|
+
/** @description Thrown when a network target is unreachable. */
|
|
44
|
+
export class Unreachable extends FreighterError.sub("unreachable") {
|
|
193
45
|
url: URL;
|
|
194
46
|
|
|
195
47
|
constructor(args: UnreachableArgs = {}) {
|
|
@@ -199,8 +51,8 @@ export class Unreachable extends BaseTypedError implements TypedError {
|
|
|
199
51
|
}
|
|
200
52
|
}
|
|
201
53
|
|
|
202
|
-
const freighterErrorEncoder:
|
|
203
|
-
if (!error.type.startsWith(
|
|
54
|
+
const freighterErrorEncoder: errors.Encoder = (error: errors.Typed) => {
|
|
55
|
+
if (!error.type.startsWith(FreighterError.TYPE)) return null;
|
|
204
56
|
if (EOF.matches(error)) return { type: EOF.TYPE, data: "EOF" };
|
|
205
57
|
if (StreamClosed.matches(error))
|
|
206
58
|
return { type: StreamClosed.TYPE, data: "StreamClosed" };
|
|
@@ -209,8 +61,8 @@ const freighterErrorEncoder: ErrorEncoder = (error: TypedError) => {
|
|
|
209
61
|
throw new Error(`Unknown error type: ${error.type}: ${error.message}`);
|
|
210
62
|
};
|
|
211
63
|
|
|
212
|
-
const freighterErrorDecoder:
|
|
213
|
-
if (!encoded.type.startsWith(
|
|
64
|
+
const freighterErrorDecoder: errors.Decoder = (encoded: errors.Payload) => {
|
|
65
|
+
if (!encoded.type.startsWith(FreighterError.TYPE)) return null;
|
|
214
66
|
switch (encoded.type) {
|
|
215
67
|
case EOF.TYPE:
|
|
216
68
|
return new EOF();
|
|
@@ -219,11 +71,11 @@ const freighterErrorDecoder: ErrorDecoder = (encoded: ErrorPayload) => {
|
|
|
219
71
|
case Unreachable.TYPE:
|
|
220
72
|
return new Unreachable();
|
|
221
73
|
default:
|
|
222
|
-
throw new
|
|
74
|
+
throw new errors.Unknown(`Unknown error type: ${encoded.data}`);
|
|
223
75
|
}
|
|
224
76
|
};
|
|
225
77
|
|
|
226
|
-
|
|
78
|
+
errors.register({
|
|
227
79
|
encode: freighterErrorEncoder,
|
|
228
80
|
decode: freighterErrorDecoder,
|
|
229
81
|
});
|
package/src/http.ts
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
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 { type binary, runtime, type URL } from "@synnaxlabs/x";
|
|
10
|
+
import { type binary, errors, runtime, type URL } from "@synnaxlabs/x";
|
|
11
11
|
import { type z } from "zod";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { Unreachable } from "@/errors";
|
|
14
14
|
import { type Context, MiddlewareCollector } from "@/middleware";
|
|
15
15
|
import { type UnaryClient } from "@/unary";
|
|
16
16
|
|
|
@@ -28,6 +28,12 @@ const resolveFetchAPI = (protocol: "http" | "https"): typeof fetch => {
|
|
|
28
28
|
return async (info, init) => await _fetch(info, { ...init, agent });
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
const shouldCastToUnreachable = (err: Error): boolean =>
|
|
32
|
+
("code" in err && err.code === "ECONNREFUSED") ||
|
|
33
|
+
err.message.toLowerCase().includes("load failed");
|
|
34
|
+
|
|
35
|
+
const HTTP_STATUS_BAD_REQUEST = 400;
|
|
36
|
+
|
|
31
37
|
/**
|
|
32
38
|
* HTTPClientFactory provides a POST and GET implementation of the Unary
|
|
33
39
|
* protocol.
|
|
@@ -65,14 +71,13 @@ export class HTTPClient extends MiddlewareCollector implements UnaryClient {
|
|
|
65
71
|
req: z.input<RQ> | z.output<RQ>,
|
|
66
72
|
reqSchema: RQ,
|
|
67
73
|
resSchema: RS,
|
|
68
|
-
): Promise<[z.output<RS
|
|
74
|
+
): Promise<[z.output<RS>, null] | [null, Error]> {
|
|
69
75
|
req = reqSchema?.parse(req);
|
|
70
|
-
let res: RS | null = null;
|
|
76
|
+
let res: z.output<RS> | null = null;
|
|
71
77
|
const url = this.endpoint.child(target);
|
|
72
78
|
const request: RequestInit = {};
|
|
73
79
|
request.method = "POST";
|
|
74
80
|
request.body = this.encoder.encode(req ?? {});
|
|
75
|
-
|
|
76
81
|
const [, err] = await this.executeMiddleware(
|
|
77
82
|
{
|
|
78
83
|
target: url.toString(),
|
|
@@ -92,18 +97,19 @@ export class HTTPClient extends MiddlewareCollector implements UnaryClient {
|
|
|
92
97
|
httpRes = await f(ctx.target, request);
|
|
93
98
|
} catch (err_) {
|
|
94
99
|
let err = err_ as Error;
|
|
95
|
-
if (err
|
|
100
|
+
if (shouldCastToUnreachable(err)) err = new Unreachable({ url });
|
|
96
101
|
return [outCtx, err];
|
|
97
102
|
}
|
|
98
|
-
const data = await httpRes.arrayBuffer();
|
|
103
|
+
const data = new Uint8Array(await (await httpRes.blob()).arrayBuffer());
|
|
99
104
|
if (httpRes?.ok) {
|
|
100
|
-
if (resSchema != null) res = this.encoder.decode(data, resSchema);
|
|
105
|
+
if (resSchema != null) res = this.encoder.decode<RS>(data, resSchema);
|
|
101
106
|
return [outCtx, null];
|
|
102
107
|
}
|
|
103
108
|
try {
|
|
104
|
-
if (httpRes.status !==
|
|
105
|
-
|
|
106
|
-
const
|
|
109
|
+
if (httpRes.status !== HTTP_STATUS_BAD_REQUEST)
|
|
110
|
+
return [outCtx, new Error(httpRes.statusText)];
|
|
111
|
+
const err = this.encoder.decode(data, errors.payloadZ);
|
|
112
|
+
const decoded = errors.decode(err);
|
|
107
113
|
return [outCtx, decoded];
|
|
108
114
|
} catch (e) {
|
|
109
115
|
return [
|
|
@@ -118,6 +124,7 @@ export class HTTPClient extends MiddlewareCollector implements UnaryClient {
|
|
|
118
124
|
},
|
|
119
125
|
);
|
|
120
126
|
|
|
121
|
-
return [
|
|
127
|
+
if (err != null) return [null, err];
|
|
128
|
+
return [res, null];
|
|
122
129
|
}
|
|
123
130
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,22 +7,10 @@
|
|
|
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
|
-
export {
|
|
11
|
-
BaseTypedError,
|
|
12
|
-
decodeError,
|
|
13
|
-
encodeError,
|
|
14
|
-
EOF,
|
|
15
|
-
errorMatcher,
|
|
16
|
-
type ErrorPayload,
|
|
17
|
-
errorZ,
|
|
18
|
-
type MatchableErrorType,
|
|
19
|
-
registerError,
|
|
20
|
-
StreamClosed,
|
|
21
|
-
type TypedError,
|
|
22
|
-
Unreachable,
|
|
23
|
-
} from "@/errors";
|
|
10
|
+
export { EOF, StreamClosed, Unreachable } from "@/errors";
|
|
24
11
|
export { HTTPClient } from "@/http";
|
|
25
12
|
export { type Context, type Middleware, type Next } from "@/middleware";
|
|
26
13
|
export { type Stream, type StreamClient } from "@/stream";
|
|
27
14
|
export { sendRequired, type UnaryClient, unaryWithBreaker } from "@/unary";
|
|
28
15
|
export { WebSocketClient } from "@/websocket";
|
|
16
|
+
export { type WebsocketMessage } from "@/websocket";
|
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, type errors } from "@synnaxlabs/x";
|
|
11
11
|
import { type z } from "zod";
|
|
12
12
|
|
|
13
13
|
import { Unreachable } from "@/errors";
|
|
@@ -30,7 +30,7 @@ export interface UnaryClient extends Transport {
|
|
|
30
30
|
req: z.input<RQ> | z.output<RQ>,
|
|
31
31
|
reqSchema: RQ,
|
|
32
32
|
resSchema: RS,
|
|
33
|
-
) => Promise<
|
|
33
|
+
) => Promise<errors.Return<z.output<RS>>>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export const unaryWithBreaker = (
|
|
@@ -53,12 +53,13 @@ export const unaryWithBreaker = (
|
|
|
53
53
|
req: z.input<RQ> | z.output<RQ>,
|
|
54
54
|
reqSchema: RQ,
|
|
55
55
|
resSchema: RS,
|
|
56
|
-
): Promise<
|
|
57
|
-
const brk = breaker.
|
|
56
|
+
): Promise<errors.Return<z.output<RS>>> {
|
|
57
|
+
const brk = new breaker.Breaker(cfg);
|
|
58
58
|
do {
|
|
59
59
|
const [res, err] = await this.wrapped.send(target, req, reqSchema, resSchema);
|
|
60
|
-
if (err == null
|
|
61
|
-
if (!(
|
|
60
|
+
if (err == null) return [res, null];
|
|
61
|
+
if (!Unreachable.matches(err)) return [null, err];
|
|
62
|
+
if (!(await brk.wait())) return [res, err];
|
|
62
63
|
} while (true);
|
|
63
64
|
}
|
|
64
65
|
}
|