PAD + Alpine

Server-sent events

padSse sends the server's numbers every second as JSON; the component opens an EventSource in x-init and closes it when the stream says it is done.

padSseevery= EventSourcex-init
Live
Server time
Load
Memory, kB
Events
Load, the last thirty seconds
Requests to PAD
    Nothing yet - use the example above, and every request a component makes to PAD shows here.

    How it is built

    PAD, on the server Alpine, in the browser

    <div x-data="live('?examples/live&stream')" x-init="open()">
    
      <div class="toolbar">
        <span class="badge" :class="state === 'open' ? 'is-good' : 'is-bad'" x-text="state === 'open' ? 'receiving' : state"></span>
        <button class="btn-sm" @click="open()" x-show="state !== 'open'" x-cloak>Listen again</button>
        <button class="btn-sm btn-ghost" @click="close()" x-show="state === 'open'" x-cloak>Stop</button>
      </div>
    
      <div class="grid tiles">
        <div class="panel"><div class="panel-title">Server time</div><div class="tile" x-text="last.time ?? '-'"></div></div>
        <div class="panel"><div class="panel-title">Load</div><div class="tile" x-text="last.load ?? '-'"></div></div>
        <div class="panel"><div class="panel-title">Memory, kB</div><div class="tile" x-text="last.memory ?? '-'"></div></div>
        <div class="panel"><div class="panel-title">Events</div><div class="tile" x-text="count"></div></div>
      </div>
    
      <div class="panel spark">
        <div class="panel-title">Load, the last thirty seconds</div>
        <svg viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true"><polyline :points="points()" fill="none" stroke="currentColor" stroke-width="2" vector-effect="non-scaling-stroke"></polyline></svg>
      </div>
    </div>
    
    <script src="{$padRoot}alpine/examples/live.js"></script>
    <?php
    
      // ?examples/live&stream is the stream: padSse calls the producer every second for half a
      // minute and sends what it answers as a 'stats' event - JSON - then a 'done' event; the
      // page itself, asked without stream, is the board the numbers come into.
    
      if ( padRequestHas ( 'stream' ) ) {
    
        $round = 0;
    
        padSse ( function ( $send ) use ( &$round ) {
    
          $round++;
    
          if ( $round > 30 ) {
            $send ( 'done', 'done' );
            return FALSE;
          }
    
          $load = function_exists ( 'sys_getloadavg' ) ? round ( sys_getloadavg () [0], 2 ) : 0;
    
          return [ 'round' => $round, 'time' => date ( 'H:i:s' ), 'load' => $load,
                   'memory' => round ( memory_get_usage () / 1024 ) ];
    
        }, [ 'every' => 1, 'for' => 35, 'event' => 'stats', 'retry' => 5000 ] );
    
      }
    
    ?>
    // The board: an EventSource on the page's stream, opened in x-init. Every 'stats' event is
    // JSON; 'done' says the stream is over, and the board closes the connection - without that
    // EventSource would reconnect after the retry the server asked for.
    
    document.addEventListener('alpine:init', () => {
    
      Alpine.data('live', (url) => ({
        source: null,
        state: 'closed',
        last: {},
        count: 0,
        loads: [],
    
        open() {
          this.close();
          this.source = new EventSource(url);
          this.state = 'connecting';
          this.source.onopen = () => { this.state = 'open'; };
          this.source.onerror = () => { if (this.state !== 'done') this.state = 'reconnecting'; };
          this.source.addEventListener('stats', (event) => {
            this.last = JSON.parse(event.data);
            this.count++;
            this.loads = [...this.loads, this.last.load].slice(-30);
          });
          this.source.addEventListener('done', () => { this.close(); this.state = 'done'; });
        },
    
        close() {
          if (this.source) this.source.close();
          this.source = null;
          this.state = 'closed';
        },
    
        // between the lowest and the highest load of the line, so a small change shows
        points() {
          const low = Math.min(...this.loads), high = Math.max(...this.loads);
          const span = Math.max(high - low, 0.05);
          const step = this.loads.length > 1 ? 300 / (this.loads.length - 1) : 0;
          return this.loads.map((load, at) => (at * step).toFixed(1) + ',' + (56 - (load - low) / span * 52).toFixed(1)).join(' ');
        },
    
        destroy() { this.close(); }
      }));
    
    });