Skip to content

Demo: SSE Ticker

This demo shows how to use SSE to implement a simple update ticker.

It connects to wikipedia's public SSE endpoint and displays the latest 10 updates in a ticker.

Code

vue
<template>
  <article class="sse-ticker-demo pico demo">
    <div class="top-bar">
      <div class="buttons">
        <button
          v-if="
            ['disconnected', 'connecting'].includes(model.connectionState) ||
            (!model.updates.length && model.connectionState === 'connected')
          "
          :disabled="model.connectionState !== 'disconnected'"
          :aria-busy="
            model.connectionState === 'connecting' || (model.connectionState === 'connected' && !model.updates.length)
          "
          @click="controller.start"
        >
          Start Receiving Updates
        </button>
        <button v-if="model.connectionState === 'connected'" class="secondary" @click="controller.stop">Stop</button>
      </div>
      <button class="outline secondary" disabled>Status: {{ model.connectionState }}</button>
    </div>
    <InlineMessage ref="inlineMessage" />
    <template v-if="model.updates.length">
      <hr />
      <table v-if="model.updates.length" class="striped">
        <colgroup>
          <col style="width: auto" />
          <col style="width: 1%" />
        </colgroup>
        <thead>
          <tr>
            <th><span class="truncate">Title</span></th>
            <th>Timestamp</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="update in model.updates" :key="update.id">
            <td>
              <span class="truncate">{{ update.title }}</span>
            </td>
            <td>{{ update.timestamp }}</td>
          </tr>
        </tbody>
      </table>
    </template>
  </article>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import controller from './sse-ticker-controller';
import model from './sse-ticker-model';
import InlineMessage from 'apihive-common-docs-assets/components/InlineMessage.vue';

const inlineMessage = ref<typeof InlineMessage | null>(null);

controller.onDisconnect((message) => {
  inlineMessage.value!.show('error', message);
});
</script>

<style lang="scss" scoped>
.sse-ticker-demo {
  table {
    width: 100% !important;
    table-layout: fixed;

    th:first-child,
    td:first-child {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      max-width: 0;
      min-width: 0;
    }

    th:nth-child(2),
    td:nth-child(2) {
      width: 1% !important;
      white-space: nowrap;
    }
  }

  .truncate {
    display: block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .top-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    .buttons {
      display: flex;
      gap: 0.5rem;
    }
  }
}
</style>
ts
import { type APIConfig, HTTPRequestFactory, SSESubscription, WrappedSSEResponse } from '../../../src';
import { SSERequestFeature } from '../../../src/features/sse-request';
import model from './sse-ticker-model';

const api: APIConfig = {
  name: 'default',
  baseURL: 'https://stream.wikimedia.org/v2/stream',
  endpoints: {
    updates: {
      method: 'SSE',
      target: '/recentchange'
    }
  }
};

class SSETickerController {
  private factory: HTTPRequestFactory;
  private subscription: SSESubscription;

  constructor() {
    this.factory = new HTTPRequestFactory()
        .use(new SSERequestFeature())
        .withWrappedResponseError()
        .withAPIConfig(api);
  }

  onDisconnect(handler:(errorMessage:string) => void) {
    this.factory.withErrorInterceptors((error) => {
        handler(error.message);
        return false;
    })
  }

  public async start() {
    model.connectionState = 'connecting';
    const {subscription, error} = 
        await this.factory
            .withAPIConfig(api)
            .createSSEAPIRequest('updates')
            .withErrorInterceptors(() => {
                model.connectionState = 'disconnected'
                return false;
            })
            .withSSEListeners((data) => {
                model.updates.unshift(data);
                if(model.updates.length > 10)
                    model.updates.splice(10);
            })
            .execute() as WrappedSSEResponse;
    if(error) {
        model.error = error.message;
        return;
    }
    this.subscription = subscription!;
    model.connectionState = 'connected';
  }

  public stop() {
    this.subscription.close();
    model.connectionState = 'disconnected';
  }
}

export default new SSETickerController();
ts
import { reactive } from 'vue';

interface Update {
    timestamp: string;
    title: string;
    id: number;
}

export default reactive<{
    updates: Update[]
    connectionState : 'disconnected' | 'connecting' | 'connected',
    error: string
}>({
    updates: [],
    connectionState : 'disconnected',
    error: ''
})