npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

safe-ssr

v0.0.7

Published

> [!NOTE] > > Safe **server-side isolated state** for **SvelteKit** & **Svelte 5**

Downloads

13

Readme

safe-ssr

[!NOTE]

Safe server-side isolated state for SvelteKit & Svelte 5

What is it?

SvelteKit leaks state between requests, which can be a security issue. This package provides a way to safely isolate data between requests in a SvelteKit app.

See this issue for more details:

  • https://github.com/sveltejs/kit/discussions/4339

1. Install

npm i safe-ssr

2. Wrapping your requests

Inside of your src/hooks.server.ts file, call the safe_request_wrapper middleware function available in safe-ssr/safe_request_wrapper in your handle

import { safe_request_wrapper } from "safe-ssr/safe_request_wrapper"
import { request_symbol } from "safe-ssr"
import { sequence } from "@sveltejs/kit/hooks"

export const handle = sequence(
    // ... handler logic BEFORE here will **not** have access to the current unique request symbol
    safe_request_wrapper,
    // ... handler logic BEYOND here will have access to the current unique request symbol
    ({ event, resolve }) => {
        console.log(request_symbol.current()) // -> Symbol()
        return resolve(event)
    }
)

This will wrap each incoming request in an AsyncLocalStorage context, with a unique Symbol assigned to it.

This symbol is then later used to retrive state uniquely associated with the current request - which is guaranteed to be isolated from other requests.

3. Passing state from the server to the client

Because state from .ts / .js modules are not returned to the browser, we need to manually pass them from the server to the client. This is done by using the SerialiseClientState component within your root +layout.svelte file.

[!WARNING] This will serialise all of the state to the client when returning the SSR response. Make sure to put it at the very end of your root +layout.svelte file so that it is serialised last, ensuring that all your state is serialized to the client.

<script lang="ts">
import { SerialiseClientState } from "safe-ssr";

let {
    children
} = $props()
</script>
{@render children()}
<!-- Make sure to put this at the very end of your root `+layout.svelte` file -->
<SerialiseClientState/>

4. Defining global state

$lib/[your_state_file].ts

// returns a reactive object, isolated from other requests
import { safe_state } from "safe-ssr";

export const app_state = safe_state("app_state", {
    counter: 0,
})

Return type

/**
 * both safe_state and safe_state return the same type, which looks like this:
 * Where {T} is the type of the initial value
 *
 * We use the `inner` property to access the value
 * Internally, `inner` is a `get` method that returns the state uniquely associated
 * with the current request.
 **/
type ReturnType<T> = { inner: T }

Retrieving state

Simply import the module into anywhere in your app.

+page.server.ts

import { app_state } from "$lib/app_state";

export function load() {
    app_state.inner.counter++;
}

+page.svelte

<script lang="ts">
import { app_state } from "$lib/app_state";
</script>
<!-- this will never be higher than 1, because requests are isolated -->
{ app_state.inner.counter } <button on:click={clicked}>+</button>

Advanced usage examples

[!TIP]

Maybe you wanna create a server-side database instance that is authenticated to individual users but isolated from other requests?

Unfortunately, by default, SvelteKit load functions waterfall, which means if you have complex nested routes and load functions, you lose out on performance because all of your load functions depend on await parent() calls in SvelteKit.

Fortunately, using the safe_request_wrapper middleware, you can isolate your database instance to individual requests, whilst making it globally accessible across your app

This means you can turn sequential waterfalled requests into parallellised requests, which can be a huge performance boost.

You can use the request_symbol store to customise and implement your own behaviour. The request_symbol import has a current() method that returns the current request symbol or throws an error if one has not been set.

Per-request database isolation example

hooks.server.ts

import { safe_request_wrapper } from "safe-ssr/safe_request_wrapper"
import { sequence } from "@sveltejs/kit/hooks"
import { auth_state } from "$lib/auth-state"
import { req_dbs } from "$lib/db"
import { Database } from "YOUR-DATABASE-LIBRARY"

export const handle = sequence(
    safe_request_wrapper,
    read_auth_token_cookie,
    setup_isolated_db,
)

async function read_auth_token_cookie({ event, resolve }) {
    auth_state.inner.token = event.cookies.get("token")

    return resolve(event)
}


async function setup_isolated_db({ event, resolve }) {
    let db: Promise<Database> | undefined
    // This gets the current unique request symbol
    let sym = request_symbol.current()

    // Associate the current request symbol with a function
    // that returns a Promise<Database>
    req_dbs.set(sym, () => {
        // create a lazy-loaded database instance,
        // because not every request will need one.
        // (e.g. setup_isolated_db will get called for asset requests and etc)
        return db ?? db = Database.connect(auth_state.inner.token)
    })

    return await resolve(event)
}

$lib/db.ts

import { request_symbol } from "safe-ssr"

// We use a WeakMap, so that the database instances are
// garbage collected after the request is complete.
export const req_dbs = new WeakMap<symbol, () => Promise<Database>>()

export const db_store = {
    get db() {
        const sym = request_symbol.current()

        let db = req_dbs.get(sym)

        if(!db) throw new Error("Database used before it was initialised")

        return db()
    }
}

$lib/auth-state.ts

import { safe_state } from "safe-ssr"

export const auth_state = safe_state("auth_state", {
    token: null
})

+page.server.ts

import { auth_state } from "$lib/auth-state"
import { db_store } from "$lib/db"

export async function load() {
    // get the database authenticated to the current
    // requests user, which is isolated from other requests.
    const db = await db_store.db

    // do something with the database
    const my_posts = await db.query("SELECT * FROM posts")

    return {
        my_posts
    }
}