sveltekit-sse

repository·main·Indexed 19 days ago

https://github.com/razshare/sveltekit-sse

A library for producing and consuming Server-Sent Events (SSE) in SvelteKit applications. It provides utilities like produce() for creating SSE responses with built-in locking and ping support, and source() for creating reactive Svelte stores that consume SSE streams. Features include automatic reconnection, visibility awareness via fetchEventSource, and the ability to filter events using select(), parse JSON with json(), and apply custom transformations with transform().

Tokens
5.8K
Snippets
18
Records
21
Agent score
66%

What's inside sveltekit-sse

  1. Control stream lifetime with Locking

    main

    By default, streams are locked server-side (kept alive indefinitely). You can control this using the lock object (a Writable<bool>) provided to the start function. Setting lock.set(false) closes the stream.

    Important: Do not attempt to emit messages after calling lock.set(false), as emit will return an error.

    import { produce } from 'sveltekit-sse'
    
    export function POST() {
      return produce(function start({ emit, lock }) {
        emit('message', 'hello world')
        setTimeout(function unlock() {
          lock.set(false)
        }, 2000)
      })
    }
    import { produce } from 'sveltekit-sse'
    export function POST() {
      return produce(function start({ emit, lock }) {
        emit('message', 'hello world')
        setTimeout(function unlock() {
          lock.set(false)
        }, 2000)
      })
    }
  2. How to produce data from third-party sources

    main

    To emit events from external sources (like a database or a global event bus), you must map user sessions to their respective emit functions.

    1. Create a global Map to store sessionId -> emit mappings.
    2. In your produce function, capture the sessionId (e.g., from headers) and add the emit function to the map.
    3. Use the stop() option to remove the entry from the map when the client disconnects.
    4. Access the emit function from your external source using the sessionId.

    Distributed Systems Note: In distributed environments, avoid storing these mappings in local memory. Instead, use a centralized system (like PostgreSQL LISTEN/NOTIFY) to broadcast events to all nodes, which then forward them to the correct local client emitters.

    // src/lib/clients.js
    export const clients = new Map()
    
    // src/routes/events/+server.js
    import { clients } from '$lib/clients'
    export function POST({ request }) {
      return produce(
        function start({ emit }) {
          const sessionId = request.headers.get('session-id') ?? ''
          if (!sessionId) return
          clients.set(sessionId, emit)
        },
        {
          stop() {
            const sessionId = request.headers.get('session-id') ?? ''
            clients.delete(sessionId)
          },
        },
      )
    }
    
    // some-file.js
    import { clients } from '$lib/clients'
    clients.get('some-session-id').emit('message', 'hello')
  3. Consume Server-Sent Events with `source`

    main

    On the client side, use the source function to connect to an SSE endpoint. Use .select(eventName) to create a Svelte store for a specific event type.

    <script>
      import { source } from 'sveltekit-sse'
      // Connect to the endpoint and select the 'message' event
      const value = source('/custom-event').select('message')
    </script>
    
    <!-- Use the $ prefix to subscribe to the store -->
    {$value}
    <script>
      import { source } from 'sveltekit-sse'
      const value = source('/custom-event').select('message')
    </script>
    
    {$value}
  4. Handle connection stops and cleanup

    main

    You can execute code when a connection is stopped (either via manual unlocking or client disconnection) using two methods:

    1. Return a function from start(): This function runs when the connection is canceled.
    2. Use options.stop(): This runs when lock.set(false) is called OR when the client cancels the connection.

    Customizing Ping Interval: To detect client disconnection faster, you can customize the ping interval in the options object (default is 30 seconds).

    import { produce } from 'sveltekit-sse'
    
    export function POST() {
      return produce(
        function start({ emit, lock }) {
          emit('message', 'hello')
          // Manual stop
          // lock.set(false)
        },
        {
          ping: 4000, // Custom ping interval in ms
          stop() {
            console.log('Connection stopped or client disconnected.')
          }
        }
      )
    }
    import { produce } from 'sveltekit-sse'
    export function POST() {
      return produce(
        function start({ emit, lock }) {
          emit('message', 'hello')
          lock.set(false)
        },
        {
          stop() {
            console.log('Connection stopped.')
          },
        },
      )
    }
  5. Produce Server-Sent Events with `produce`

    main

    Use the produce function in a SvelteKit server route (e.g., +server.js) to create an SSE stream. The produce function accepts a start function which provides an emit method to send events to the client.

    import { produce } from 'sveltekit-sse'
    
    export function POST() {
      return produce(async function start({ emit }) {
        while (true) {
          // emit returns an object containing an optional error
          const { error } = emit('message', `the time is ${Date.now()}`)
          if (error) {
            return
          }
          await new Promise(r => setTimeout(r, 1000))
        }
      })
    }
    import { produce } from 'sveltekit-sse'
    
    export function POST() {
      return produce(async function start({ emit }) {
          while (true) {
            const {error} = emit('message', `the time is ${Date.now()}`)
            if(error) {
              return
            }
            await new Promise(function run(resolve) { setTimeout(resolve, 1000) })
          }
      })
    }
  6. Configure fetchEventSource reconnection and visibility behavior

    main

    You can control how fetchEventSource manages the connection lifecycle through specific options:

    Handling Document Visibility

    By default, the utility listens to visibilitychange. When the user switches tabs or minimizes the window, the connection is aborted to prevent unnecessary network usage. When the user returns, it automatically attempts to reconnect.

    To keep the connection alive even when the tab is in the background, set openWhenHidden to true:

    fetchEventSource('/api/sse', {
      openWhenHidden: true
    });

    Manual Cancellation

    To stop the stream manually from your application code, pass an AbortController signal in the options:

    const controller = new AbortController();
    
    fetchEventSource('/api/sse', {
      signal: controller.signal
    });
    
    // Later, to stop the stream:
    controller.abort();
  7. Reconnect to a closed stream

    main

    If a stream closes, you can implement reconnection logic using the onclose option in the source function. The onclose callback provides a connect function to trigger a retry.

    <script>
      import { source } from 'sveltekit-sse'
    
      const connection = source('/custom-event', {
        async onclose({ connect }) {
          console.log('reconnecting with a delay...')
          setTimeout(connect, 1000)
        },
      })
    
      const data = connection.select('message')
    </script>
    
    {$data}
    <script>
      import { source } from 'sveltekit-sse'
    
      const connection = source('/custom-event', {
        async onclose({ connect }) {
          console.log('reconnecting with a delay...')
          setTimeout(connect, 1000)
        },
      })
    
      const data = connection.select('message')
    
      setTimeout(function run() {
        connection.close()
      }, 3000)
    </script>
    
    {$data}
  8. Transform and Parse SSE data

    main

    Once you have selected an event via .select(eventName), you can manipulate the data stream:

    • transform(fn): Applies a function to the incoming string value to change its type or format.
    • json(fn): Attempts to parse the incoming string as JSON. If parsing fails, the provided function is called, allowing you to handle errors and return a fallback value (e.g., the previous valid value).
    <script>
      import { source } from 'sveltekit-sse'
      const connection = source('/custom-event')
    
      // Example: Transform
      const channel = connection.select('message')
      const transformed = channel.transform(data => `transformed: ${data}`)
    
      // Example: JSON parsing with error handling
      const json = connection.select('message').json((err, { raw, previous }) => {
        console.error(`Parse error: ${err}`, raw)
        return previous // Fallback to last valid value
      })
    </script>
    <script>
      import { source } from 'sveltekit-sse'
      const connection = source('/custom-event')
      const channel = connection.select('message')
    
      const transformed = channel.transform(function run(data) {
        return `transformed: ${data}`
      })
    
      $: console.log({ $transformed })
    </script>
  9. Pass custom headers to `source`

    main

    You can include custom headers (like Authorization) when initializing a source connection.

    <script>
      import { source } from 'sveltekit-sse'
      const connection = source('/event', {
        headers: {
          Authorization: 'Bearer ...',
        },
      })
      const data = connection.select('message')
    </script>
    <script>
      import { source } from 'sveltekit-sse'
      const connection = source('/event', {
        headers: {
          Authorization: 'Bearer ...',
        },
      })
    
      const data = connection.select('message')
    </script>
  10. Parse event data as JSON using json()

    main

    When using a selector created via select(eventName), you can call .json() to return a derived Svelte store that automatically attempts to parse the incoming string data as JSON.

    Error Handling: If parsing fails, the store will emit an object containing the error and the raw data instead of throwing. You can provide a fallback function or to define what should be emitted on failure.

    Parameters:

    • or (function, optional): A fallback function that receives an object with { error, raw, previous } when parsing fails. If not provided, it returns null on error.
    const connection = source('/events');
    
    const userStore = connection.select('user_updated').json((err) => {
      console.error('JSON Parse Error:', err.error);
      return { error: true, fallback: true };
    });
    
    userStore.subscribe(data => {
      // data is the parsed JSON object or the result of the fallback function
      console.log(data);
    });
  11. Use produce() to create an SSE Response

    main

    The produce function is the primary entrypoint for creating a Server-Sent Events (SSE) response in SvelteKit. It accepts a start function which provides an emit method to send events to the client.

    Important: Error Handling You must check the return value of emit. If emit returns an object containing an error, you must stop your execution (e.g., by returning from the function). Failing to check for errors can lead to memory leaks because the server will continue attempting to produce data for a disconnected client.

    export function POST() {
      return produce(function start({ emit }) {
        const notifications = [
          { title: 'title-1', body: 'lorem...' },
          { title: 'title-2', body: 'lorem...' },
          { title: 'title-3', body: 'lorem...' },
        ]
    
        for (const notification of notifications) {
          const { error } = emit('notification', JSON.stringify(notification))
          if (error) {
            // Make sure to check for errors,
            // otherwise your stream will keep producing data
            // and you'll create a memory leak.
            return
          }
        }
      })
    }