Drastically reduce the bundle size by replacing Typebox with Valibot.

This commit is contained in:
Arnaud 2024-08-15 12:08:38 +02:00
parent aa831228ea
commit 7c40e4af5f
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
7 changed files with 112 additions and 98 deletions

12
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sinclair/typebox": "^0.32.35" "valibot": "^0.36.0"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
@ -36,11 +36,6 @@
"npm": ">=6.14.13" "npm": ">=6.14.13"
} }
}, },
"node_modules/@sinclair/typebox": {
"version": "0.32.35",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.32.35.tgz",
"integrity": "sha512-Ul3YyOTU++to8cgNkttakC0dWvpERr6RYoHO2W47DLbFvrwBDJUY31B1sImH6JZSYc4Kt4PyHtoPNu+vL2r2dA=="
},
"node_modules/@tsconfig/strictest": { "node_modules/@tsconfig/strictest": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@tsconfig/strictest/-/strictest-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/strictest/-/strictest-2.0.5.tgz",
@ -59,6 +54,11 @@
"engines": { "engines": {
"node": ">=14.17" "node": ">=14.17"
} }
},
"node_modules/valibot": {
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-0.36.0.tgz",
"integrity": "sha512-CjF1XN4sUce8sBK9TixrDqFM7RwNkuXdJu174/AwmQUB62QbCQADg5lLe8ldBalFgtj1uKj+pKwDJiNo4Mn+eQ=="
} }
} }
} }

View File

@ -37,6 +37,6 @@
"typescript": "^5.5.4" "typescript": "^5.5.4"
}, },
"dependencies": { "dependencies": {
"@sinclair/typebox": "^0.32.35" "valibot": "^0.36.0"
} }
} }

View File

@ -1,8 +1,10 @@
import { ValueErrorIterator } from "@sinclair/typebox/build/cjs/errors" import { type InferIssue } from "valibot"
type ValidationError = { type ValidationError = {
path: string expected: string
received: string
message: string message: string
path: string
} }
/** /**
@ -25,27 +27,11 @@ export type CodexError = {
errors: ValidationError[] errors: ValidationError[]
} }
export const CodexValidationErrors = { export const CodexValibotIssuesMap = (issues: InferIssue<any>[]) => issues.map(i => ({
map(iterator: ValueErrorIterator) { expected: i.expected,
let error received: i.received,
const errors = [] message: i.message,
path: i.path.map((item: { key: string }) => item.key).join('.')
}))
while (error = iterator.First()) {
errors.push({
path: error.path,
message: error.message
})
}
return errors
}
}
// export class CodexError extends Error {
// readonly status: number | null
// constructor(message: string, status: number | null = null) {
// super(message)
// this.status = status
// }
// }

View File

@ -56,13 +56,29 @@ function missingNumberValidationError(field: string) {
message: "Cannot validate the input", message: "Cannot validate the input",
errors: [ errors: [
{ {
path: "/" + field, path: field,
message: "Expected required property" expected: 'number',
message: 'Invalid type: Expected number but received undefined',
received: 'undefined'
}, },
]
}
}
}
function extraValidationError(field: string, value: unknown) {
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: [
{ {
path: "/" + field, path: field,
message: "Expected number" expected: 'never',
} message: `Invalid type: Expected never but received "${value}"`,
received: `"${value}"`
},
] ]
} }
} }
@ -76,19 +92,17 @@ function missingStringValidationError(field: string) {
message: "Cannot validate the input", message: "Cannot validate the input",
errors: [ errors: [
{ {
path: "/" + field, path: field,
message: "Expected required property" expected: 'string',
}, message: 'Invalid type: Expected string but received undefined',
{ received: 'undefined'
path: "/" + field,
message: "Expected string"
} }
] ]
} }
} }
} }
function mistypeNumberValidationError(field: string) { function mistypeNumberValidationError(field: string, value: string) {
return { return {
error: true, error: true,
data: { data: {
@ -96,8 +110,10 @@ function mistypeNumberValidationError(field: string) {
message: "Cannot validate the input", message: "Cannot validate the input",
errors: [ errors: [
{ {
path: "/" + field, path: field,
message: "Expected number" expected: 'number',
message: `Invalid type: Expected number but received "${value}"`,
received: `"${value}"`
}, },
] ]
} }
@ -112,8 +128,10 @@ function minNumberValidationError(field: string, min: number) {
message: "Cannot validate the input", message: "Cannot validate the input",
errors: [ errors: [
{ {
path: "/" + field, path: field,
message: "Expected number to be greater or equal to " + min expected: '>=' + min,
message: 'Invalid value: Expected >=1 but received 0',
received: '0'
} }
] ]
} }
@ -143,7 +161,7 @@ describe("marketplace", () => {
assert.deepStrictEqual(response, missingNumberValidationError("totalSize")); assert.deepStrictEqual(response, missingNumberValidationError("totalSize"));
}); });
it("returns an error when trying to create an availability with an invalid number valid", async () => { it.only("returns an error when trying to create an availability with an invalid number valid", async () => {
const response = await marketplace.createAvailability({ const response = await marketplace.createAvailability({
duration: 3000, duration: 3000,
maxCollateral: 1, maxCollateral: 1,
@ -151,7 +169,7 @@ describe("marketplace", () => {
totalSize: "abc" totalSize: "abc"
} as any) } as any)
assert.deepStrictEqual(response, mistypeNumberValidationError("totalSize")); assert.deepStrictEqual(response, mistypeNumberValidationError("totalSize", "abc"));
}); });
it("returns an error when trying to create an availability with zero total size", async () => { it("returns an error when trying to create an availability with zero total size", async () => {
@ -172,6 +190,8 @@ describe("marketplace", () => {
minPrice: 100, minPrice: 100,
} as any) } as any)
console.info(response.error)
assert.deepStrictEqual(response, missingNumberValidationError("duration")); assert.deepStrictEqual(response, missingNumberValidationError("duration"));
}); });
@ -206,6 +226,18 @@ describe("marketplace", () => {
assert.deepStrictEqual(response, missingNumberValidationError("maxCollateral")); assert.deepStrictEqual(response, missingNumberValidationError("maxCollateral"));
}); });
it("returns an error when trying to create an availability with an extra field", async () => {
const response = await marketplace.createAvailability({
maxCollateral: 1,
totalSize: 3000,
minPrice: 100,
duration: 100,
hello: "world"
} as any)
assert.deepStrictEqual(response, extraValidationError("hello", "world"));
});
it("returns a response when the request succeed", async (t) => { it("returns a response when the request succeed", async (t) => {
const data = { ...createAvailability(), freeSize: 1000 } const data = { ...createAvailability(), freeSize: 1000 }
@ -218,8 +250,7 @@ describe("marketplace", () => {
totalSize: 3000, totalSize: 3000,
minPrice: 100, minPrice: 100,
duration: 100, duration: 100,
hello: "world" })
} as any)
assert.deepStrictEqual(response, { error: false, data }); assert.deepStrictEqual(response, { error: false, data });
}); });

View File

@ -1,6 +1,6 @@
import { Value } from '@sinclair/typebox/value' import * as v from 'valibot'
import { Api } from "../api/config" import { Api } from "../api/config"
import { CodexValidationErrors } from '../errors/errors' import { CodexValibotIssuesMap } from '../errors/errors'
import { Fetch } from "../fetch-safe/fetch-safe" import { Fetch } from "../fetch-safe/fetch-safe"
import { SafeValue } from "../values/values" import { SafeValue } from "../values/values"
import { CodexAvailability, CodexAvailabilityCreateResponse, CodexCreateAvailabilityInput, CodexCreateStorageRequestInput, CodexCreateStorageRequestResponse, CodexPurchase, CodexReservation, CodexSlot, CodexUpdateAvailabilityInput } from "./types" import { CodexAvailability, CodexAvailabilityCreateResponse, CodexCreateAvailabilityInput, CodexCreateStorageRequestInput, CodexCreateStorageRequestResponse, CodexPurchase, CodexReservation, CodexSlot, CodexUpdateAvailabilityInput } from "./types"
@ -50,17 +50,15 @@ export class Marketplace {
*/ */
async createAvailability(input: CodexCreateAvailabilityInput) async createAvailability(input: CodexCreateAvailabilityInput)
: Promise<SafeValue<CodexAvailabilityCreateResponse>> { : Promise<SafeValue<CodexAvailabilityCreateResponse>> {
const cleaned = Value.Clean(CodexCreateAvailabilityInput, input) const result = v.safeParse(CodexCreateAvailabilityInput, input)
if (!Value.Check(CodexCreateAvailabilityInput, cleaned)) {
const iterator = Value.Errors(CodexCreateAvailabilityInput, cleaned)
if (!result.success) {
return { return {
error: true, error: true,
data: { data: {
type: "validation", type: "validation",
message: "Cannot validate the input", message: "Cannot validate the input",
errors: CodexValidationErrors.map(iterator) errors: CodexValibotIssuesMap(result.issues)
} }
} }
} }
@ -72,7 +70,7 @@ export class Marketplace {
headers: { headers: {
"content-type": "application/json" "content-type": "application/json"
}, },
body: JSON.stringify(cleaned) body: JSON.stringify(result.output)
}) })
} }
@ -81,29 +79,27 @@ export class Marketplace {
* Existing Requests linked to this Availability will continue as is. * Existing Requests linked to this Availability will continue as is.
*/ */
async updateAvailability(input: CodexUpdateAvailabilityInput): Promise<SafeValue<CodexAvailability>> { async updateAvailability(input: CodexUpdateAvailabilityInput): Promise<SafeValue<CodexAvailability>> {
const cleaned = Value.Clean(CodexUpdateAvailabilityInput, input) const result = v.safeParse(CodexUpdateAvailabilityInput, input)
if (!Value.Check(CodexUpdateAvailabilityInput, cleaned)) {
const iterator = Value.Errors(CodexUpdateAvailabilityInput, cleaned)
if (!result.success) {
return { return {
error: true, error: true,
data: { data: {
type: "validation", type: "validation",
message: "Cannot validate the input", message: "Cannot validate the input",
errors: CodexValidationErrors.map(iterator) errors: CodexValibotIssuesMap(result.issues)
} }
} }
} }
const url = this.url + Api.config.prefix + "/sales/availability/" + cleaned.id const url = this.url + Api.config.prefix + "/sales/availability/" + result.output.id
return Fetch.safe<CodexAvailability>(url, { return Fetch.safe<CodexAvailability>(url, {
method: "POST", method: "POST",
headers: { headers: {
"content-type": "application/json" "content-type": "application/json"
}, },
body: JSON.stringify(cleaned) body: JSON.stringify(result.output)
}) })
} }
@ -144,22 +140,20 @@ export class Marketplace {
* Creates a new request for storage. * Creates a new request for storage.
*/ */
async createStorageRequest(input: CodexCreateStorageRequestInput): Promise<SafeValue<CodexCreateStorageRequestResponse>> { async createStorageRequest(input: CodexCreateStorageRequestInput): Promise<SafeValue<CodexCreateStorageRequestResponse>> {
const cleaned = Value.Clean(CodexCreateStorageRequestInput, input) const result = v.safeParse(CodexCreateStorageRequestInput, input)
if (!Value.Check(CodexCreateStorageRequestInput, cleaned)) {
const iterator = Value.Errors(CodexCreateStorageRequestInput, cleaned)
if (!result.success) {
return { return {
error: true, error: true,
data: { data: {
type: "validation", type: "validation",
message: "Cannot validate the input", message: "Cannot validate the input",
errors: CodexValidationErrors.map(iterator) errors: CodexValibotIssuesMap(result.issues)
} }
} }
} }
const { cid, ...body } = cleaned const { cid, ...body } = result.output
const url = this.url + Api.config.prefix + "/storage/request/" + cid const url = this.url + Api.config.prefix + "/storage/request/" + cid
return Fetch.safe<CodexCreateStorageRequestResponse>(url, { return Fetch.safe<CodexCreateStorageRequestResponse>(url, {

View File

@ -1,4 +1,4 @@
import { Static, Type } from "@sinclair/typebox" import * as v from 'valibot';
export type CodexStorageRequest = { export type CodexStorageRequest = {
id: string id: string
@ -130,24 +130,24 @@ export type CodexAvailabilityCreateResponse = CodexAvailability & {
freeSize: string freeSize: string
} }
export const CodexCreateAvailabilityInput = Type.Object({ export const CodexCreateAvailabilityInput = v.strictObject({
totalSize: Type.Number({ minimum: 1 }), totalSize: v.pipe(v.number(), v.minValue(1)),
duration: Type.Number({ minimum: 1 }), duration: v.pipe(v.number(), v.minValue(1)),
minPrice: Type.Number(), minPrice: v.number(),
maxCollateral: Type.Number() maxCollateral: v.number(),
}) })
export type CodexCreateAvailabilityInput = Static<typeof CodexCreateAvailabilityInput> export type CodexCreateAvailabilityInput = v.InferOutput<typeof CodexCreateAvailabilityInput>;
export const CodexUpdateAvailabilityInput = Type.Object({ export const CodexUpdateAvailabilityInput = v.strictObject({
id: Type.String(), id: v.string(),
totalSize: Type.Optional(Type.Number({ minimum: 1 })), totalSize: v.optional(v.pipe(v.number(), v.minValue(1))),
duration: Type.Optional(Type.Number({ minimum: 1 })), duration: v.optional(v.pipe(v.number(), v.minValue(1))),
minPrice: Type.Optional(Type.Number()), minPrice: v.optional(v.number()),
maxCollateral: Type.Optional(Type.Number()) maxCollateral: v.optional(v.number()),
}) })
export type CodexUpdateAvailabilityInput = Static<typeof CodexUpdateAvailabilityInput> export type CodexUpdateAvailabilityInput = v.InferOutput<typeof CodexUpdateAvailabilityInput>;
export type CodexReservation = { export type CodexReservation = {
id: string id: string
@ -179,17 +179,17 @@ export type CodexPurchase = {
request: CodexStorageRequest request: CodexStorageRequest
} }
export const CodexCreateStorageRequestInput = Type.Object({ export const CodexCreateStorageRequestInput = v.strictObject({
cid: Type.String(), cid: v.string(),
duration: Type.Number({ minimum: 1 }), duration: v.pipe(v.number(), v.minValue(1)),
reward: Type.Number(), reward: v.number(),
proofProbability: Type.Number(), proofProbability: v.number(),
nodes: Type.Optional(Type.Number({ default: 1 })), nodes: v.optional(v.number(), 1),
tolerance: Type.Optional(Type.Number({ default: 0 })), tolerance: v.optional(v.number(), 0),
expiry: Type.Number({ minimum: 1 }), expiry: v.pipe(v.number(), v.minValue(1)),
collateral: Type.Number() collateral: v.number(),
}) })
export type CodexCreateStorageRequestInput = Static<typeof CodexCreateStorageRequestInput> export type CodexCreateStorageRequestInput = v.InferOutput<typeof CodexCreateStorageRequestInput>;
export type CodexCreateStorageRequestResponse = Omit<CodexCreateStorageRequestInput, "cid"> export type CodexCreateStorageRequestResponse = Omit<CodexCreateStorageRequestInput, "cid">

View File

@ -11,5 +11,8 @@
"DOM", "DOM",
], ],
"outDir": "./dist", "outDir": "./dist",
} // "module": "ES2015",
// "moduleResolution": "Bundler"
},
"buildOptions": {}
} }