github.com/blixtra/nomad@v0.7.2-0.20171221000451-da9a1d7bb050/ui/app/utils/classes/stream-logger.js (about) 1 import Ember from 'ember'; 2 import { task } from 'ember-concurrency'; 3 import TextDecoder from 'nomad-ui/utils/classes/text-decoder'; 4 import AbstractLogger from './abstract-logger'; 5 6 const { Object: EmberObject, computed } = Ember; 7 8 export default EmberObject.extend(AbstractLogger, { 9 reader: null, 10 11 additionalParams: computed(() => ({ 12 follow: true, 13 })), 14 15 start() { 16 return this.get('poll').perform(); 17 }, 18 19 stop() { 20 const reader = this.get('reader'); 21 if (reader) { 22 reader.cancel(); 23 } 24 return this.get('poll').cancelAll(); 25 }, 26 27 poll: task(function*() { 28 const url = this.get('fullUrl'); 29 const logFetch = this.get('logFetch'); 30 31 let streamClosed = false; 32 let buffer = ''; 33 34 const decoder = new TextDecoder(); 35 const reader = yield logFetch(url).then(res => res.body.getReader()); 36 37 this.set('reader', reader); 38 39 while (!streamClosed) { 40 yield reader.read().then(({ value, done }) => { 41 streamClosed = done; 42 43 // There is no guarantee that value will be a complete JSON object, 44 // so it needs to be buffered. 45 buffer += decoder.decode(value, { stream: true }); 46 47 // Only when the buffer contains a close bracket can we be sure the buffer 48 // is in a complete state 49 if (buffer.indexOf('}') !== -1) { 50 // The buffer can be one or more complete frames with additional text for the 51 // next frame 52 const [, chunk, newBuffer] = buffer.match(/(.*\})(.*)$/); 53 54 // Peel chunk off the front of the buffer (since it represents complete frames) 55 // and set the buffer to be the remainder 56 buffer = newBuffer; 57 58 // Assuming the logs endpoint never returns nested JSON (it shouldn't), at this 59 // point chunk is a series of valid JSON objects with no delimiter. 60 const lines = chunk.replace(/\}\{/g, '}\n{').split('\n'); 61 const frames = lines.map(line => JSON.parse(line)).filter(frame => frame.Data); 62 63 if (frames.length) { 64 frames.forEach(frame => (frame.Data = window.atob(frame.Data))); 65 this.set('endOffset', frames[frames.length - 1].Offset); 66 this.get('write')(frames.mapBy('Data').join('')); 67 } 68 } 69 }); 70 } 71 }), 72 }).reopenClass({ 73 isSupported: !!window.ReadableStream, 74 });