Elysia 0.7 - Stellar Stellar | ElysiaJS

ID: 2207https://elysiajs.com/blog/elysia-07
Source

Elysia 0.7 – Stellar Stellar

Published on 20 Sep 2023 by @saltyaom

Landscape of wild and mountain in the night full of stars


Overview

Stellar Stellar brings many exciting new updates to help Elysia solidify the foundation and handle complexity with ease, featuring:

  • Entirely rewrite type, up to 13 × faster type inference.
  • “Trace” for declarative telemetry and better performance audit.
  • Reactive Cookie model and cookie validation to simplify cookie handling.
  • TypeBox 0.31 with custom decoder support.
  • Rewritten Web Socket for even better support.
  • Definitions remapping, and declarative affix for preventing name collisions.
  • Text‑based status

Rewritten Type

Core feature of Elysia about developer experience.

Type is one of the most important aspects of Elysia, as it allows us to do many amazing things like unified type, syncing your business logic, typing, documentation and frontend.

We want you to have an outstanding experience with Elysia, focusing on your business logic part, and let Elysia handle the rest whether it’s type‑inference with unified type, and Eden connector for syncing type with backend.

To achieve that, we put our effort into creating a unified type system for synchronizing all of the type, but as the feature grows, we found that our type inference might not be fast enough from our lack of TypeScript experience a year ago.

With our experience we made along the way of handling complex type system, various optimizations and many projects like Mobius. We challenge ourselves to speed up our type system once again, making this a second type rewrite for Elysia.

We delete and rewrite every Elysia type from ground up to make Elysia type to be magnitude faster.

Speed comparison

Elysia 0.6Elysia 0.7

Using Perfetto and TypeScript CLI to generate trace on a large‑scale and complex app, we measure up to 13× inference speed.

We do have a unit test in type‑level to make sure most of the case, there’s no breaking change for type.


Trace

Performance is another important aspect for Elysia.

We don’t want to be fast for benchmarking purpose, we want you to have a real fast server in real‑world scenario, not just benchmarking.

There are many factors that can slow down your app, and it’s hard to identify one, that’s why we introduce “Trace”.

Trace allows us to tap into a life‑cycle event and identify performance bottleneck for our app.

Example of usage of Trace

This example code allows you to tap into all beforeHandle event, and extract the execution time one‑by‑one before setting the Server‑Timing API to inspect the performance bottleneck.

It’s not limited to only beforeHandle, and events can be traced even the handler itself. The naming convention is named after life‑cycle event you’re already familiar with.

This API allows us to effortlessly audit performance bottleneck of your Elysia server and integrate with the report tools of your choice.

By default, Trace uses AoT compilation and dynamic code injection to conditionally report, and even if you actually use it automatically, there’s no performance impact at all.


Reactive Cookie

We merged our cookie plugin into Elysia core.

Like Trace, Reactive Cookie uses AoT compilation and dynamic code injection to conditionally inject the cookie usage code, leading to no performance impact if you don’t use one.

Reactive Cookie takes a more modern approach like signal to handle cookie with an ergonomic API.

Example of usage of Reactive Cookie

app.get('/', ({ cookie: { name } }) => {
  // Get
  name.value

  // Set
  name.value = "New Value"
})

Then the cookie will be automatically sync the value with headers, and the cookie jar, making the cookie object a single source of truth for handling cookie.

The Cookie Jar is reactive, which means that if you don’t set the new value for the cookie, the Set-Cookie header will not be sent to keep the same cookie value and reduce performance bottleneck.

Cookie Schema

With the merge of cookie into the core of Elysia, we introduce a new Cookie Schema for validating cookie value.

app.get(
  '/',
  ({ cookie: { name } }) => {
    // Set
    name.value = {
      id: 617,
      name: 'Summoning 101',
    }
  },
  {
    cookie: t.Cookie({
      value: t.Object({
        id: t.Numeric(),
        name: t.String(),
      }),
    }),
  }
)

Elysia encodes and decodes cookie value for you automatically, so if you want to store JSON in a cookie like decoded JWT value, or just want to make sure if the value is a numeric string, you can do that effortlessly.

Cookie Signature

And lastly, with the introduction of Cookie Schema and t.Cookie type, we are able to create a unified type for handling sign/verify cookie signature automatically.

Cookie signature is a cryptographic hash appended to a cookie’s value, generated using a secret key and the content of the cookie to enhance security by adding a signature to the cookie.
This ensures that the cookie value is not modified by a malicious actor, helps in verifying the authenticity and integrity of the cookie data.

To handle cookie signature in Elysia, it’s simple as providing a secret and sign property:

new Elysia({
  cookie: {
    secret: 'Fischl von Luftschloss Narfidort',
  },
}).get(
  '/',
  ({ cookie: { profile } }) => {
    profile.value = {
      id: 617,
      name: 'Summoning 101',
    }
  },
  {
    cookie: t.Cookie({
      profile: t.Object({
        id: t.Numeric(),
        name: t.String(),
      }),
    }, {
      sign: ['profile'],
    }),
  }
)

By providing a cookie secret and sign property to indicate which cookie should have a signature verification.
Elysia then signs and unsigns cookie value automatically, eliminating the need for manual sign / unsign functions.
Elysia handles cookie secret rotation automatically – you can append new secrets and Elysia will use the first value to sign a new cookie, while trying to unsign with the rest of the secrets if match.

new Elysia({
  cookie: {
    secrets: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort'],
  },
})

TypeBox 0.31

With the release of 0.7, we are updating to TypeBox 0.31 to bring even more features to Elysia.

This brings new exciting feature like support for TypeBox’s Decode in Elysia natively.

Previously, a custom type like Numeric required a dynamic code injection to convert numeric string to number, but with the use of TypeBox’s decode, we can define a custom function to encode and decode the value of a type automatically.

Numeric = (property?: NumericOptions<number>) =>
  Type.Transform(
    Type.Union([Type.String(), Type.Number(property)]))
    .Decode((value) => {
      const number = +value
      if (isNaN(number)) return value
      return number
    })
    .Encode((value) => value as any as TNumber)

We have rewritten all types that require dynamic code injection to use Transform for easier code maintenance.

New Type: t.ObjectString

With the introduction of Transform, we added a new type like t.ObjectString to automatically decode a value of object in request.

This is useful when you need to use multipart/formdata for handling file uploading but doesn’t support object. You can now just use t.ObjectString() to tell Elysia that the field is a stringified JSON, so Elysia can decode it automatically.

new Elysia()
  .post('/', ({ body: { data } }) => name, {
    body: t.Object({
      image: t.File(),
      data: t.ObjectString({
        name: t.String(),
      }),
    }),
  })

Rewritten Web Socket

Aside from entirely rewritten type, we also entirely rewritten Web Socket as well.

Previous issues:

  1. Schema is not strictly validated.
  2. Slow type inference.
  3. Need for .use(ws()) in every plugin.

With this update, we solve all of those and improve performance.

  • Web Socket is now strictly validated and type is synced automatically.
  • Remove the need for .use(ws()) for using WebSocket in every plugin.
  • Bring performance improvement to near Bun native WebSocket performance.

Thanks to Bogeychan for providing the test case for Elysia Web Socket.


Definitions Remap

Proposed on #83 by Bogeychan.

Elysia allows us to decorate and store request with any value we desire, but some plugins may have duplicate names with the value we have, leading to name collisions.

Remapping

This allows us to remap existing state, decorate, model, derive to anything we like to prevent name collision, or just rename a property.

new Elysia()
  .state({ a: 'a', b: 'b' })
  // Exclude b state
  .state(({ b, ...rest }) => rest)

Affix

When plugins have many property values, it can be overwhelming to remap one‑by‑one.
The Affix function, which consists of a prefix and suffix, allows us to remap all properties of an instance, preventing the name collision of the plugin.

const setup = new Elysia({ name: 'setup' })
  .decorate({
    argon: 'a',
    boron: 'b',
    carbon: 'c',
  })

const app = new Elysia()
  .use(
    setup
      .prefix('decorator', 'setup')
  )
  .get('/', ({ setupCarbon }) => setupCarbon)

By default, affix will handle both runtime and type‑level code automatically, remapping the property to camelCase as naming convention.

You can also remap all property of the plugin:

const app = new Elysia()
  .use(
    setup
      .prefix('all', 'setup')
  )
  .get('/', ({ setupCarbon }) => setupCarbon)

True Encapsulation Scope

With the introduction of Elysia 0.7, Elysia can now truly encapsulate an instance by treating a scoped instance as another instance.

The new scope model can even prevent events like onRequest to be resolved on a main instance which is not possible before.

const plugin = new Elysia({
  scoped: true,
  prefix: '/hello',
})
  .onRequest(() => console.log('In Scoped'))
  .get('/', () => 'hello')

const app = new Elysia()
  .use(plugin)
  .get('/', () => 'Hello World')

Further, scoped is now truly scoped down both in runtime and type level which is not possible without the type rewrite mentioned before.
This is exciting from a maintainer side because previously, it was almost impossible to truly encapsulate the scope of an instance, but using mount and WinterCG compilation, we are finally able to truly encapsulate the instance of the plugin while providing a soft link with main instance property like state, decorate.


Text‑Based Status

There are over 64 standard HTTP status codes to remember, and sometimes we also forget the status we want to use.

We ship 64 HTTP Status codes in text‑based form with autocompletion for you.

Example of using text‑base status code

Text will then resolve to status code automatically as expected.

Text‑base status code showing autocompletion

This is a small ergonomic feature to help you develop your server without switching between IDE and MDN to search for a correct status code.


Notable Improvement

Improvements

  • onRequest can now be async
  • add Context to onError
  • lifecycle hook now accepts array functions
  • static Code Analysis now supports rest parameters
  • breakdown dynamic router into single pipeline instead of inlining to static router to reduce memory usage
  • set t.File and t.Files to File instead of Blob
  • skip class instance merging
  • handle UnknownContextPassToFunction
  • WebSocket – added unit tests and fixed example & API (#157)
  • Add GitHub action to run Bun test (#179)

Breaking Changes

  • remove ws plugin, migrate to core
  • rename addError to error

Changes

  • using single findDynamicRoute instead of inlining to static map
  • remove mergician
  • remove array routes due to problem with TypeScript
  • rewrite Type.ElysiaMeta to use TypeBox.Transform

Bug Fixes

  • strictly validate response by default
  • t.Numeric not working on headers / query / params
  • t.Optional(t.Object({ [name]: t.Numeric })) causing error
  • add null check before converting Numeric
  • inherit store to instance plugin
  • handle class overlapping
  • InternalServerError message fixed to "INTERNAL_SERVER_ERROR" instead of "NOT_FOUND" (#187)
  • mapEarlyResponse with aot on after handle (#167)

Afterward

Since the latest release, we have gained over 2 000 stars on GitHub!

Pushing the boundary of TypeScript, and developer experience even to the point that we are doing something we feel truly profound.

With every release, we are gradually one step closer to bringing the future we drew long time ago.

A future where we can freely create anything we want with an astonishing developer experience.

We feel truly grateful to be loved by you and the lovely community of TypeScript and Bun.

It’s exciting to see Elysia is being brought to life with amazing developers like:

And much more developers that choose Elysia for their next project.

Our goal is simple: to bring an eternal paradise where you can pursue your dream and everyone can live happily.

Thank you and your love and overwhelming support for Elysia. We hope we can paint the future to pursue our dream a reality one day.

May all the beauty be blessed

Stretch out that hand as if to reach someone
I’m just like you, nothing special
That’s right, I’ll sing the song of the night
Stellar Stellar
In the middle of the world, the universe
The music won’t ever, ever stop tonight
That’s right, I’d always longed to be
Not Cinderella, forever waiting
But the prince that came to for her
Cause I’m a star, that’s why
Stellar Stellar


← Elysia: Ergonomic Framework for Humans

Next: At Glance