@synnaxlabs/x 0.56.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/.turbo/turbo-build.log +4 -4
- package/dist/src/deep/merge.d.ts.map +1 -1
- package/dist/src/deep/set.d.ts.map +1 -1
- package/dist/src/errors/errors.d.ts +12 -0
- package/dist/src/errors/errors.d.ts.map +1 -1
- package/dist/src/migrate/migrate.d.ts.map +1 -1
- package/dist/src/status/status.d.ts.map +1 -1
- package/dist/x.cjs +10 -10
- package/dist/x.js +1099 -1090
- package/package.json +3 -3
- package/src/deep/merge.ts +2 -1
- package/src/deep/set.ts +2 -1
- package/src/errors/errors.spec.ts +92 -0
- package/src/errors/errors.ts +22 -0
- package/src/migrate/migrate.ts +2 -1
- package/src/status/status.spec.ts +17 -2
- package/src/status/status.ts +15 -9
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/x",
|
|
3
|
-
"version": "0.56.
|
|
3
|
+
"version": "0.56.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Common Utilities for Synnax Labs",
|
|
6
6
|
"repository": "https://github.com/synnaxlabs/synnax/tree/main/x/ts",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"vite": "^8.0.13",
|
|
28
28
|
"vitest": "^4.1.6",
|
|
29
|
-
"@synnaxlabs/
|
|
29
|
+
"@synnaxlabs/tsconfig": "^0.0.0",
|
|
30
30
|
"@synnaxlabs/vite-plugin": "^0.0.0",
|
|
31
|
-
"@synnaxlabs/
|
|
31
|
+
"@synnaxlabs/eslint-config": "^0.0.0"
|
|
32
32
|
},
|
|
33
33
|
"main": "dist/x.cjs",
|
|
34
34
|
"module": "dist/x.js",
|
package/src/deep/merge.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { type z } from "zod";
|
|
11
11
|
|
|
12
12
|
import { type Partial } from "@/deep/partial";
|
|
13
|
+
import { errors } from "@/errors";
|
|
13
14
|
import { narrow } from "@/narrow";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -32,7 +33,7 @@ export const override = <T>(base: T, ...overrides: Array<Partial<T>>): T => {
|
|
|
32
33
|
} catch (e) {
|
|
33
34
|
if (e instanceof TypeError)
|
|
34
35
|
throw new TypeError(`.${key}: ${e.message}`, { cause: e });
|
|
35
|
-
throw e;
|
|
36
|
+
throw errors.fromUnknown(e);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
return override(base, ...overrides);
|
package/src/deep/set.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// included in the file licenses/APL.txt.
|
|
9
9
|
|
|
10
10
|
import { defaultGetter, findBestKey, getIndex, SEPARATOR } from "@/deep/path";
|
|
11
|
+
import { errors } from "@/errors";
|
|
11
12
|
import { type record } from "@/record";
|
|
12
13
|
|
|
13
14
|
export const set = <V>(obj: V, path: string, value: unknown): void => {
|
|
@@ -86,6 +87,6 @@ export const set = <V>(obj: V, path: string, value: unknown): void => {
|
|
|
86
87
|
result[lastPart] = value;
|
|
87
88
|
} catch (e) {
|
|
88
89
|
console.error("failed to set value", value, "at path", path, "on object", obj);
|
|
89
|
-
throw e;
|
|
90
|
+
throw errors.fromUnknown(e);
|
|
90
91
|
}
|
|
91
92
|
};
|
|
@@ -128,6 +128,98 @@ describe("errors", () => {
|
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
describe("fromUnknown", () => {
|
|
132
|
+
it("should return the same Error instance when given an Error", () => {
|
|
133
|
+
const original = new Error("already an error");
|
|
134
|
+
const result = errors.fromUnknown(original);
|
|
135
|
+
expect(result).toBe(original);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should preserve typed-error subclasses when given one", () => {
|
|
139
|
+
const original = new ErrorOne("typed");
|
|
140
|
+
const result = errors.fromUnknown(original);
|
|
141
|
+
expect(result).toBe(original);
|
|
142
|
+
expect(ErrorOne.matches(result)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should preserve other Error subclasses like TypeError", () => {
|
|
146
|
+
const original = new TypeError("bad type");
|
|
147
|
+
const result = errors.fromUnknown(original);
|
|
148
|
+
expect(result).toBe(original);
|
|
149
|
+
expect(result).toBeInstanceOf(TypeError);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should wrap a string with the string as the message", () => {
|
|
153
|
+
const result = errors.fromUnknown("plain string");
|
|
154
|
+
expect(result).toBeInstanceOf(Error);
|
|
155
|
+
expect(result.message).toEqual('"plain string"');
|
|
156
|
+
expect(result.cause).toEqual("plain string");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should wrap a number using JSON.stringify for the message", () => {
|
|
160
|
+
const result = errors.fromUnknown(42);
|
|
161
|
+
expect(result).toBeInstanceOf(Error);
|
|
162
|
+
expect(result.message).toEqual("42");
|
|
163
|
+
expect(result.cause).toEqual(42);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should wrap an object using JSON.stringify for the message", () => {
|
|
167
|
+
const value = { foo: "bar", n: 1 };
|
|
168
|
+
const result = errors.fromUnknown(value);
|
|
169
|
+
expect(result).toBeInstanceOf(Error);
|
|
170
|
+
expect(result.message).toEqual('{"foo":"bar","n":1}');
|
|
171
|
+
expect(result.cause).toBe(value);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should fall back to String() when JSON.stringify throws on a circular ref", () => {
|
|
175
|
+
const value: Record<string, unknown> = { name: "loop" };
|
|
176
|
+
value.self = value;
|
|
177
|
+
const result = errors.fromUnknown(value);
|
|
178
|
+
expect(result).toBeInstanceOf(Error);
|
|
179
|
+
// Plain objects without a custom toString fall through to "[object Object]".
|
|
180
|
+
expect(result.message).toEqual("[object Object]");
|
|
181
|
+
expect(result.cause).toBe(value);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should fall back to String() for a BigInt", () => {
|
|
185
|
+
const value = 9007199254740993n;
|
|
186
|
+
const result = errors.fromUnknown(value);
|
|
187
|
+
expect(result).toBeInstanceOf(Error);
|
|
188
|
+
expect(result.message).toEqual(String(value));
|
|
189
|
+
expect(result.cause).toBe(value);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should wrap null with a stringified message", () => {
|
|
193
|
+
const result = errors.fromUnknown(null);
|
|
194
|
+
expect(result).toBeInstanceOf(Error);
|
|
195
|
+
expect(result.message).toEqual("null");
|
|
196
|
+
expect(result.cause).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should fall back to String() for undefined (JSON.stringify returns undefined)", () => {
|
|
200
|
+
const result = errors.fromUnknown(undefined);
|
|
201
|
+
expect(result).toBeInstanceOf(Error);
|
|
202
|
+
expect(result.message).toEqual("undefined");
|
|
203
|
+
expect(result.cause).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should fall back to String() for a function (JSON.stringify returns undefined)", () => {
|
|
207
|
+
const value = function named() {};
|
|
208
|
+
const result = errors.fromUnknown(value);
|
|
209
|
+
expect(result).toBeInstanceOf(Error);
|
|
210
|
+
expect(result.message).toEqual(String(value));
|
|
211
|
+
expect(result.cause).toBe(value);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should fall back to String() for a symbol (JSON.stringify returns undefined)", () => {
|
|
215
|
+
const value = Symbol("sym");
|
|
216
|
+
const result = errors.fromUnknown(value);
|
|
217
|
+
expect(result).toBeInstanceOf(Error);
|
|
218
|
+
expect(result.message).toEqual("Symbol(sym)");
|
|
219
|
+
expect(result.cause).toBe(value);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
131
223
|
describe("matches", () => {
|
|
132
224
|
it("should return true if the errors are exactly the same", () => {
|
|
133
225
|
const v = new ErrorOne("test");
|
package/src/errors/errors.ts
CHANGED
|
@@ -142,6 +142,28 @@ export const isTyped = (error: unknown): error is Typed => {
|
|
|
142
142
|
return true;
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Coerces an arbitrary thrown value into an `Error` so it can be re-thrown without
|
|
147
|
+
* tripping `@typescript-eslint/only-throw-error` and so callers can rely on a uniform
|
|
148
|
+
* `Error` shape. The original value is preserved on `Error.cause` for stack-trace
|
|
149
|
+
* continuity.
|
|
150
|
+
*
|
|
151
|
+
* - If `value` is already an `Error`, it is returned unchanged.
|
|
152
|
+
* - Otherwise the message is derived from `JSON.stringify(value)` when possible (which
|
|
153
|
+
* carries more detail for plain objects), falling back to `String(value)` for
|
|
154
|
+
* circular structures, BigInts, or anything else that fails to serialize.
|
|
155
|
+
*/
|
|
156
|
+
export const fromUnknown = (value: unknown): Error => {
|
|
157
|
+
if (value instanceof Error) return value;
|
|
158
|
+
let message: string;
|
|
159
|
+
try {
|
|
160
|
+
message = JSON.stringify(value) ?? String(value);
|
|
161
|
+
} catch {
|
|
162
|
+
message = String(value);
|
|
163
|
+
}
|
|
164
|
+
return new Error(message, { cause: value });
|
|
165
|
+
};
|
|
166
|
+
|
|
145
167
|
/** Constant representing an unknown error type */
|
|
146
168
|
export const UNKNOWN = "unknown";
|
|
147
169
|
|
package/src/migrate/migrate.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
12
|
import { compare } from "@/compare";
|
|
13
|
+
import { errors } from "@/errors";
|
|
13
14
|
import { type optional } from "@/optional";
|
|
14
15
|
|
|
15
16
|
export const semVerZ = z
|
|
@@ -203,7 +204,7 @@ export const createMigration =
|
|
|
203
204
|
} catch (e) {
|
|
204
205
|
console.log(`${name} failed to migrate from ${input.version}`);
|
|
205
206
|
console.error(e);
|
|
206
|
-
throw e;
|
|
207
|
+
throw errors.fromUnknown(e);
|
|
207
208
|
}
|
|
208
209
|
};
|
|
209
210
|
|
|
@@ -110,9 +110,24 @@ describe("status", () => {
|
|
|
110
110
|
expect(s.details.error).toBe(error);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
it("should
|
|
113
|
+
it("should coerce a non-Error throwable into an error status via fromUnknown", () => {
|
|
114
114
|
const notAnError = "just a string";
|
|
115
|
-
|
|
115
|
+
const s = status.fromException(notAnError);
|
|
116
|
+
expect(s.variant).toEqual("error");
|
|
117
|
+
// JSON.stringify of a string quotes it.
|
|
118
|
+
expect(s.message).toEqual('"just a string"');
|
|
119
|
+
expect(s.details.error).toBeInstanceOf(Error);
|
|
120
|
+
expect(s.details.error.cause).toEqual("just a string");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should pick up custom toStatus contributions from non-Error throwables", () => {
|
|
124
|
+
const custom = {
|
|
125
|
+
toStatus: () => ({ message: "custom msg", description: "custom desc" }),
|
|
126
|
+
};
|
|
127
|
+
const s = status.fromException(custom);
|
|
128
|
+
expect(s.variant).toEqual("error");
|
|
129
|
+
expect(s.message).toEqual("custom msg");
|
|
130
|
+
expect(s.description).toEqual("custom desc");
|
|
116
131
|
});
|
|
117
132
|
|
|
118
133
|
it("should include valid key and timestamp", () => {
|
package/src/status/status.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
|
+
import { errors } from "@/errors";
|
|
12
13
|
import { id } from "@/id";
|
|
13
14
|
import { narrow } from "@/narrow";
|
|
14
15
|
import { type optional } from "@/optional";
|
|
@@ -52,11 +53,14 @@ const customReturnZ = z.object({
|
|
|
52
53
|
details: record.unknownZ().optional(),
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const safeToStatus = (exc: unknown): z.infer<typeof customReturnZ> | undefined => {
|
|
57
|
+
if (
|
|
58
|
+
exc == null ||
|
|
59
|
+
typeof exc !== "object" ||
|
|
60
|
+
!("toStatus" in exc) ||
|
|
61
|
+
typeof exc.toStatus !== "function"
|
|
62
|
+
)
|
|
63
|
+
return undefined;
|
|
60
64
|
let raw: unknown;
|
|
61
65
|
try {
|
|
62
66
|
raw = exc.toStatus();
|
|
@@ -78,13 +82,15 @@ export const fromException = (
|
|
|
78
82
|
exc: unknown,
|
|
79
83
|
message?: string,
|
|
80
84
|
): Status<typeof exceptionDetailsSchema, z.ZodLiteral<"error">> => {
|
|
81
|
-
|
|
85
|
+
const err = errors.fromUnknown(exc);
|
|
82
86
|
const crude: Crude<typeof exceptionDetailsSchema, "error"> = {
|
|
83
87
|
variant: "error",
|
|
84
|
-
message: message ??
|
|
85
|
-
description: message != null ?
|
|
86
|
-
details: { stack:
|
|
88
|
+
message: message ?? err.message,
|
|
89
|
+
description: message != null ? err.message : undefined,
|
|
90
|
+
details: { stack: err.stack ?? "", error: err },
|
|
87
91
|
};
|
|
92
|
+
// Probe the original (pre-coercion) value so a non-Error throwable with a custom
|
|
93
|
+
// `toStatus()` method still contributes its status fields.
|
|
88
94
|
const custom = safeToStatus(exc);
|
|
89
95
|
if (custom != null) {
|
|
90
96
|
if (message != null && custom.message != null)
|