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 }