From 7c40e4af5f55183c1a05f44e7b3e84a14b2e8584 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 15 Aug 2024 12:08:38 +0200 Subject: [PATCH] Drastically reduce the bundle size by replacing Typebox with Valibot. --- package-lock.json | 12 ++--- package.json | 4 +- src/errors/errors.ts | 34 ++++---------- src/marketplace/marketplace.test.ts | 71 +++++++++++++++++++++-------- src/marketplace/marketplace.ts | 36 ++++++--------- src/marketplace/types.ts | 48 +++++++++---------- tsconfig.json | 5 +- 7 files changed, 112 insertions(+), 98 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10c142b..abe8765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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==" } } } diff --git a/package.json b/package.json index 41860ca..cf4f085 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,6 @@ "typescript": "^5.5.4" }, "dependencies": { - "@sinclair/typebox": "^0.32.35" + "valibot": "^0.36.0" } -} \ No newline at end of file +} diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 86615be..901ff73 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -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[]) => 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 -// } -// } diff --git a/src/marketplace/marketplace.test.ts b/src/marketplace/marketplace.test.ts index 37d90bb..0af2438 100644 --- a/src/marketplace/marketplace.test.ts +++ b/src/marketplace/marketplace.test.ts @@ -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 }); }); diff --git a/src/marketplace/marketplace.ts b/src/marketplace/marketplace.ts index 1c98932..0160823 100644 --- a/src/marketplace/marketplace.ts +++ b/src/marketplace/marketplace.ts @@ -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> { - 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> { - 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(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> { - 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(url, { diff --git a/src/marketplace/types.ts b/src/marketplace/types.ts index a9a9957..219d457 100644 --- a/src/marketplace/types.ts +++ b/src/marketplace/types.ts @@ -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 +export type CodexCreateAvailabilityInput = v.InferOutput; -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 +export type CodexUpdateAvailabilityInput = v.InferOutput; 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 +export type CodexCreateStorageRequestInput = v.InferOutput; export type CodexCreateStorageRequestResponse = Omit \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c950c29..21fa776 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,8 @@ "DOM", ], "outDir": "./dist", - } + // "module": "ES2015", + // "moduleResolution": "Bundler" + }, + "buildOptions": {} } \ No newline at end of file