Skip to content

Releases: razshare/sveltekit-server-session

0.1.5

02 Apr 02:07
Compare
Choose a tag to compare

SvelteKit Server Session

This library provides an easy way to start, serve and modify server sessions.

Install with:

npm i -D sveltekit-server-session

Start a session

Use session.start() to start a session.
It requires SvelteKits' Cookies interface.

import { session } from 'sveltekit-server-session'

export async function GET({ cookies }) {
    const {data, response} await session.start({ cookies })
    return response("hello world")
}

Note

The response() function creates a Response object and appends to it the headers required for session management.

An example

<!-- src/routes/+page.svelte -->
<script>
  import { onMount } from 'svelte'
  let text = ''
  let ready = false
  let sending = false

  onMount(async function start() {
    const response = await fetch('/session/quote/get')
    text = await response.text()
    ready = true
  })

  async function set() {
    sending = true
    await fetch('/session/quote/update', { method: 'PUT', body: text })
    sending = false
  }
</script>

{#if ready}
  <div class="content">
    <textarea bind:value={text}></textarea>
    <br />
    <button disabled={sending} on:mouseup={set}>
      <span>Save</span>
    </button>
  </div>
{/if}
// src/routes/session/quote/get/+server.js
import { session } from 'sveltekit-server-session'

export async function GET({ cookies }) {
  const {
    error,
    value: { data, response },
  } = await session.start({ cookies })

  if (error) {
    return new Response(error.message, { status: 500 })
  }

  if (!data.has('quote')) {
    data.set('quote', 'initial quote')
  }

  return response(data.get('quote'))
}
// src/routes/session/quote/update/+server.js
import { session } from 'sveltekit-server-session'

export async function PUT({ cookies, request }) {
  const {
    error,
    value: { data, response },
  } = await session.start({ cookies })

  if (error) {
    return new Response(error.message, { status: 500 })
  }

  data.set('quote', await request.text())

  return response(data.get('quote'))
}

Peek 2024-04-01 03-15

Lifetime

The only way to start a session is through

await session.start({ cookies })

Whenever you start a session you're actually trying to retrieve a KITSESSID cookie from the client, which should hold a session id.

Note

Sessions are internally mapped with a Map<string, Session> object.
This map's keys are the sessions ids of your application.

If the client doesn't hold a session id cookie it means it has no session, so a new one is created.

If the client does have a session id but is expired, the relative session is immediately destroyed, then a new session is created.
This new session doesn't contain any of the old session's data.

Finally, if the client has a valid session id cookie, then the relative session is retrieved.


Starting a session should always succeed, wether it is by creating a new session or retrieving an existing one.

Destroy & Flush

As explained above, in the lifetime section, clients that present an expired session id have their sessions destroyed immediately.

But sometimes clients want to create new sessions regardless if the current one has expired or not.
Other times clients abandon their sessions and never awaken them again.

These use cases can be a problem.

Even though these "abandoned" sessions are not active, they will still use some memory,
so they must be destroyed one way or another.

You can use destroy()

// src/routes/session/destroy/+server.js
import { session } from 'sveltekit-server-session'

/**
 *
 * @param {number} milliseconds
 * @returns {Promise<void>}
 */
function delay(milliseconds) {
  return new Promise(function start(resolve) {
    setTimeout(resolve, milliseconds)
  })
}

export async function GET({ cookies }) {
  const {
    error,
    value: { destroy },    // <=== Obtain the function here.
  } = await session.start({ cookies })

  if (error) {
    return new Response(error.message, { status: 500 })
  }

  await destroy()    // <=== Destroy the session here.

  await delay(3000)

  return new Response('Session destroyed.')
}

to destroy each session manually.

For example this may be useful to invoke when clicking a logout button.

Peek 2024-04-01 04-50

However one problem still remains

Other times clients abandon their sessions and never awaken them again.

When a client simply stops using your application for days even, destroying a session can become a bit more convoluted,
because in those cases there's no client to click the hypothetical logout button.

So what do we do?

The answer is nothing, this library takes care of that.

Whenever you create a session, a destructor function is automatically dispatched to destroy the session when it expires.
This is simply achieved through the event loop, using a Timer through setTimeout.

Custom Behavior

You can customize your sessions behavior with session.setOperations().

All operations related to session management are defined by SessionInterface.
Even though some operations may appear to act directly on an instance of a session, like .destroy(), in reality they all use only operations defined by SessionInterface.

This means you have total control over how sessions are retrieved, modified and validated.

Here's an example of how to set a custom set of operations for session management

import { ok } from 'sveltekit-unsafe' // peer dependency
import { session } from 'sveltekit-server-session'
/**
 * @type {Map<string,import('$lib/types').Session>}
 */
const map = new Map()
session.setOperations({
  async exists(id) {
    return ok(map.has(id))
  },
  async isValid(id) {
    const session = map.get(id)
    if (!session) {
      return ok(false)
    }
    return ok(session.getRemainingSeconds() > 0)
  },
  async has(id) {
    return ok(map.has(id))
  },
  async get(id) {
    return ok(map.get(id))
  },
  async set(id, session) {
    map.set(id, session)
    return ok()
  },
  async delete(id) {
    map.delete(id)
    return ok()
  },
})

Note

As you may have figured out, since these function definitions have an implicit asynchronous nature
they might as well query a database instead of working with an in-memory map, for improved resilience.

Using SvelteKit Hooks

You can simplify your developer experience by moving the session management logic into your src/hooks.server.js.

  1. First of all create a new src/hooks.server.js and move your session management logic in your handle function
    image
    import { session } from 'sveltekit-server-session';
    
    /**
    * @type {import("@sveltejs/kit").Handle}
    */
    export async function handle({ event, resolve }) {
    const { error, value: sessionLocal } = await session.start({ cookies: event.cookies });
    
    if (error) {
      return new Response(error.message, { status: 500 });
    }
    
    event.locals.session = sessionLocal;  // <=== Set session here.
                                          // You will get a type hinting error, this is normal.
                                          // See next step in order to fix this.
    
    const response = await resolve(event);
    
    for (const [key, value] of sessionLocal.response().headers) {
      response.headers.set(key, value);
    }
    
    return response;
    }
  2. Open your src/app.d.ts file and define your session key under interface Locals
    image
    // See https://kit.svelte.dev/docs/types#app
    
    import type { Session } from 'sveltekit-server-session';
    
    // for information about these interfaces
    declare global {
    namespace App {
      // interface Error {}
      interface Locals {
        session: Session; // <== Here.
      }
      // interface PageData {}
      // interface PageState {}
      // interface Platform {}
    }
    }
    
    export {};

And you're done, now all you have to do is destruct your session from your endpoints like so

// src/routes/session/quote/update/+server.js
export async function PUT({ locals, request }) {
  const { data, response } = locals.session // <=== Here.
  data.set('quote', await request.text())
  return response(data.get('quote'))
}

Recommended Usage

So far the development process in the example above has involved using a fetch('/session/quote/get') call to retrieve the initial value of the quote, but that is not necessary, we can simplify things even further by building on top of the Using SvelteKit Hooks section.

Since the hook handler defined above populates our locals prop, this means we now have access to the session from any +page.server.js file, so the following is now possible

// src/routes/+page.server.js
/**
 * @type {import("./$types").PageServerLoad}
 */
export function load({ locals }) {
  const { data } = locals.session;

  if (!data.has('quote')) {
    data.set('quote', 'initial quote');
  }

  return {
    text: data.get('quote')
  };
}

There is no need for the src/routes/session/quote/get/+server.js file an...

Read more