github.com/manicqin/nomad@v0.9.5/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  
     8  export default EmberObject.extend(AbstractLogger, {
     9    reader: null,
    10  
    11    additionalParams: computed(() => ({
    12      follow: true,
    13    })),
    14  
    15    start() {
    16      return this.poll.perform();
    17    },
    18  
    19    stop() {
    20      const reader = this.reader;
    21      if (reader) {
    22        reader.cancel();
    23      }
    24      return this.poll.cancelAll();
    25    },
    26  
    27    poll: task(function*() {
    28      const url = this.fullUrl;
    29      const logFetch = this.logFetch;
    30  
    31      const reader = yield logFetch(url).then(res => {
    32        const reader = res.body.getReader();
    33        // It's possible that the logger was stopped between the time
    34        // polling was started and the log request responded.
    35        // If the logger was stopped, the reader needs to be immediately
    36        // canceled to prevent an endless request running in the background.
    37        if (this.poll.isRunning) {
    38          return reader;
    39        }
    40        reader.cancel();
    41      }, fetchFailure(url));
    42  
    43      if (!reader) {
    44        return;
    45      }
    46  
    47      this.set('reader', reader);
    48  
    49      let streamClosed = false;
    50      let buffer = '';
    51      const decoder = new TextDecoder();
    52  
    53      while (!streamClosed) {
    54        yield reader.read().then(({ value, done }) => {
    55          streamClosed = done;
    56  
    57          // There is no guarantee that value will be a complete JSON object,
    58          // so it needs to be buffered.
    59          buffer += decoder.decode(value, { stream: true });
    60  
    61          // Only when the buffer contains a close bracket can we be sure the buffer
    62          // is in a complete state
    63          if (buffer.indexOf('}') !== -1) {
    64            // The buffer can be one or more complete frames with additional text for the
    65            // next frame
    66            const [, chunk, newBuffer] = buffer.match(/(.*\})(.*)$/);
    67  
    68            // Peel chunk off the front of the buffer (since it represents complete frames)
    69            // and set the buffer to be the remainder
    70            buffer = newBuffer;
    71  
    72            // Assuming the logs endpoint never returns nested JSON (it shouldn't), at this
    73            // point chunk is a series of valid JSON objects with no delimiter.
    74            const { offset, message } = decode(chunk);
    75            if (message) {
    76              this.set('endOffset', offset);
    77              this.write(message);
    78            }
    79          }
    80        });
    81      }
    82    }),
    83  }).reopenClass({
    84    isSupported: !!window.ReadableStream && !isSafari(),
    85  });
    86  
    87  // Fetch streaming doesn't work in Safari yet despite all the primitives being in place.
    88  // Bug: https://bugs.webkit.org/show_bug.cgi?id=185924
    89  // Until this is fixed, Safari needs to be explicitly targeted for poll-based logging.
    90  function isSafari() {
    91    const oldSafariTest = /constructor/i.test(window.HTMLElement);
    92    const newSafariTest = (function(p) {
    93      return p.toString() === '[object SafariRemoteNotification]';
    94    })(!window['safari'] || (typeof window.safari !== 'undefined' && window.safari.pushNotification));
    95    return oldSafariTest || newSafariTest;
    96  }