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