github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/utils/classes/stream-logger.js (about)

     1  import EmberObject, { computed } from '@ember/object';
     2  import { task } from 'ember-concurrency';
     3  import TextDecoder from 'nomad-ui/utils/classes/text-decoder';
     4  import { decode } from 'nomad-ui/utils/stream-frames';
     5  import AbstractLogger from './abstract-logger';
     6  import { fetchFailure } from './log';
     7  import classic from 'ember-classic-decorator';
     8  
     9  @classic
    10  export default class StreamLogger extends EmberObject.extend(AbstractLogger) {
    11    reader = null;
    12  
    13    static get isSupported() {
    14      return !!window.ReadableStream;
    15    }
    16  
    17    @computed()
    18    get additionalParams() {
    19      return {
    20        follow: true,
    21      };
    22    }
    23  
    24    start() {
    25      return this.poll.perform();
    26    }
    27  
    28    stop() {
    29      const reader = this.reader;
    30      if (reader) {
    31        reader.cancel();
    32      }
    33      return this.poll.cancelAll();
    34    }
    35  
    36    @task(function* () {
    37      const url = this.fullUrl;
    38      const logFetch = this.logFetch;
    39  
    40      const reader = yield logFetch(url).then((res) => {
    41        const reader = res.body.getReader();
    42        // It's possible that the logger was stopped between the time
    43        // polling was started and the log request responded.
    44        // If the logger was stopped, the reader needs to be immediately
    45        // canceled to prevent an endless request running in the background.
    46        if (this.poll.isRunning) {
    47          return reader;
    48        }
    49        reader.cancel();
    50      }, fetchFailure(url));
    51  
    52      if (!reader) {
    53        return;
    54      }
    55  
    56      this.set('reader', reader);
    57  
    58      let streamClosed = false;
    59      let buffer = '';
    60      const decoder = new TextDecoder();
    61  
    62      while (!streamClosed) {
    63        yield reader.read().then(({ value, done }) => {
    64          streamClosed = done;
    65  
    66          // There is no guarantee that value will be a complete JSON object,
    67          // so it needs to be buffered.
    68          buffer += decoder.decode(value, { stream: true });
    69  
    70          // Only when the buffer contains a close bracket can we be sure the buffer
    71          // is in a complete state
    72          if (buffer.indexOf('}') !== -1) {
    73            // The buffer can be one or more complete frames with additional text for the
    74            // next frame
    75            const [, chunk, newBuffer] = buffer.match(/(.*\})(.*)$/);
    76  
    77            // Peel chunk off the front of the buffer (since it represents complete frames)
    78            // and set the buffer to be the remainder
    79            buffer = newBuffer;
    80  
    81            // Assuming the logs endpoint never returns nested JSON (it shouldn't), at this
    82            // point chunk is a series of valid JSON objects with no delimiter.
    83            const { offset, message } = decode(chunk);
    84            if (message) {
    85              this.set('endOffset', offset);
    86              this.write(message);
    87            }
    88          }
    89        });
    90      }
    91    })
    92    poll;
    93  }