textiot
Version:
A framework for building web and native (IoT) Dapps on the IPFS network
130 lines (122 loc) • 3.67 kB
text/typescript
import { Readable } from 'stream'
import { ReadableStream } from 'web-streams-polyfill/ponyfill'
// polyfill TextDecoder to be backward compatible with older
// nodejs that doesn't expose TextDecoder as a global variable
if (typeof TextDecoder === 'undefined' && typeof require !== 'undefined') {
(global as any).TextDecoder = require('util').TextDecoder
}
// https://github.com/gwicke/node-web-streams
export const readableNodeToWeb = <T>(nodeStream: Readable | ReadableStream<T>) => {
if (!(nodeStream instanceof Readable)) {
return nodeStream
}
return new ReadableStream<T>({
start(controller) {
nodeStream.pause()
nodeStream.on('data', (chunk: T) => {
controller.enqueue(chunk)
nodeStream.pause()
})
nodeStream.on('end', () => controller.close())
nodeStream.on('error', (e: Error) => controller.error(e))
},
pull(_controller) {
nodeStream.resume()
},
cancel(_reason) {
nodeStream.pause()
}
})
}
export class NodeReadable<T> extends Readable {
_webStream: ReadableStream
_reader: ReadableStreamDefaultReader
_reading: boolean
constructor(webStream: ReadableStream<T>, options?: {}) {
super(options)
this._webStream = webStream
this._reader = webStream.getReader()
this._reading = false
}
_read(size: number) {
if (this._reading) {
return
}
this._reading = true
const doRead = (size: number) => {
this._reader.read()
.then((res) => {
if (res.done) {
// tslint:disable-next-line:no-null-keyword
this.push(null)
return
}
if (this.push(res.value)) {
return doRead(size)
} else {
this._reading = false
}
})
}
doRead(size)
}
}
export const readableWebToNode = <T>(webStream: ReadableStream<T>) => {
return new NodeReadable(webStream)
}
// https://github.com/canjs/can-ndjson-stream
export const streamHandler = <T>(response: ReadableStream<ArrayBuffer>) => {
// For cancellation
let isReader: any
let cancellationRequest = false
return new ReadableStream<T>({
start(controller) {
const reader = readableNodeToWeb(response).getReader()
isReader = reader
const decoder = new TextDecoder()
let dataBuffer = ''
const processResult = (result: ReadableStreamReadResult<ArrayBuffer>) => {
if (result.done) {
if (cancellationRequest) {
return // Immediately exit
}
dataBuffer = dataBuffer.trim()
if (dataBuffer.length !== 0) {
try {
const dataLine: T = JSON.parse(dataBuffer)
controller.enqueue(dataLine)
} catch (e) {
controller.error(e)
return
}
}
controller.close()
return
}
const data = decoder.decode(result.value, { stream: true })
dataBuffer += data
const lines = dataBuffer.split('\n')
for (const line of lines) {
const l = line.trim()
if (l.length > 0) {
try {
controller.enqueue(JSON.parse(l))
} catch (e) {
controller.error(e)
cancellationRequest = true
reader.cancel(undefined)
return
}
}
}
dataBuffer = lines.length > 1 ? lines[lines.length - 1] : ''
reader.read().then(processResult)
}
reader.read().then(processResult)
},
cancel(reason?: string) {
cancellationRequest = true
isReader.cancel(reason)
}
})
}