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",
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.32.35"
"valibot": "^0.36.0"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
@ -36,11 +36,6 @@
"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": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@tsconfig/strictest/-/strictest-2.0.5.tgz",
@ -59,6 +54,11 @@
"engines": {
"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"
},
"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 = {
path: string
expected: string
received: string
message: string
path: string
}
/**
@ -25,27 +27,11 @@ export type CodexError = {
errors: ValidationError[]
}
export const CodexValidationErrors = {
map(iterator: ValueErrorIterator) {
let error
const errors = []
export const CodexValibotIssuesMap = (issues: InferIssue<any>[]) => issues.map(i => ({
expected: i.expected,
received: i.received,
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",
errors: [
{
path: "/" + field,
message: "Expected required property"
path: field,
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,
message: "Expected number"
}
path: field,
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",
errors: [
{
path: "/" + field,
message: "Expected required property"
},
{
path: "/" + field,
message: "Expected string"
path: field,
expected: 'string',
message: 'Invalid type: Expected string but received undefined',
received: 'undefined'
}
]
}
}
}
function mistypeNumberValidationError(field: string) {
function mistypeNumberValidationError(field: string, value: string) {
return {
error: true,
data: {
@ -96,8 +110,10 @@ function mistypeNumberValidationError(field: string) {
message: "Cannot validate the input",
errors: [
{
path: "/" + field,
message: "Expected number"
path: field,
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",
errors: [
{
path: "/" + field,
message: "Expected number to be greater or equal to " + min
path: field,
expected: '>=' + min,
message: 'Invalid value: Expected >=1 but received 0',
received: '0'
}
]
}
@ -143,7 +161,7 @@ describe("marketplace", () => {
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({
duration: 3000,
maxCollateral: 1,
@ -151,7 +169,7 @@ describe("marketplace", () => {
totalSize: "abc"
} 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 () => {
@ -172,6 +190,8 @@ describe("marketplace", () => {
minPrice: 100,
} as any)
console.info(response.error)
assert.deepStrictEqual(response, missingNumberValidationError("duration"));
});
@ -206,6 +226,18 @@ describe("marketplace", () => {
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) => {
const data = { ...createAvailability(), freeSize: 1000 }
@ -218,8 +250,7 @@ describe("marketplace", () => {
totalSize: 3000,
minPrice: 100,
duration: 100,
hello: "world"
} as any)
})
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 { CodexValidationErrors } from '../errors/errors'
import { CodexValibotIssuesMap } from '../errors/errors'
import { Fetch } from "../fetch-safe/fetch-safe"
import { SafeValue } from "../values/values"
import { CodexAvailability, CodexAvailabilityCreateResponse, CodexCreateAvailabilityInput, CodexCreateStorageRequestInput, CodexCreateStorageRequestResponse, CodexPurchase, CodexReservation, CodexSlot, CodexUpdateAvailabilityInput } from "./types"
@ -50,17 +50,15 @@ export class Marketplace {
*/
async createAvailability(input: CodexCreateAvailabilityInput)
: Promise<SafeValue<CodexAvailabilityCreateResponse>> {
const cleaned = Value.Clean(CodexCreateAvailabilityInput, input)
if (!Value.Check(CodexCreateAvailabilityInput, cleaned)) {
const iterator = Value.Errors(CodexCreateAvailabilityInput, cleaned)
const result = v.safeParse(CodexCreateAvailabilityInput, input)
if (!result.success) {
return {
error: true,
data: {
type: "validation",
message: "Cannot validate the input",
errors: CodexValidationErrors.map(iterator)
errors: CodexValibotIssuesMap(result.issues)
}
}
}
@ -72,7 +70,7 @@ export class Marketplace {
headers: {
"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.
*/
async updateAvailability(input: CodexUpdateAvailabilityInput): Promise<SafeValue<CodexAvailability>> {
const cleaned = Value.Clean(CodexUpdateAvailabilityInput, input)
if (!Value.Check(CodexUpdateAvailabilityInput, cleaned)) {
const iterator = Value.Errors(CodexUpdateAvailabilityInput, cleaned)
const result = v.safeParse(CodexUpdateAvailabilityInput, input)
if (!result.success) {
return {
error: true,
data: {
type: "validation",
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, {
method: "POST",
headers: {
"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.
*/
async createStorageRequest(input: CodexCreateStorageRequestInput): Promise<SafeValue<CodexCreateStorageRequestResponse>> {
const cleaned = Value.Clean(CodexCreateStorageRequestInput, input)
if (!Value.Check(CodexCreateStorageRequestInput, cleaned)) {
const iterator = Value.Errors(CodexCreateStorageRequestInput, cleaned)
const result = v.safeParse(CodexCreateStorageRequestInput, input)
if (!result.success) {
return {
error: true,
data: {
type: "validation",
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
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 = {
id: string
@ -130,24 +130,24 @@ export type CodexAvailabilityCreateResponse = CodexAvailability & {
freeSize: string
}
export const CodexCreateAvailabilityInput = Type.Object({
totalSize: Type.Number({ minimum: 1 }),
duration: Type.Number({ minimum: 1 }),
minPrice: Type.Number(),
maxCollateral: Type.Number()
export const CodexCreateAvailabilityInput = v.strictObject({
totalSize: v.pipe(v.number(), v.minValue(1)),
duration: v.pipe(v.number(), v.minValue(1)),
minPrice: v.number(),
maxCollateral: v.number(),
})
export type CodexCreateAvailabilityInput = Static<typeof CodexCreateAvailabilityInput>
export type CodexCreateAvailabilityInput = v.InferOutput<typeof CodexCreateAvailabilityInput>;
export const CodexUpdateAvailabilityInput = Type.Object({
id: Type.String(),
totalSize: Type.Optional(Type.Number({ minimum: 1 })),
duration: Type.Optional(Type.Number({ minimum: 1 })),
minPrice: Type.Optional(Type.Number()),
maxCollateral: Type.Optional(Type.Number())
export const CodexUpdateAvailabilityInput = v.strictObject({
id: v.string(),
totalSize: v.optional(v.pipe(v.number(), v.minValue(1))),
duration: v.optional(v.pipe(v.number(), v.minValue(1))),
minPrice: v.optional(v.number()),
maxCollateral: v.optional(v.number()),
})
export type CodexUpdateAvailabilityInput = Static<typeof CodexUpdateAvailabilityInput>
export type CodexUpdateAvailabilityInput = v.InferOutput<typeof CodexUpdateAvailabilityInput>;
export type CodexReservation = {
id: string
@ -179,17 +179,17 @@ export type CodexPurchase = {
request: CodexStorageRequest
}
export const CodexCreateStorageRequestInput = Type.Object({
cid: Type.String(),
duration: Type.Number({ minimum: 1 }),
reward: Type.Number(),
proofProbability: Type.Number(),
nodes: Type.Optional(Type.Number({ default: 1 })),
tolerance: Type.Optional(Type.Number({ default: 0 })),
expiry: Type.Number({ minimum: 1 }),
collateral: Type.Number()
export const CodexCreateStorageRequestInput = v.strictObject({
cid: v.string(),
duration: v.pipe(v.number(), v.minValue(1)),
reward: v.number(),
proofProbability: v.number(),
nodes: v.optional(v.number(), 1),
tolerance: v.optional(v.number(), 0),
expiry: v.pipe(v.number(), v.minValue(1)),
collateral: v.number(),
})
export type CodexCreateStorageRequestInput = Static<typeof CodexCreateStorageRequestInput>
export type CodexCreateStorageRequestInput = v.InferOutput<typeof CodexCreateStorageRequestInput>;
export type CodexCreateStorageRequestResponse = Omit<CodexCreateStorageRequestInput, "cid">

View File

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