diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5600a..11fd38e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ +# 0.1.6 - 24 Apr 2025 +Improvement: +- reduce instruction for string placement +- inline regex test to each string + +# 0.1.5 - 22 Apr 2025 +Improvement: +- add `schema.trusted` for string +- improve string placement when using `t.String({ trusted: true })` and `sanitize: manual` + # 0.1.4 - 27 Mar 2025 Improvement: -- Improve array performance by avoiding unnecessary closure reference +- improve array performance by avoiding unnecessary closure reference # 0.1.3 - 14 Mar 2025 Bug fix: diff --git a/benchmarks/message.ts b/benchmarks/message.ts new file mode 100644 index 0000000..222c9b6 --- /dev/null +++ b/benchmarks/message.ts @@ -0,0 +1,11 @@ +import { t } from 'elysia' +import { benchmark } from './utils' + +benchmark( + t.Object({ + message: t.String({ + trusted: true + }) + }), + { message: 'Hello, World!' as const } +) diff --git a/benchmarks/utils.ts b/benchmarks/utils.ts index 37c0809..98ec118 100644 --- a/benchmarks/utils.ts +++ b/benchmarks/utils.ts @@ -1,4 +1,4 @@ -import { bench, run, barplot, summary, compact } from 'mitata' +import { bench, run, barplot, summary, compact, do_not_optimize } from 'mitata' import { createAccelerator } from '../src' import { TypeCompiler } from '@sinclair/typebox/compiler' @@ -28,7 +28,7 @@ export const benchmark = ( compact(() => { barplot(() => { summary(() => { - bench('JSON Stingify', () => { + bench('JSON Stringify', () => { return JSON.stringify(value) }) @@ -40,13 +40,13 @@ export const benchmark = ( return encode(value) }) - const validator = TypeCompiler.Compile(model) + // const validator = TypeCompiler.Compile(model) - bench('JSON Accelerator w/ validation', () => { - validator.Check(value) + // bench('JSON Accelerator w/ validation', () => { + // validator.Check(value) - return encode(value) - }) + // return encode(value) + // }) }) }) }) diff --git a/bun.lock b/bun.lock index 7216f9e..9a6a936 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ "typescript": "^5.5.3", }, "peerDependencies": { - "@sinclair/typebox": "^0.34.31", + "@sinclair/typebox": ">= 0.34.0", }, "optionalPeers": [ "@sinclair/typebox", diff --git a/example/index.ts b/example/index.ts index b389168..fee8d56 100644 --- a/example/index.ts +++ b/example/index.ts @@ -2,17 +2,17 @@ import { t } from 'elysia' import { createAccelerator } from '../src/index' const shape = t.Object({ - name: t.String(), - playing: t.Nullable(t.Integer({ default: 1 })) + name: t.String({ + // trusted: true + }) }) -console.log(t.Optional(t.String())) +const string = `hi awd` const value = { - name: 'saltyaom', - playing: null + name: string } satisfies typeof shape.static const mirror = createAccelerator(shape) -console.log(mirror(value)) +console.log(JSON.parse(mirror(value))) diff --git a/package.json b/package.json index 071cb71..43bef97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-accelerator", - "version": "0.1.4", + "version": "0.1.6", "description": "Speed up JSON stringification by providing OpenAPI/TypeBox model", "license": "MIT", "scripts": { @@ -11,7 +11,7 @@ "release": "npm run build && npm run test && npm publish --access public" }, "peerDependencies": { - "@sinclair/typebox": "^0.34.31" + "@sinclair/typebox": ">= 0.34.0" }, "peerDependenciesMeta": { "@sinclair/typebox": { diff --git a/src/index.ts b/src/index.ts index e133cd0..d638f9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,10 @@ const isInteger = (schema: TAnySchema) => { continue } - if (!hasNumberType && (type.type === 'number' || type.type === 'integer')) { + if ( + !hasNumberType && + (type.type === 'number' || type.type === 'integer') + ) { hasNumberType = true continue } @@ -153,7 +156,6 @@ const isDateType = (schema: TAnySchema): boolean => { interface Instruction { array: number optional: number - hasString: boolean properties: string[] /** * If unsafe character is found, how should the encoder handle it? @@ -170,12 +172,15 @@ interface Instruction { definitions: Record } +// equivalent to /["\n\r\t\b\f\v]/ +const findEscapeSequence = /["\b\t\n\v\f\r\/]/ + const SANITIZE = { auto: (property: string) => - `re.test(${property})?JSON.stringify(${property}):\`"$\{${property}}"\``, + `${findEscapeSequence}.test(${property})?JSON.stringify(${property}).slice(1,-1):${property}`, manual: (property: string) => `${property}`, throw: (property: string) => - `re.test(${property})?(()=>{throw new Error("Property '${property}' contains invalid characters")})():${property}` + `${findEscapeSequence}.test(${property})?(()=>{throw new Error("Property '${property}' contains invalid characters")})():${property}` } satisfies Record string> const joinStringArray = (p: string) => @@ -257,8 +262,6 @@ const accelerate = ( switch (schema.type) { case 'string': - instruction.hasString = true - // string operation would be repeated multiple time // it's fine to optimize it to the most optimized way if ( @@ -272,10 +275,19 @@ const accelerate = ( // this handle the case where the string contains double quotes // As slice(1,-1) is use several compute and would be called multiple times // it's not ideal to slice(1, -1) of JSON.stringify - if (nullableCondition) - v = `\${${nullableCondition}?${schema.const !== undefined ? `'${JSON.stringify(schema.const)}'` : schema.default !== undefined ? `'${JSON.stringify(schema.default)}'` : `'null'`}:${sanitize(property)}}` - else - v = `${schema.const !== undefined ? `${JSON.stringify(schema.const)}` : `\${${sanitize(property)}}`}` + if (nullableCondition) { + if (schema.trusted) + sanitize = (v: string) => + `\`"$\{${SANITIZE['manual'](v)}}"\`` + + v = `\${${nullableCondition}?${schema.const !== undefined ? `'${JSON.stringify(schema.const)}'` : schema.default !== undefined ? `'${JSON.stringify(schema.default)}'` : `'null'`}:\`"\${${sanitize(property)}}"\`}` + } else { + if (schema.const !== undefined) + v = JSON.stringify(schema.const) + else if (schema.trusted) + v = `"\${${SANITIZE['manual'](property)}}"` + else v = `"\${${sanitize(property)}}"` + } } else { // In this case quote is handle outside to improve performance if (nullableCondition) @@ -344,7 +356,8 @@ const accelerate = ( const name = joinProperty(property, key) const hasShortName = schema.properties[key].type === 'object' && - !name.startsWith('ar') + !name.startsWith('ar') && + Object.keys(schema.properties).length > 5 const i = instruction.properties.length if (hasShortName) instruction.properties.push(name) @@ -475,9 +488,6 @@ const accelerate = ( let setup = '' - if (instruction.hasString) - setup += `const re=/[\\b\\f\\n\\r\\t\\\\\\\\/"]/\n` - if (instruction.optional) { setup += 'let ' @@ -516,7 +526,6 @@ export const createAccelerator = ( array: 0, optional: 0, properties: [], - hasString: false, sanitize, definitions })