├── .glitch-assets ├── README.md ├── package.json ├── private └── streams │ ├── byte-length-queuing-strategy.js │ ├── count-queuing-strategy.js │ ├── helpers.js │ ├── queue-with-sizes.js │ ├── readable-stream.js │ ├── transform-stream.js │ ├── utils.js │ └── writable-stream.js ├── public ├── assets │ └── templates │ │ ├── body.html │ │ ├── foot.html │ │ ├── head.html │ │ └── item.html ├── data │ └── columns.json ├── manifest.json ├── scripts │ ├── DOMParser.js │ ├── dot.js │ ├── platform │ │ ├── common.js │ │ ├── node.js │ │ └── web.js │ ├── router.js │ └── routes │ │ ├── index.js │ │ ├── proxy.js │ │ └── root.js └── sw.js └── server.js /.glitch-assets: -------------------------------------------------------------------------------- 1 | {"name":"drag-in-files.svg","date":"2016-10-22T16:17:49.954Z","url":"https://cdn.hyperdev.com/drag-in-files.svg","type":"image/svg","size":7646,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/drag-in-files.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(102, 153, 205)","uuid":"adSBq97hhhpFNUna"} 2 | {"name":"click-me.svg","date":"2016-10-23T16:17:49.954Z","url":"https://cdn.hyperdev.com/click-me.svg","type":"image/svg","size":7116,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/click-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(243, 185, 186)","uuid":"adSBq97hhhpFNUnb"} 3 | {"name":"paste-me.svg","date":"2016-10-24T16:17:49.954Z","url":"https://cdn.hyperdev.com/paste-me.svg","type":"image/svg","size":7242,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/paste-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(42, 179, 185)","uuid":"adSBq97hhhpFNUnc"} 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Feeddeck 💯 2 | ========================= 3 | 4 | Inspired by River of news, but HTTPs only and a PWA. 5 | 6 | Attempt to create a PWA that is a news reader that renders on the server and on the client using all the same templating and logic. 7 | 8 | 9 | Progress 10 | -------- 11 | 12 | * Client logic in SW is now shared with the Server logic [done] 13 | * Templating is done via streaming templates [done] 14 | * Use WhatWG streams in node. [done] 15 | * Need to clean up a lot. [errrr.....] 16 | * Configuration file should really be an OPML file. [todo] 17 | * Cache fetched data on the client and server [done] 18 | * To render the client it still needs JS turned on. Investigate server 19 | load of RSS feeds. 20 | * This is done in the SW, will use on Server soon. 21 | 22 | 23 | Thoughts 24 | -------- 25 | 26 | If possible the server logic and the client logic should be near exactly the same. 27 | 28 | Ideally configured via a OPML file that defines the columns, that links to other OPML files that contain the feeds that should be aggregrated in this. 29 | 30 | Thoughts: 31 | 32 | * The UI should render the structure without JS. Ideally after this has been 33 | rendered, I can stream in the first batch of aggreagated and merged feeds. 34 | * The UI should then take over and update the feeds in the client as much as 35 | possible. 36 | * On reload, the server should not be hit at all, save for any updates to the 37 | OPML file. In an ideal world, the SW would be doing the work. 38 | 39 | 40 | Technical hitches: 41 | We are going to have to proxy the RSS feed requests. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feeddeck", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "node server.js" 6 | }, 7 | "dependencies": { 8 | "express": "^4.15.2", 9 | "node-fetch": "*", 10 | "text-encoding": "*", 11 | "xmldom-alpha": "*" 12 | }, 13 | "engines": { 14 | "node": "6.9.x" 15 | }, 16 | "license": "Apache-2.0", 17 | "keywords": [ 18 | "node", 19 | "glitch", 20 | "express" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /private/streams/byte-length-queuing-strategy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { createDataProperty } = require('./helpers.js'); 3 | 4 | module.exports = class ByteLengthQueuingStrategy { 5 | constructor({ highWaterMark }) { 6 | createDataProperty(this, 'highWaterMark', highWaterMark); 7 | } 8 | 9 | size(chunk) { 10 | return chunk.byteLength; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /private/streams/count-queuing-strategy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { createDataProperty } = require('./helpers.js'); 3 | 4 | module.exports = class CountQueuingStrategy { 5 | constructor({ highWaterMark }) { 6 | createDataProperty(this, 'highWaterMark', highWaterMark); 7 | } 8 | 9 | size() { 10 | return 1; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /private/streams/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | 4 | function IsPropertyKey(argument) { 5 | return typeof argument === 'string' || typeof argument === 'symbol'; 6 | } 7 | 8 | exports.typeIsObject = x => (typeof x === 'object' && x !== null) || typeof x === 'function'; 9 | 10 | exports.createDataProperty = (o, p, v) => { 11 | assert(exports.typeIsObject(o)); 12 | Object.defineProperty(o, p, { value: v, writable: true, enumerable: true, configurable: true }); 13 | }; 14 | 15 | exports.createArrayFromList = elements => { 16 | // We use arrays to represent lists, so this is basically a no-op. 17 | // Do a slice though just in case we happen to depend on the unique-ness. 18 | return elements.slice(); 19 | }; 20 | 21 | exports.ArrayBufferCopy = (dest, destOffset, src, srcOffset, n) => { 22 | new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset); 23 | }; 24 | 25 | exports.CreateIterResultObject = (value, done) => { 26 | assert(typeof done === 'boolean'); 27 | const obj = {}; 28 | Object.defineProperty(obj, 'value', { value, enumerable: true, writable: true, configurable: true }); 29 | Object.defineProperty(obj, 'done', { value: done, enumerable: true, writable: true, configurable: true }); 30 | return obj; 31 | }; 32 | 33 | exports.IsFiniteNonNegativeNumber = v => { 34 | if (Number.isNaN(v)) { 35 | return false; 36 | } 37 | if (v === Infinity) { 38 | return false; 39 | } 40 | if (v < 0) { 41 | return false; 42 | } 43 | 44 | return true; 45 | }; 46 | 47 | function Call(F, V, args) { 48 | if (typeof F !== 'function') { 49 | throw new TypeError('Argument is not a function'); 50 | } 51 | 52 | return Function.prototype.apply.call(F, V, args); 53 | } 54 | 55 | exports.InvokeOrNoop = (O, P, args) => { 56 | assert(O !== undefined); 57 | assert(IsPropertyKey(P)); 58 | assert(Array.isArray(args)); 59 | 60 | const method = O[P]; 61 | if (method === undefined) { 62 | return undefined; 63 | } 64 | 65 | return Call(method, O, args); 66 | }; 67 | 68 | exports.PromiseInvokeOrNoop = (O, P, args) => { 69 | assert(O !== undefined); 70 | assert(IsPropertyKey(P)); 71 | assert(Array.isArray(args)); 72 | try { 73 | return Promise.resolve(exports.InvokeOrNoop(O, P, args)); 74 | } catch (returnValueE) { 75 | return Promise.reject(returnValueE); 76 | } 77 | }; 78 | 79 | exports.PromiseInvokeOrPerformFallback = (O, P, args, F, argsF) => { 80 | assert(O !== undefined); 81 | assert(IsPropertyKey(P)); 82 | assert(Array.isArray(args)); 83 | assert(Array.isArray(argsF)); 84 | 85 | let method; 86 | try { 87 | method = O[P]; 88 | } catch (methodE) { 89 | return Promise.reject(methodE); 90 | } 91 | 92 | if (method === undefined) { 93 | return F(...argsF); 94 | } 95 | 96 | try { 97 | return Promise.resolve(Call(method, O, args)); 98 | } catch (e) { 99 | return Promise.reject(e); 100 | } 101 | }; 102 | 103 | // Not implemented correctly 104 | exports.TransferArrayBuffer = O => O.slice(); 105 | 106 | exports.ValidateAndNormalizeHighWaterMark = highWaterMark => { 107 | highWaterMark = Number(highWaterMark); 108 | if (Number.isNaN(highWaterMark) || highWaterMark < 0) { 109 | throw new RangeError('highWaterMark property of a queuing strategy must be non-negative and non-NaN'); 110 | } 111 | 112 | return highWaterMark; 113 | }; 114 | 115 | exports.ValidateAndNormalizeQueuingStrategy = (size, highWaterMark) => { 116 | if (size !== undefined && typeof size !== 'function') { 117 | throw new TypeError('size property of a queuing strategy must be a function'); 118 | } 119 | 120 | highWaterMark = exports.ValidateAndNormalizeHighWaterMark(highWaterMark); 121 | 122 | return { size, highWaterMark }; 123 | }; 124 | -------------------------------------------------------------------------------- /private/streams/queue-with-sizes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const { IsFiniteNonNegativeNumber } = require('./helpers.js'); 4 | 5 | exports.DequeueValue = container => { 6 | assert('_queue' in container && '_queueTotalSize' in container, 7 | 'Spec-level failure: DequeueValue should only be used on containers with [[queue]] and [[queueTotalSize]].'); 8 | assert(container._queue.length > 0, 'Spec-level failure: should never dequeue from an empty queue.'); 9 | 10 | const pair = container._queue.shift(); 11 | container._queueTotalSize -= pair.size; 12 | if (container._queueTotalSize < 0) { 13 | container._queueTotalSize = 0; 14 | } 15 | 16 | return pair.value; 17 | }; 18 | 19 | exports.EnqueueValueWithSize = (container, value, size) => { 20 | assert('_queue' in container && '_queueTotalSize' in container, 21 | 'Spec-level failure: EnqueueValueWithSize should only be used on containers with [[queue]] and ' + 22 | '[[queueTotalSize]].'); 23 | 24 | size = Number(size); 25 | if (!IsFiniteNonNegativeNumber(size)) { 26 | throw new RangeError('Size must be a finite, non-NaN, non-negative number.'); 27 | } 28 | 29 | container._queue.push({ value, size }); 30 | container._queueTotalSize += size; 31 | }; 32 | 33 | exports.PeekQueueValue = container => { 34 | assert('_queue' in container && '_queueTotalSize' in container, 35 | 'Spec-level failure: PeekQueueValue should only be used on containers with [[queue]] and [[queueTotalSize]].'); 36 | assert(container._queue.length > 0, 'Spec-level failure: should never peek at an empty queue.'); 37 | 38 | const pair = container._queue[0]; 39 | return pair.value; 40 | }; 41 | 42 | exports.ResetQueue = container => { 43 | assert('_queue' in container && '_queueTotalSize' in container, 44 | 'Spec-level failure: ResetQueue should only be used on containers with [[queue]] and [[queueTotalSize]].'); 45 | 46 | container._queue = []; 47 | container._queueTotalSize = 0; 48 | }; 49 | -------------------------------------------------------------------------------- /private/streams/readable-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const { ArrayBufferCopy, CreateIterResultObject, IsFiniteNonNegativeNumber, InvokeOrNoop, PromiseInvokeOrNoop, 4 | TransferArrayBuffer, ValidateAndNormalizeQueuingStrategy, ValidateAndNormalizeHighWaterMark } = 5 | require('./helpers.js'); 6 | const { createArrayFromList, createDataProperty, typeIsObject } = require('./helpers.js'); 7 | const { rethrowAssertionErrorRejection } = require('./utils.js'); 8 | const { DequeueValue, EnqueueValueWithSize, ResetQueue } = require('./queue-with-sizes.js'); 9 | const { AcquireWritableStreamDefaultWriter, IsWritableStream, IsWritableStreamLocked, 10 | WritableStreamAbort, WritableStreamDefaultWriterCloseWithErrorPropagation, 11 | WritableStreamDefaultWriterRelease, WritableStreamDefaultWriterWrite, WritableStreamCloseQueuedOrInFlight } = 12 | require('./writable-stream.js'); 13 | 14 | const CancelSteps = Symbol('[[CancelSteps]]'); 15 | const PullSteps = Symbol('[[PullSteps]]'); 16 | 17 | class ReadableStream { 18 | constructor(underlyingSource = {}, { size, highWaterMark } = {}) { 19 | // Exposed to controllers. 20 | this._state = 'readable'; 21 | 22 | this._reader = undefined; 23 | this._storedError = undefined; 24 | 25 | this._disturbed = false; 26 | 27 | // Initialize to undefined first because the constructor of the controller checks this 28 | // variable to validate the caller. 29 | this._readableStreamController = undefined; 30 | const type = underlyingSource.type; 31 | const typeString = String(type); 32 | if (typeString === 'bytes') { 33 | if (highWaterMark === undefined) { 34 | highWaterMark = 0; 35 | } 36 | this._readableStreamController = new ReadableByteStreamController(this, underlyingSource, highWaterMark); 37 | } else if (type === undefined) { 38 | if (highWaterMark === undefined) { 39 | highWaterMark = 1; 40 | } 41 | this._readableStreamController = new ReadableStreamDefaultController(this, underlyingSource, size, highWaterMark); 42 | } else { 43 | throw new RangeError('Invalid type is specified'); 44 | } 45 | } 46 | 47 | get locked() { 48 | if (IsReadableStream(this) === false) { 49 | throw streamBrandCheckException('locked'); 50 | } 51 | 52 | return IsReadableStreamLocked(this); 53 | } 54 | 55 | cancel(reason) { 56 | if (IsReadableStream(this) === false) { 57 | return Promise.reject(streamBrandCheckException('cancel')); 58 | } 59 | 60 | if (IsReadableStreamLocked(this) === true) { 61 | return Promise.reject(new TypeError('Cannot cancel a stream that already has a reader')); 62 | } 63 | 64 | return ReadableStreamCancel(this, reason); 65 | } 66 | 67 | getReader({ mode } = {}) { 68 | if (IsReadableStream(this) === false) { 69 | throw streamBrandCheckException('getReader'); 70 | } 71 | 72 | if (mode === undefined) { 73 | return AcquireReadableStreamDefaultReader(this); 74 | } 75 | 76 | mode = String(mode); 77 | 78 | if (mode === 'byob') { 79 | return AcquireReadableStreamBYOBReader(this); 80 | } 81 | 82 | throw new RangeError('Invalid mode is specified'); 83 | } 84 | 85 | pipeThrough({ writable, readable }, options) { 86 | const promise = this.pipeTo(writable, options); 87 | 88 | ifIsObjectAndHasAPromiseIsHandledInternalSlotSetPromiseIsHandledToTrue(promise); 89 | 90 | return readable; 91 | } 92 | 93 | pipeTo(dest, { preventClose, preventAbort, preventCancel } = {}) { 94 | if (IsReadableStream(this) === false) { 95 | return Promise.reject(streamBrandCheckException('pipeTo')); 96 | } 97 | if (IsWritableStream(dest) === false) { 98 | return Promise.reject( 99 | new TypeError('ReadableStream.prototype.pipeTo\'s first argument must be a WritableStream')); 100 | } 101 | 102 | preventClose = Boolean(preventClose); 103 | preventAbort = Boolean(preventAbort); 104 | preventCancel = Boolean(preventCancel); 105 | 106 | if (IsReadableStreamLocked(this) === true) { 107 | return Promise.reject(new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream')); 108 | } 109 | if (IsWritableStreamLocked(dest) === true) { 110 | return Promise.reject(new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream')); 111 | } 112 | 113 | const reader = AcquireReadableStreamDefaultReader(this); 114 | const writer = AcquireWritableStreamDefaultWriter(dest); 115 | 116 | let shuttingDown = false; 117 | 118 | // This is used to keep track of the spec's requirement that we wait for ongoing writes during shutdown. 119 | let currentWrite = Promise.resolve(); 120 | 121 | return new Promise((resolve, reject) => { 122 | // Using reader and writer, read all chunks from this and write them to dest 123 | // - Backpressure must be enforced 124 | // - Shutdown must stop all activity 125 | function pipeLoop() { 126 | currentWrite = Promise.resolve(); 127 | 128 | if (shuttingDown === true) { 129 | return Promise.resolve(); 130 | } 131 | 132 | return writer._readyPromise.then(() => { 133 | return ReadableStreamDefaultReaderRead(reader).then(({ value, done }) => { 134 | if (done === true) { 135 | return; 136 | } 137 | 138 | currentWrite = WritableStreamDefaultWriterWrite(writer, value).catch(() => {}); 139 | }); 140 | }) 141 | .then(pipeLoop); 142 | } 143 | 144 | // Errors must be propagated forward 145 | isOrBecomesErrored(this, reader._closedPromise, storedError => { 146 | if (preventAbort === false) { 147 | shutdownWithAction(() => WritableStreamAbort(dest, storedError), true, storedError); 148 | } else { 149 | shutdown(true, storedError); 150 | } 151 | }); 152 | 153 | // Errors must be propagated backward 154 | isOrBecomesErrored(dest, writer._closedPromise, storedError => { 155 | if (preventCancel === false) { 156 | shutdownWithAction(() => ReadableStreamCancel(this, storedError), true, storedError); 157 | } else { 158 | shutdown(true, storedError); 159 | } 160 | }); 161 | 162 | // Closing must be propagated forward 163 | isOrBecomesClosed(this, reader._closedPromise, () => { 164 | if (preventClose === false) { 165 | shutdownWithAction(() => WritableStreamDefaultWriterCloseWithErrorPropagation(writer)); 166 | } else { 167 | shutdown(); 168 | } 169 | }); 170 | 171 | // Closing must be propagated backward 172 | if (WritableStreamCloseQueuedOrInFlight(dest) === true || dest._state === 'closed') { 173 | const destClosed = new TypeError('the destination writable stream closed before all data could be piped to it'); 174 | 175 | if (preventCancel === false) { 176 | shutdownWithAction(() => ReadableStreamCancel(this, destClosed), true, destClosed); 177 | } else { 178 | shutdown(true, destClosed); 179 | } 180 | } 181 | 182 | pipeLoop().catch(err => { 183 | currentWrite = Promise.resolve(); 184 | rethrowAssertionErrorRejection(err); 185 | }); 186 | 187 | function waitForWritesToFinish() { 188 | // Another write may have started while we were waiting on this currentWrite, so we have to be sure to wait 189 | // for that too. 190 | const oldCurrentWrite = currentWrite; 191 | return currentWrite.then(() => oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : undefined); 192 | } 193 | 194 | function isOrBecomesErrored(stream, promise, action) { 195 | if (stream._state === 'errored') { 196 | action(stream._storedError); 197 | } else { 198 | promise.catch(action).catch(rethrowAssertionErrorRejection); 199 | } 200 | } 201 | 202 | function isOrBecomesClosed(stream, promise, action) { 203 | if (stream._state === 'closed') { 204 | action(); 205 | } else { 206 | promise.then(action).catch(rethrowAssertionErrorRejection); 207 | } 208 | } 209 | 210 | function shutdownWithAction(action, originalIsError, originalError) { 211 | if (shuttingDown === true) { 212 | return; 213 | } 214 | shuttingDown = true; 215 | 216 | if (dest._state === 'writable' && WritableStreamCloseQueuedOrInFlight(dest) === false) { 217 | waitForWritesToFinish().then(doTheRest); 218 | } else { 219 | doTheRest(); 220 | } 221 | 222 | function doTheRest() { 223 | action().then( 224 | () => finalize(originalIsError, originalError), 225 | newError => finalize(true, newError) 226 | ) 227 | .catch(rethrowAssertionErrorRejection); 228 | } 229 | } 230 | 231 | function shutdown(isError, error) { 232 | if (shuttingDown === true) { 233 | return; 234 | } 235 | shuttingDown = true; 236 | 237 | if (dest._state === 'writable' && WritableStreamCloseQueuedOrInFlight(dest) === false) { 238 | waitForWritesToFinish().then(() => finalize(isError, error)).catch(rethrowAssertionErrorRejection); 239 | } else { 240 | finalize(isError, error); 241 | } 242 | } 243 | 244 | function finalize(isError, error) { 245 | WritableStreamDefaultWriterRelease(writer); 246 | ReadableStreamReaderGenericRelease(reader); 247 | 248 | if (isError) { 249 | reject(error); 250 | } else { 251 | resolve(undefined); 252 | } 253 | } 254 | }); 255 | } 256 | 257 | tee() { 258 | if (IsReadableStream(this) === false) { 259 | throw streamBrandCheckException('tee'); 260 | } 261 | 262 | const branches = ReadableStreamTee(this, false); 263 | return createArrayFromList(branches); 264 | } 265 | } 266 | 267 | module.exports = { 268 | ReadableStream, 269 | IsReadableStreamDisturbed, 270 | ReadableStreamDefaultControllerClose, 271 | ReadableStreamDefaultControllerEnqueue, 272 | ReadableStreamDefaultControllerError, 273 | ReadableStreamDefaultControllerGetDesiredSize 274 | }; 275 | 276 | // Abstract operations for the ReadableStream. 277 | 278 | function AcquireReadableStreamBYOBReader(stream) { 279 | return new ReadableStreamBYOBReader(stream); 280 | } 281 | 282 | function AcquireReadableStreamDefaultReader(stream) { 283 | return new ReadableStreamDefaultReader(stream); 284 | } 285 | 286 | function IsReadableStream(x) { 287 | if (!typeIsObject(x)) { 288 | return false; 289 | } 290 | 291 | if (!Object.prototype.hasOwnProperty.call(x, '_readableStreamController')) { 292 | return false; 293 | } 294 | 295 | return true; 296 | } 297 | 298 | function IsReadableStreamDisturbed(stream) { 299 | assert(IsReadableStream(stream) === true, 'IsReadableStreamDisturbed should only be used on known readable streams'); 300 | 301 | return stream._disturbed; 302 | } 303 | 304 | function IsReadableStreamLocked(stream) { 305 | assert(IsReadableStream(stream) === true, 'IsReadableStreamLocked should only be used on known readable streams'); 306 | 307 | if (stream._reader === undefined) { 308 | return false; 309 | } 310 | 311 | return true; 312 | } 313 | 314 | function ReadableStreamTee(stream, cloneForBranch2) { 315 | assert(IsReadableStream(stream) === true); 316 | assert(typeof cloneForBranch2 === 'boolean'); 317 | 318 | const reader = AcquireReadableStreamDefaultReader(stream); 319 | 320 | const teeState = { 321 | closedOrErrored: false, 322 | canceled1: false, 323 | canceled2: false, 324 | reason1: undefined, 325 | reason2: undefined 326 | }; 327 | teeState.promise = new Promise(resolve => { 328 | teeState._resolve = resolve; 329 | }); 330 | 331 | const pull = create_ReadableStreamTeePullFunction(); 332 | pull._reader = reader; 333 | pull._teeState = teeState; 334 | pull._cloneForBranch2 = cloneForBranch2; 335 | 336 | const cancel1 = create_ReadableStreamTeeBranch1CancelFunction(); 337 | cancel1._stream = stream; 338 | cancel1._teeState = teeState; 339 | 340 | const cancel2 = create_ReadableStreamTeeBranch2CancelFunction(); 341 | cancel2._stream = stream; 342 | cancel2._teeState = teeState; 343 | 344 | const underlyingSource1 = Object.create(Object.prototype); 345 | createDataProperty(underlyingSource1, 'pull', pull); 346 | createDataProperty(underlyingSource1, 'cancel', cancel1); 347 | const branch1Stream = new ReadableStream(underlyingSource1); 348 | 349 | const underlyingSource2 = Object.create(Object.prototype); 350 | createDataProperty(underlyingSource2, 'pull', pull); 351 | createDataProperty(underlyingSource2, 'cancel', cancel2); 352 | const branch2Stream = new ReadableStream(underlyingSource2); 353 | 354 | pull._branch1 = branch1Stream._readableStreamController; 355 | pull._branch2 = branch2Stream._readableStreamController; 356 | 357 | reader._closedPromise.catch(r => { 358 | if (teeState.closedOrErrored === true) { 359 | return; 360 | } 361 | 362 | ReadableStreamDefaultControllerError(pull._branch1, r); 363 | ReadableStreamDefaultControllerError(pull._branch2, r); 364 | teeState.closedOrErrored = true; 365 | }); 366 | 367 | return [branch1Stream, branch2Stream]; 368 | } 369 | 370 | function create_ReadableStreamTeePullFunction() { 371 | function f() { 372 | const { _reader: reader, _branch1: branch1, _branch2: branch2, _teeState: teeState/* , 373 | _cloneForBranch2: cloneForBranch2*/ } = f; 374 | 375 | return ReadableStreamDefaultReaderRead(reader).then(result => { 376 | assert(typeIsObject(result)); 377 | const value = result.value; 378 | const done = result.done; 379 | assert(typeof done === 'boolean'); 380 | 381 | if (done === true && teeState.closedOrErrored === false) { 382 | if (teeState.canceled1 === false) { 383 | ReadableStreamDefaultControllerClose(branch1); 384 | } 385 | if (teeState.canceled2 === false) { 386 | ReadableStreamDefaultControllerClose(branch2); 387 | } 388 | teeState.closedOrErrored = true; 389 | } 390 | 391 | if (teeState.closedOrErrored === true) { 392 | return; 393 | } 394 | 395 | const value1 = value; 396 | const value2 = value; 397 | 398 | // There is no way to access the cloning code right now in the reference implementation. 399 | // If we add one then we'll need an implementation for serializable objects. 400 | // if (teeState.canceled2 === false && cloneForBranch2 === true) { 401 | // value2 = StructuredDeserialize(StructuredSerialize(value2)); 402 | // } 403 | 404 | if (teeState.canceled1 === false) { 405 | ReadableStreamDefaultControllerEnqueue(branch1, value1); 406 | } 407 | 408 | if (teeState.canceled2 === false) { 409 | ReadableStreamDefaultControllerEnqueue(branch2, value2); 410 | } 411 | }); 412 | } 413 | return f; 414 | } 415 | 416 | function create_ReadableStreamTeeBranch1CancelFunction() { 417 | function f(reason) { 418 | const { _stream: stream, _teeState: teeState } = f; 419 | 420 | teeState.canceled1 = true; 421 | teeState.reason1 = reason; 422 | if (teeState.canceled2 === true) { 423 | const compositeReason = createArrayFromList([teeState.reason1, teeState.reason2]); 424 | const cancelResult = ReadableStreamCancel(stream, compositeReason); 425 | teeState._resolve(cancelResult); 426 | } 427 | return teeState.promise; 428 | } 429 | return f; 430 | } 431 | 432 | function create_ReadableStreamTeeBranch2CancelFunction() { 433 | function f(reason) { 434 | const { _stream: stream, _teeState: teeState } = f; 435 | 436 | teeState.canceled2 = true; 437 | teeState.reason2 = reason; 438 | if (teeState.canceled1 === true) { 439 | const compositeReason = createArrayFromList([teeState.reason1, teeState.reason2]); 440 | const cancelResult = ReadableStreamCancel(stream, compositeReason); 441 | teeState._resolve(cancelResult); 442 | } 443 | return teeState.promise; 444 | } 445 | return f; 446 | } 447 | 448 | // ReadableStream API exposed for controllers. 449 | 450 | function ReadableStreamAddReadIntoRequest(stream) { 451 | assert(IsReadableStreamBYOBReader(stream._reader) === true); 452 | assert(stream._state === 'readable' || stream._state === 'closed'); 453 | 454 | const promise = new Promise((resolve, reject) => { 455 | const readIntoRequest = { 456 | _resolve: resolve, 457 | _reject: reject 458 | }; 459 | 460 | stream._reader._readIntoRequests.push(readIntoRequest); 461 | }); 462 | 463 | return promise; 464 | } 465 | 466 | function ReadableStreamAddReadRequest(stream) { 467 | assert(IsReadableStreamDefaultReader(stream._reader) === true); 468 | assert(stream._state === 'readable'); 469 | 470 | const promise = new Promise((resolve, reject) => { 471 | const readRequest = { 472 | _resolve: resolve, 473 | _reject: reject 474 | }; 475 | 476 | stream._reader._readRequests.push(readRequest); 477 | }); 478 | 479 | return promise; 480 | } 481 | 482 | function ReadableStreamCancel(stream, reason) { 483 | stream._disturbed = true; 484 | 485 | if (stream._state === 'closed') { 486 | return Promise.resolve(undefined); 487 | } 488 | if (stream._state === 'errored') { 489 | return Promise.reject(stream._storedError); 490 | } 491 | 492 | ReadableStreamClose(stream); 493 | 494 | const sourceCancelPromise = stream._readableStreamController[CancelSteps](reason); 495 | return sourceCancelPromise.then(() => undefined); 496 | } 497 | 498 | function ReadableStreamClose(stream) { 499 | assert(stream._state === 'readable'); 500 | 501 | stream._state = 'closed'; 502 | 503 | const reader = stream._reader; 504 | 505 | if (reader === undefined) { 506 | return undefined; 507 | } 508 | 509 | if (IsReadableStreamDefaultReader(reader) === true) { 510 | for (const { _resolve } of reader._readRequests) { 511 | _resolve(CreateIterResultObject(undefined, true)); 512 | } 513 | reader._readRequests = []; 514 | } 515 | 516 | defaultReaderClosedPromiseResolve(reader); 517 | 518 | return undefined; 519 | } 520 | 521 | function ReadableStreamError(stream, e) { 522 | assert(IsReadableStream(stream) === true, 'stream must be ReadableStream'); 523 | assert(stream._state === 'readable', 'state must be readable'); 524 | 525 | stream._state = 'errored'; 526 | stream._storedError = e; 527 | 528 | const reader = stream._reader; 529 | 530 | if (reader === undefined) { 531 | return undefined; 532 | } 533 | 534 | if (IsReadableStreamDefaultReader(reader) === true) { 535 | for (const readRequest of reader._readRequests) { 536 | readRequest._reject(e); 537 | } 538 | 539 | reader._readRequests = []; 540 | } else { 541 | assert(IsReadableStreamBYOBReader(reader), 'reader must be ReadableStreamBYOBReader'); 542 | 543 | for (const readIntoRequest of reader._readIntoRequests) { 544 | readIntoRequest._reject(e); 545 | } 546 | 547 | reader._readIntoRequests = []; 548 | } 549 | 550 | defaultReaderClosedPromiseReject(reader, e); 551 | reader._closedPromise.catch(() => {}); 552 | } 553 | 554 | function ReadableStreamFulfillReadIntoRequest(stream, chunk, done) { 555 | const reader = stream._reader; 556 | 557 | assert(reader._readIntoRequests.length > 0); 558 | 559 | const readIntoRequest = reader._readIntoRequests.shift(); 560 | readIntoRequest._resolve(CreateIterResultObject(chunk, done)); 561 | } 562 | 563 | function ReadableStreamFulfillReadRequest(stream, chunk, done) { 564 | const reader = stream._reader; 565 | 566 | assert(reader._readRequests.length > 0); 567 | 568 | const readRequest = reader._readRequests.shift(); 569 | readRequest._resolve(CreateIterResultObject(chunk, done)); 570 | } 571 | 572 | function ReadableStreamGetNumReadIntoRequests(stream) { 573 | return stream._reader._readIntoRequests.length; 574 | } 575 | 576 | function ReadableStreamGetNumReadRequests(stream) { 577 | return stream._reader._readRequests.length; 578 | } 579 | 580 | function ReadableStreamHasBYOBReader(stream) { 581 | const reader = stream._reader; 582 | 583 | if (reader === undefined) { 584 | return false; 585 | } 586 | 587 | if (IsReadableStreamBYOBReader(reader) === false) { 588 | return false; 589 | } 590 | 591 | return true; 592 | } 593 | 594 | function ReadableStreamHasDefaultReader(stream) { 595 | const reader = stream._reader; 596 | 597 | if (reader === undefined) { 598 | return false; 599 | } 600 | 601 | if (IsReadableStreamDefaultReader(reader) === false) { 602 | return false; 603 | } 604 | 605 | return true; 606 | } 607 | 608 | // Readers 609 | 610 | class ReadableStreamDefaultReader { 611 | constructor(stream) { 612 | if (IsReadableStream(stream) === false) { 613 | throw new TypeError('ReadableStreamDefaultReader can only be constructed with a ReadableStream instance'); 614 | } 615 | if (IsReadableStreamLocked(stream) === true) { 616 | throw new TypeError('This stream has already been locked for exclusive reading by another reader'); 617 | } 618 | 619 | ReadableStreamReaderGenericInitialize(this, stream); 620 | 621 | this._readRequests = []; 622 | } 623 | 624 | get closed() { 625 | if (IsReadableStreamDefaultReader(this) === false) { 626 | return Promise.reject(defaultReaderBrandCheckException('closed')); 627 | } 628 | 629 | return this._closedPromise; 630 | } 631 | 632 | cancel(reason) { 633 | if (IsReadableStreamDefaultReader(this) === false) { 634 | return Promise.reject(defaultReaderBrandCheckException('cancel')); 635 | } 636 | 637 | if (this._ownerReadableStream === undefined) { 638 | return Promise.reject(readerLockException('cancel')); 639 | } 640 | 641 | return ReadableStreamReaderGenericCancel(this, reason); 642 | } 643 | 644 | read() { 645 | if (IsReadableStreamDefaultReader(this) === false) { 646 | return Promise.reject(defaultReaderBrandCheckException('read')); 647 | } 648 | 649 | if (this._ownerReadableStream === undefined) { 650 | return Promise.reject(readerLockException('read from')); 651 | } 652 | 653 | return ReadableStreamDefaultReaderRead(this); 654 | } 655 | 656 | releaseLock() { 657 | if (IsReadableStreamDefaultReader(this) === false) { 658 | throw defaultReaderBrandCheckException('releaseLock'); 659 | } 660 | 661 | if (this._ownerReadableStream === undefined) { 662 | return; 663 | } 664 | 665 | if (this._readRequests.length > 0) { 666 | throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled'); 667 | } 668 | 669 | ReadableStreamReaderGenericRelease(this); 670 | } 671 | } 672 | 673 | class ReadableStreamBYOBReader { 674 | constructor(stream) { 675 | if (!IsReadableStream(stream)) { 676 | throw new TypeError('ReadableStreamBYOBReader can only be constructed with a ReadableStream instance given a ' + 677 | 'byte source'); 678 | } 679 | if (IsReadableByteStreamController(stream._readableStreamController) === false) { 680 | throw new TypeError('Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte ' + 681 | 'source'); 682 | } 683 | if (IsReadableStreamLocked(stream)) { 684 | throw new TypeError('This stream has already been locked for exclusive reading by another reader'); 685 | } 686 | 687 | ReadableStreamReaderGenericInitialize(this, stream); 688 | 689 | this._readIntoRequests = []; 690 | } 691 | 692 | get closed() { 693 | if (!IsReadableStreamBYOBReader(this)) { 694 | return Promise.reject(byobReaderBrandCheckException('closed')); 695 | } 696 | 697 | return this._closedPromise; 698 | } 699 | 700 | cancel(reason) { 701 | if (!IsReadableStreamBYOBReader(this)) { 702 | return Promise.reject(byobReaderBrandCheckException('cancel')); 703 | } 704 | 705 | if (this._ownerReadableStream === undefined) { 706 | return Promise.reject(readerLockException('cancel')); 707 | } 708 | 709 | return ReadableStreamReaderGenericCancel(this, reason); 710 | } 711 | 712 | read(view) { 713 | if (!IsReadableStreamBYOBReader(this)) { 714 | return Promise.reject(byobReaderBrandCheckException('read')); 715 | } 716 | 717 | if (this._ownerReadableStream === undefined) { 718 | return Promise.reject(readerLockException('read from')); 719 | } 720 | 721 | if (!ArrayBuffer.isView(view)) { 722 | return Promise.reject(new TypeError('view must be an array buffer view')); 723 | } 724 | 725 | if (view.byteLength === 0) { 726 | return Promise.reject(new TypeError('view must have non-zero byteLength')); 727 | } 728 | 729 | return ReadableStreamBYOBReaderRead(this, view); 730 | } 731 | 732 | releaseLock() { 733 | if (!IsReadableStreamBYOBReader(this)) { 734 | throw byobReaderBrandCheckException('releaseLock'); 735 | } 736 | 737 | if (this._ownerReadableStream === undefined) { 738 | return; 739 | } 740 | 741 | if (this._readIntoRequests.length > 0) { 742 | throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled'); 743 | } 744 | 745 | ReadableStreamReaderGenericRelease(this); 746 | } 747 | } 748 | 749 | // Abstract operations for the readers. 750 | 751 | function IsReadableStreamBYOBReader(x) { 752 | if (!typeIsObject(x)) { 753 | return false; 754 | } 755 | 756 | if (!Object.prototype.hasOwnProperty.call(x, '_readIntoRequests')) { 757 | return false; 758 | } 759 | 760 | return true; 761 | } 762 | 763 | function IsReadableStreamDefaultReader(x) { 764 | if (!typeIsObject(x)) { 765 | return false; 766 | } 767 | 768 | if (!Object.prototype.hasOwnProperty.call(x, '_readRequests')) { 769 | return false; 770 | } 771 | 772 | return true; 773 | } 774 | 775 | function ReadableStreamReaderGenericInitialize(reader, stream) { 776 | reader._ownerReadableStream = stream; 777 | stream._reader = reader; 778 | 779 | if (stream._state === 'readable') { 780 | defaultReaderClosedPromiseInitialize(reader); 781 | } else if (stream._state === 'closed') { 782 | defaultReaderClosedPromiseInitializeAsResolved(reader); 783 | } else { 784 | assert(stream._state === 'errored', 'state must be errored'); 785 | 786 | defaultReaderClosedPromiseInitializeAsRejected(reader, stream._storedError); 787 | reader._closedPromise.catch(() => {}); 788 | } 789 | } 790 | 791 | // A client of ReadableStreamDefaultReader and ReadableStreamBYOBReader may use these functions directly to bypass state 792 | // check. 793 | 794 | function ReadableStreamReaderGenericCancel(reader, reason) { 795 | const stream = reader._ownerReadableStream; 796 | assert(stream !== undefined); 797 | return ReadableStreamCancel(stream, reason); 798 | } 799 | 800 | function ReadableStreamReaderGenericRelease(reader) { 801 | assert(reader._ownerReadableStream !== undefined); 802 | assert(reader._ownerReadableStream._reader === reader); 803 | 804 | if (reader._ownerReadableStream._state === 'readable') { 805 | defaultReaderClosedPromiseReject( 806 | reader, 807 | new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness')); 808 | } else { 809 | defaultReaderClosedPromiseResetToRejected( 810 | reader, 811 | new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness')); 812 | } 813 | reader._closedPromise.catch(() => {}); 814 | 815 | reader._ownerReadableStream._reader = undefined; 816 | reader._ownerReadableStream = undefined; 817 | } 818 | 819 | function ReadableStreamBYOBReaderRead(reader, view) { 820 | const stream = reader._ownerReadableStream; 821 | 822 | assert(stream !== undefined); 823 | 824 | stream._disturbed = true; 825 | 826 | if (stream._state === 'errored') { 827 | return Promise.reject(stream._storedError); 828 | } 829 | 830 | // Controllers must implement this. 831 | return ReadableByteStreamControllerPullInto(stream._readableStreamController, view); 832 | } 833 | 834 | function ReadableStreamDefaultReaderRead(reader) { 835 | const stream = reader._ownerReadableStream; 836 | 837 | assert(stream !== undefined); 838 | 839 | stream._disturbed = true; 840 | 841 | if (stream._state === 'closed') { 842 | return Promise.resolve(CreateIterResultObject(undefined, true)); 843 | } 844 | 845 | if (stream._state === 'errored') { 846 | return Promise.reject(stream._storedError); 847 | } 848 | 849 | assert(stream._state === 'readable'); 850 | 851 | return stream._readableStreamController[PullSteps](); 852 | } 853 | 854 | // Controllers 855 | 856 | class ReadableStreamDefaultController { 857 | constructor(stream, underlyingSource, size, highWaterMark) { 858 | if (IsReadableStream(stream) === false) { 859 | throw new TypeError('ReadableStreamDefaultController can only be constructed with a ReadableStream instance'); 860 | } 861 | 862 | if (stream._readableStreamController !== undefined) { 863 | throw new TypeError( 864 | 'ReadableStreamDefaultController instances can only be created by the ReadableStream constructor'); 865 | } 866 | 867 | this._controlledReadableStream = stream; 868 | 869 | this._underlyingSource = underlyingSource; 870 | 871 | // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly. 872 | this._queue = undefined; 873 | this._queueTotalSize = undefined; 874 | ResetQueue(this); 875 | 876 | this._started = false; 877 | this._closeRequested = false; 878 | this._pullAgain = false; 879 | this._pulling = false; 880 | 881 | const normalizedStrategy = ValidateAndNormalizeQueuingStrategy(size, highWaterMark); 882 | this._strategySize = normalizedStrategy.size; 883 | this._strategyHWM = normalizedStrategy.highWaterMark; 884 | 885 | const controller = this; 886 | 887 | const startResult = InvokeOrNoop(underlyingSource, 'start', [this]); 888 | Promise.resolve(startResult).then( 889 | () => { 890 | controller._started = true; 891 | 892 | assert(controller._pulling === false); 893 | assert(controller._pullAgain === false); 894 | 895 | ReadableStreamDefaultControllerCallPullIfNeeded(controller); 896 | }, 897 | r => { 898 | ReadableStreamDefaultControllerErrorIfNeeded(controller, r); 899 | } 900 | ) 901 | .catch(rethrowAssertionErrorRejection); 902 | } 903 | 904 | get desiredSize() { 905 | if (IsReadableStreamDefaultController(this) === false) { 906 | throw defaultControllerBrandCheckException('desiredSize'); 907 | } 908 | 909 | return ReadableStreamDefaultControllerGetDesiredSize(this); 910 | } 911 | 912 | close() { 913 | if (IsReadableStreamDefaultController(this) === false) { 914 | throw defaultControllerBrandCheckException('close'); 915 | } 916 | 917 | if (this._closeRequested === true) { 918 | throw new TypeError('The stream has already been closed; do not close it again!'); 919 | } 920 | 921 | const state = this._controlledReadableStream._state; 922 | if (state !== 'readable') { 923 | throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`); 924 | } 925 | 926 | ReadableStreamDefaultControllerClose(this); 927 | } 928 | 929 | enqueue(chunk) { 930 | if (IsReadableStreamDefaultController(this) === false) { 931 | throw defaultControllerBrandCheckException('enqueue'); 932 | } 933 | 934 | if (this._closeRequested === true) { 935 | throw new TypeError('stream is closed or draining'); 936 | } 937 | 938 | const state = this._controlledReadableStream._state; 939 | if (state !== 'readable') { 940 | throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`); 941 | } 942 | 943 | return ReadableStreamDefaultControllerEnqueue(this, chunk); 944 | } 945 | 946 | error(e) { 947 | if (IsReadableStreamDefaultController(this) === false) { 948 | throw defaultControllerBrandCheckException('error'); 949 | } 950 | 951 | const stream = this._controlledReadableStream; 952 | if (stream._state !== 'readable') { 953 | throw new TypeError(`The stream is ${stream._state} and so cannot be errored`); 954 | } 955 | 956 | ReadableStreamDefaultControllerError(this, e); 957 | } 958 | 959 | [CancelSteps](reason) { 960 | ResetQueue(this); 961 | return PromiseInvokeOrNoop(this._underlyingSource, 'cancel', [reason]); 962 | } 963 | 964 | [PullSteps]() { 965 | const stream = this._controlledReadableStream; 966 | 967 | if (this._queue.length > 0) { 968 | const chunk = DequeueValue(this); 969 | 970 | if (this._closeRequested === true && this._queue.length === 0) { 971 | ReadableStreamClose(stream); 972 | } else { 973 | ReadableStreamDefaultControllerCallPullIfNeeded(this); 974 | } 975 | 976 | return Promise.resolve(CreateIterResultObject(chunk, false)); 977 | } 978 | 979 | const pendingPromise = ReadableStreamAddReadRequest(stream); 980 | ReadableStreamDefaultControllerCallPullIfNeeded(this); 981 | return pendingPromise; 982 | } 983 | } 984 | 985 | // Abstract operations for the ReadableStreamDefaultController. 986 | 987 | function IsReadableStreamDefaultController(x) { 988 | if (!typeIsObject(x)) { 989 | return false; 990 | } 991 | 992 | if (!Object.prototype.hasOwnProperty.call(x, '_underlyingSource')) { 993 | return false; 994 | } 995 | 996 | return true; 997 | } 998 | 999 | function ReadableStreamDefaultControllerCallPullIfNeeded(controller) { 1000 | const shouldPull = ReadableStreamDefaultControllerShouldCallPull(controller); 1001 | if (shouldPull === false) { 1002 | return undefined; 1003 | } 1004 | 1005 | if (controller._pulling === true) { 1006 | controller._pullAgain = true; 1007 | return undefined; 1008 | } 1009 | 1010 | assert(controller._pullAgain === false); 1011 | 1012 | controller._pulling = true; 1013 | 1014 | const pullPromise = PromiseInvokeOrNoop(controller._underlyingSource, 'pull', [controller]); 1015 | pullPromise.then( 1016 | () => { 1017 | controller._pulling = false; 1018 | 1019 | if (controller._pullAgain === true) { 1020 | controller._pullAgain = false; 1021 | return ReadableStreamDefaultControllerCallPullIfNeeded(controller); 1022 | } 1023 | return undefined; 1024 | }, 1025 | e => { 1026 | ReadableStreamDefaultControllerErrorIfNeeded(controller, e); 1027 | } 1028 | ) 1029 | .catch(rethrowAssertionErrorRejection); 1030 | 1031 | return undefined; 1032 | } 1033 | 1034 | function ReadableStreamDefaultControllerShouldCallPull(controller) { 1035 | const stream = controller._controlledReadableStream; 1036 | 1037 | if (stream._state === 'closed' || stream._state === 'errored') { 1038 | return false; 1039 | } 1040 | 1041 | if (controller._closeRequested === true) { 1042 | return false; 1043 | } 1044 | 1045 | if (controller._started === false) { 1046 | return false; 1047 | } 1048 | 1049 | if (IsReadableStreamLocked(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) { 1050 | return true; 1051 | } 1052 | 1053 | const desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller); 1054 | if (desiredSize > 0) { 1055 | return true; 1056 | } 1057 | 1058 | return false; 1059 | } 1060 | 1061 | // A client of ReadableStreamDefaultController may use these functions directly to bypass state check. 1062 | 1063 | function ReadableStreamDefaultControllerClose(controller) { 1064 | const stream = controller._controlledReadableStream; 1065 | 1066 | assert(controller._closeRequested === false); 1067 | assert(stream._state === 'readable'); 1068 | 1069 | controller._closeRequested = true; 1070 | 1071 | if (controller._queue.length === 0) { 1072 | ReadableStreamClose(stream); 1073 | } 1074 | } 1075 | 1076 | function ReadableStreamDefaultControllerEnqueue(controller, chunk) { 1077 | const stream = controller._controlledReadableStream; 1078 | 1079 | assert(controller._closeRequested === false); 1080 | assert(stream._state === 'readable'); 1081 | 1082 | if (IsReadableStreamLocked(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) { 1083 | ReadableStreamFulfillReadRequest(stream, chunk, false); 1084 | } else { 1085 | let chunkSize = 1; 1086 | 1087 | if (controller._strategySize !== undefined) { 1088 | const strategySize = controller._strategySize; 1089 | try { 1090 | chunkSize = strategySize(chunk); 1091 | } catch (chunkSizeE) { 1092 | ReadableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE); 1093 | throw chunkSizeE; 1094 | } 1095 | } 1096 | 1097 | try { 1098 | EnqueueValueWithSize(controller, chunk, chunkSize); 1099 | } catch (enqueueE) { 1100 | ReadableStreamDefaultControllerErrorIfNeeded(controller, enqueueE); 1101 | throw enqueueE; 1102 | } 1103 | } 1104 | 1105 | ReadableStreamDefaultControllerCallPullIfNeeded(controller); 1106 | 1107 | return undefined; 1108 | } 1109 | 1110 | function ReadableStreamDefaultControllerError(controller, e) { 1111 | const stream = controller._controlledReadableStream; 1112 | 1113 | assert(stream._state === 'readable'); 1114 | 1115 | ResetQueue(controller); 1116 | 1117 | ReadableStreamError(stream, e); 1118 | } 1119 | 1120 | function ReadableStreamDefaultControllerErrorIfNeeded(controller, e) { 1121 | if (controller._controlledReadableStream._state === 'readable') { 1122 | ReadableStreamDefaultControllerError(controller, e); 1123 | } 1124 | } 1125 | 1126 | function ReadableStreamDefaultControllerGetDesiredSize(controller) { 1127 | const stream = controller._controlledReadableStream; 1128 | const state = stream._state; 1129 | 1130 | if (state === 'errored') { 1131 | return null; 1132 | } 1133 | if (state === 'closed') { 1134 | return 0; 1135 | } 1136 | 1137 | return controller._strategyHWM - controller._queueTotalSize; 1138 | } 1139 | 1140 | class ReadableStreamBYOBRequest { 1141 | constructor(controller, view) { 1142 | this._associatedReadableByteStreamController = controller; 1143 | this._view = view; 1144 | } 1145 | 1146 | get view() { 1147 | return this._view; 1148 | } 1149 | 1150 | respond(bytesWritten) { 1151 | if (IsReadableStreamBYOBRequest(this) === false) { 1152 | throw byobRequestBrandCheckException('respond'); 1153 | } 1154 | 1155 | if (this._associatedReadableByteStreamController === undefined) { 1156 | throw new TypeError('This BYOB request has been invalidated'); 1157 | } 1158 | 1159 | ReadableByteStreamControllerRespond(this._associatedReadableByteStreamController, bytesWritten); 1160 | } 1161 | 1162 | respondWithNewView(view) { 1163 | if (IsReadableStreamBYOBRequest(this) === false) { 1164 | throw byobRequestBrandCheckException('respond'); 1165 | } 1166 | 1167 | if (this._associatedReadableByteStreamController === undefined) { 1168 | throw new TypeError('This BYOB request has been invalidated'); 1169 | } 1170 | 1171 | if (!ArrayBuffer.isView(view)) { 1172 | throw new TypeError('You can only respond with array buffer views'); 1173 | } 1174 | 1175 | ReadableByteStreamControllerRespondWithNewView(this._associatedReadableByteStreamController, view); 1176 | } 1177 | } 1178 | 1179 | class ReadableByteStreamController { 1180 | constructor(stream, underlyingByteSource, highWaterMark) { 1181 | if (IsReadableStream(stream) === false) { 1182 | throw new TypeError('ReadableByteStreamController can only be constructed with a ReadableStream instance given ' + 1183 | 'a byte source'); 1184 | } 1185 | 1186 | if (stream._readableStreamController !== undefined) { 1187 | throw new TypeError( 1188 | 'ReadableByteStreamController instances can only be created by the ReadableStream constructor given a byte ' + 1189 | 'source'); 1190 | } 1191 | 1192 | this._controlledReadableStream = stream; 1193 | 1194 | this._underlyingByteSource = underlyingByteSource; 1195 | 1196 | this._pullAgain = false; 1197 | this._pulling = false; 1198 | 1199 | ReadableByteStreamControllerClearPendingPullIntos(this); 1200 | 1201 | // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly. 1202 | this._queue = this._queueTotalSize = undefined; 1203 | ResetQueue(this); 1204 | 1205 | this._closeRequested = false; 1206 | this._started = false; 1207 | 1208 | this._strategyHWM = ValidateAndNormalizeHighWaterMark(highWaterMark); 1209 | 1210 | const autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize; 1211 | if (autoAllocateChunkSize !== undefined) { 1212 | if (Number.isInteger(autoAllocateChunkSize) === false || autoAllocateChunkSize <= 0) { 1213 | throw new RangeError('autoAllocateChunkSize must be a positive integer'); 1214 | } 1215 | } 1216 | this._autoAllocateChunkSize = autoAllocateChunkSize; 1217 | 1218 | this._pendingPullIntos = []; 1219 | 1220 | const controller = this; 1221 | 1222 | const startResult = InvokeOrNoop(underlyingByteSource, 'start', [this]); 1223 | Promise.resolve(startResult).then( 1224 | () => { 1225 | controller._started = true; 1226 | 1227 | assert(controller._pulling === false); 1228 | assert(controller._pullAgain === false); 1229 | 1230 | ReadableByteStreamControllerCallPullIfNeeded(controller); 1231 | }, 1232 | r => { 1233 | if (stream._state === 'readable') { 1234 | ReadableByteStreamControllerError(controller, r); 1235 | } 1236 | } 1237 | ) 1238 | .catch(rethrowAssertionErrorRejection); 1239 | } 1240 | 1241 | get byobRequest() { 1242 | if (IsReadableByteStreamController(this) === false) { 1243 | throw byteStreamControllerBrandCheckException('byobRequest'); 1244 | } 1245 | 1246 | if (this._byobRequest === undefined && this._pendingPullIntos.length > 0) { 1247 | const firstDescriptor = this._pendingPullIntos[0]; 1248 | const view = new Uint8Array(firstDescriptor.buffer, 1249 | firstDescriptor.byteOffset + firstDescriptor.bytesFilled, 1250 | firstDescriptor.byteLength - firstDescriptor.bytesFilled); 1251 | 1252 | this._byobRequest = new ReadableStreamBYOBRequest(this, view); 1253 | } 1254 | 1255 | return this._byobRequest; 1256 | } 1257 | 1258 | get desiredSize() { 1259 | if (IsReadableByteStreamController(this) === false) { 1260 | throw byteStreamControllerBrandCheckException('desiredSize'); 1261 | } 1262 | 1263 | return ReadableByteStreamControllerGetDesiredSize(this); 1264 | } 1265 | 1266 | close() { 1267 | if (IsReadableByteStreamController(this) === false) { 1268 | throw byteStreamControllerBrandCheckException('close'); 1269 | } 1270 | 1271 | if (this._closeRequested === true) { 1272 | throw new TypeError('The stream has already been closed; do not close it again!'); 1273 | } 1274 | 1275 | const state = this._controlledReadableStream._state; 1276 | if (state !== 'readable') { 1277 | throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`); 1278 | } 1279 | 1280 | ReadableByteStreamControllerClose(this); 1281 | } 1282 | 1283 | enqueue(chunk) { 1284 | if (IsReadableByteStreamController(this) === false) { 1285 | throw byteStreamControllerBrandCheckException('enqueue'); 1286 | } 1287 | 1288 | if (this._closeRequested === true) { 1289 | throw new TypeError('stream is closed or draining'); 1290 | } 1291 | 1292 | const state = this._controlledReadableStream._state; 1293 | if (state !== 'readable') { 1294 | throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`); 1295 | } 1296 | 1297 | if (!ArrayBuffer.isView(chunk)) { 1298 | throw new TypeError('You can only enqueue array buffer views when using a ReadableByteStreamController'); 1299 | } 1300 | 1301 | ReadableByteStreamControllerEnqueue(this, chunk); 1302 | } 1303 | 1304 | error(e) { 1305 | if (IsReadableByteStreamController(this) === false) { 1306 | throw byteStreamControllerBrandCheckException('error'); 1307 | } 1308 | 1309 | const stream = this._controlledReadableStream; 1310 | if (stream._state !== 'readable') { 1311 | throw new TypeError(`The stream is ${stream._state} and so cannot be errored`); 1312 | } 1313 | 1314 | ReadableByteStreamControllerError(this, e); 1315 | } 1316 | 1317 | [CancelSteps](reason) { 1318 | if (this._pendingPullIntos.length > 0) { 1319 | const firstDescriptor = this._pendingPullIntos[0]; 1320 | firstDescriptor.bytesFilled = 0; 1321 | } 1322 | 1323 | ResetQueue(this); 1324 | 1325 | return PromiseInvokeOrNoop(this._underlyingByteSource, 'cancel', [reason]); 1326 | } 1327 | 1328 | [PullSteps]() { 1329 | const stream = this._controlledReadableStream; 1330 | assert(ReadableStreamHasDefaultReader(stream) === true); 1331 | 1332 | if (this._queueTotalSize > 0) { 1333 | assert(ReadableStreamGetNumReadRequests(stream) === 0); 1334 | 1335 | const entry = this._queue.shift(); 1336 | this._queueTotalSize -= entry.byteLength; 1337 | 1338 | ReadableByteStreamControllerHandleQueueDrain(this); 1339 | 1340 | let view; 1341 | try { 1342 | view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength); 1343 | } catch (viewE) { 1344 | return Promise.reject(viewE); 1345 | } 1346 | 1347 | return Promise.resolve(CreateIterResultObject(view, false)); 1348 | } 1349 | 1350 | const autoAllocateChunkSize = this._autoAllocateChunkSize; 1351 | if (autoAllocateChunkSize !== undefined) { 1352 | let buffer; 1353 | try { 1354 | buffer = new ArrayBuffer(autoAllocateChunkSize); 1355 | } catch (bufferE) { 1356 | return Promise.reject(bufferE); 1357 | } 1358 | 1359 | const pullIntoDescriptor = { 1360 | buffer, 1361 | byteOffset: 0, 1362 | byteLength: autoAllocateChunkSize, 1363 | bytesFilled: 0, 1364 | elementSize: 1, 1365 | ctor: Uint8Array, 1366 | readerType: 'default' 1367 | }; 1368 | 1369 | this._pendingPullIntos.push(pullIntoDescriptor); 1370 | } 1371 | 1372 | const promise = ReadableStreamAddReadRequest(stream); 1373 | 1374 | ReadableByteStreamControllerCallPullIfNeeded(this); 1375 | 1376 | return promise; 1377 | } 1378 | } 1379 | 1380 | // Abstract operations for the ReadableByteStreamController. 1381 | 1382 | function IsReadableByteStreamController(x) { 1383 | if (!typeIsObject(x)) { 1384 | return false; 1385 | } 1386 | 1387 | if (!Object.prototype.hasOwnProperty.call(x, '_underlyingByteSource')) { 1388 | return false; 1389 | } 1390 | 1391 | return true; 1392 | } 1393 | 1394 | function IsReadableStreamBYOBRequest(x) { 1395 | if (!typeIsObject(x)) { 1396 | return false; 1397 | } 1398 | 1399 | if (!Object.prototype.hasOwnProperty.call(x, '_associatedReadableByteStreamController')) { 1400 | return false; 1401 | } 1402 | 1403 | return true; 1404 | } 1405 | 1406 | function ReadableByteStreamControllerCallPullIfNeeded(controller) { 1407 | const shouldPull = ReadableByteStreamControllerShouldCallPull(controller); 1408 | if (shouldPull === false) { 1409 | return undefined; 1410 | } 1411 | 1412 | if (controller._pulling === true) { 1413 | controller._pullAgain = true; 1414 | return undefined; 1415 | } 1416 | 1417 | assert(controller._pullAgain === false); 1418 | 1419 | controller._pulling = true; 1420 | 1421 | // TODO: Test controller argument 1422 | const pullPromise = PromiseInvokeOrNoop(controller._underlyingByteSource, 'pull', [controller]); 1423 | pullPromise.then( 1424 | () => { 1425 | controller._pulling = false; 1426 | 1427 | if (controller._pullAgain === true) { 1428 | controller._pullAgain = false; 1429 | ReadableByteStreamControllerCallPullIfNeeded(controller); 1430 | } 1431 | }, 1432 | e => { 1433 | if (controller._controlledReadableStream._state === 'readable') { 1434 | ReadableByteStreamControllerError(controller, e); 1435 | } 1436 | } 1437 | ) 1438 | .catch(rethrowAssertionErrorRejection); 1439 | 1440 | return undefined; 1441 | } 1442 | 1443 | function ReadableByteStreamControllerClearPendingPullIntos(controller) { 1444 | ReadableByteStreamControllerInvalidateBYOBRequest(controller); 1445 | controller._pendingPullIntos = []; 1446 | } 1447 | 1448 | function ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor) { 1449 | assert(stream._state !== 'errored', 'state must not be errored'); 1450 | 1451 | let done = false; 1452 | if (stream._state === 'closed') { 1453 | assert(pullIntoDescriptor.bytesFilled === 0); 1454 | done = true; 1455 | } 1456 | 1457 | const filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor); 1458 | if (pullIntoDescriptor.readerType === 'default') { 1459 | ReadableStreamFulfillReadRequest(stream, filledView, done); 1460 | } else { 1461 | assert(pullIntoDescriptor.readerType === 'byob'); 1462 | ReadableStreamFulfillReadIntoRequest(stream, filledView, done); 1463 | } 1464 | } 1465 | 1466 | function ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor) { 1467 | const bytesFilled = pullIntoDescriptor.bytesFilled; 1468 | const elementSize = pullIntoDescriptor.elementSize; 1469 | 1470 | assert(bytesFilled <= pullIntoDescriptor.byteLength); 1471 | assert(bytesFilled % elementSize === 0); 1472 | 1473 | return new pullIntoDescriptor.ctor( 1474 | pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize); 1475 | } 1476 | 1477 | function ReadableByteStreamControllerEnqueueChunkToQueue(controller, buffer, byteOffset, byteLength) { 1478 | controller._queue.push({ buffer, byteOffset, byteLength }); 1479 | controller._queueTotalSize += byteLength; 1480 | } 1481 | 1482 | function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) { 1483 | const elementSize = pullIntoDescriptor.elementSize; 1484 | 1485 | const currentAlignedBytes = pullIntoDescriptor.bytesFilled - pullIntoDescriptor.bytesFilled % elementSize; 1486 | 1487 | const maxBytesToCopy = Math.min(controller._queueTotalSize, 1488 | pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled); 1489 | const maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy; 1490 | const maxAlignedBytes = maxBytesFilled - maxBytesFilled % elementSize; 1491 | 1492 | let totalBytesToCopyRemaining = maxBytesToCopy; 1493 | let ready = false; 1494 | if (maxAlignedBytes > currentAlignedBytes) { 1495 | totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled; 1496 | ready = true; 1497 | } 1498 | 1499 | const queue = controller._queue; 1500 | 1501 | while (totalBytesToCopyRemaining > 0) { 1502 | const headOfQueue = queue[0]; 1503 | 1504 | const bytesToCopy = Math.min(totalBytesToCopyRemaining, headOfQueue.byteLength); 1505 | 1506 | const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; 1507 | ArrayBufferCopy(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy); 1508 | 1509 | if (headOfQueue.byteLength === bytesToCopy) { 1510 | queue.shift(); 1511 | } else { 1512 | headOfQueue.byteOffset += bytesToCopy; 1513 | headOfQueue.byteLength -= bytesToCopy; 1514 | } 1515 | controller._queueTotalSize -= bytesToCopy; 1516 | 1517 | ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor); 1518 | 1519 | totalBytesToCopyRemaining -= bytesToCopy; 1520 | } 1521 | 1522 | if (ready === false) { 1523 | assert(controller._queueTotalSize === 0, 'queue must be empty'); 1524 | assert(pullIntoDescriptor.bytesFilled > 0); 1525 | assert(pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize); 1526 | } 1527 | 1528 | return ready; 1529 | } 1530 | 1531 | function ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, size, pullIntoDescriptor) { 1532 | assert(controller._pendingPullIntos.length === 0 || controller._pendingPullIntos[0] === pullIntoDescriptor); 1533 | 1534 | ReadableByteStreamControllerInvalidateBYOBRequest(controller); 1535 | pullIntoDescriptor.bytesFilled += size; 1536 | } 1537 | 1538 | function ReadableByteStreamControllerHandleQueueDrain(controller) { 1539 | assert(controller._controlledReadableStream._state === 'readable'); 1540 | 1541 | if (controller._queueTotalSize === 0 && controller._closeRequested === true) { 1542 | ReadableStreamClose(controller._controlledReadableStream); 1543 | } else { 1544 | ReadableByteStreamControllerCallPullIfNeeded(controller); 1545 | } 1546 | } 1547 | 1548 | function ReadableByteStreamControllerInvalidateBYOBRequest(controller) { 1549 | if (controller._byobRequest === undefined) { 1550 | return; 1551 | } 1552 | 1553 | controller._byobRequest._associatedReadableByteStreamController = undefined; 1554 | controller._byobRequest._view = undefined; 1555 | controller._byobRequest = undefined; 1556 | } 1557 | 1558 | function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller) { 1559 | assert(controller._closeRequested === false); 1560 | 1561 | while (controller._pendingPullIntos.length > 0) { 1562 | if (controller._queueTotalSize === 0) { 1563 | return; 1564 | } 1565 | 1566 | const pullIntoDescriptor = controller._pendingPullIntos[0]; 1567 | 1568 | if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) === true) { 1569 | ReadableByteStreamControllerShiftPendingPullInto(controller); 1570 | 1571 | ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableStream, pullIntoDescriptor); 1572 | } 1573 | } 1574 | } 1575 | 1576 | function ReadableByteStreamControllerPullInto(controller, view) { 1577 | const stream = controller._controlledReadableStream; 1578 | 1579 | let elementSize = 1; 1580 | if (view.constructor !== DataView) { 1581 | elementSize = view.constructor.BYTES_PER_ELEMENT; 1582 | } 1583 | 1584 | const ctor = view.constructor; 1585 | 1586 | const pullIntoDescriptor = { 1587 | buffer: view.buffer, 1588 | byteOffset: view.byteOffset, 1589 | byteLength: view.byteLength, 1590 | bytesFilled: 0, 1591 | elementSize, 1592 | ctor, 1593 | readerType: 'byob' 1594 | }; 1595 | 1596 | if (controller._pendingPullIntos.length > 0) { 1597 | pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer); 1598 | controller._pendingPullIntos.push(pullIntoDescriptor); 1599 | 1600 | // No ReadableByteStreamControllerCallPullIfNeeded() call since: 1601 | // - No change happens on desiredSize 1602 | // - The source has already been notified of that there's at least 1 pending read(view) 1603 | 1604 | return ReadableStreamAddReadIntoRequest(stream); 1605 | } 1606 | 1607 | if (stream._state === 'closed') { 1608 | const emptyView = new view.constructor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0); 1609 | return Promise.resolve(CreateIterResultObject(emptyView, true)); 1610 | } 1611 | 1612 | if (controller._queueTotalSize > 0) { 1613 | if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) === true) { 1614 | const filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor); 1615 | 1616 | ReadableByteStreamControllerHandleQueueDrain(controller); 1617 | 1618 | return Promise.resolve(CreateIterResultObject(filledView, false)); 1619 | } 1620 | 1621 | if (controller._closeRequested === true) { 1622 | const e = new TypeError('Insufficient bytes to fill elements in the given buffer'); 1623 | ReadableByteStreamControllerError(controller, e); 1624 | 1625 | return Promise.reject(e); 1626 | } 1627 | } 1628 | 1629 | pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer); 1630 | controller._pendingPullIntos.push(pullIntoDescriptor); 1631 | 1632 | const promise = ReadableStreamAddReadIntoRequest(stream); 1633 | 1634 | ReadableByteStreamControllerCallPullIfNeeded(controller); 1635 | 1636 | return promise; 1637 | } 1638 | 1639 | function ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor) { 1640 | firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer); 1641 | 1642 | assert(firstDescriptor.bytesFilled === 0, 'bytesFilled must be 0'); 1643 | 1644 | const stream = controller._controlledReadableStream; 1645 | if (ReadableStreamHasBYOBReader(stream) === true) { 1646 | while (ReadableStreamGetNumReadIntoRequests(stream) > 0) { 1647 | const pullIntoDescriptor = ReadableByteStreamControllerShiftPendingPullInto(controller); 1648 | ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor); 1649 | } 1650 | } 1651 | } 1652 | 1653 | function ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor) { 1654 | if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength) { 1655 | throw new RangeError('bytesWritten out of range'); 1656 | } 1657 | 1658 | ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor); 1659 | 1660 | if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize) { 1661 | // TODO: Figure out whether we should detach the buffer or not here. 1662 | return; 1663 | } 1664 | 1665 | ReadableByteStreamControllerShiftPendingPullInto(controller); 1666 | 1667 | const remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize; 1668 | if (remainderSize > 0) { 1669 | const end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; 1670 | const remainder = pullIntoDescriptor.buffer.slice(end - remainderSize, end); 1671 | ReadableByteStreamControllerEnqueueChunkToQueue(controller, remainder, 0, remainder.byteLength); 1672 | } 1673 | 1674 | pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer); 1675 | pullIntoDescriptor.bytesFilled -= remainderSize; 1676 | ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableStream, pullIntoDescriptor); 1677 | 1678 | ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); 1679 | } 1680 | 1681 | function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) { 1682 | const firstDescriptor = controller._pendingPullIntos[0]; 1683 | 1684 | const stream = controller._controlledReadableStream; 1685 | 1686 | if (stream._state === 'closed') { 1687 | if (bytesWritten !== 0) { 1688 | throw new TypeError('bytesWritten must be 0 when calling respond() on a closed stream'); 1689 | } 1690 | 1691 | ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor); 1692 | } else { 1693 | assert(stream._state === 'readable'); 1694 | 1695 | ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor); 1696 | } 1697 | } 1698 | 1699 | function ReadableByteStreamControllerShiftPendingPullInto(controller) { 1700 | const descriptor = controller._pendingPullIntos.shift(); 1701 | ReadableByteStreamControllerInvalidateBYOBRequest(controller); 1702 | return descriptor; 1703 | } 1704 | 1705 | function ReadableByteStreamControllerShouldCallPull(controller) { 1706 | const stream = controller._controlledReadableStream; 1707 | 1708 | if (stream._state !== 'readable') { 1709 | return false; 1710 | } 1711 | 1712 | if (controller._closeRequested === true) { 1713 | return false; 1714 | } 1715 | 1716 | if (controller._started === false) { 1717 | return false; 1718 | } 1719 | 1720 | if (ReadableStreamHasDefaultReader(stream) === true && ReadableStreamGetNumReadRequests(stream) > 0) { 1721 | return true; 1722 | } 1723 | 1724 | if (ReadableStreamHasBYOBReader(stream) === true && ReadableStreamGetNumReadIntoRequests(stream) > 0) { 1725 | return true; 1726 | } 1727 | 1728 | if (ReadableByteStreamControllerGetDesiredSize(controller) > 0) { 1729 | return true; 1730 | } 1731 | 1732 | return false; 1733 | } 1734 | 1735 | // A client of ReadableByteStreamController may use these functions directly to bypass state check. 1736 | 1737 | function ReadableByteStreamControllerClose(controller) { 1738 | const stream = controller._controlledReadableStream; 1739 | 1740 | assert(controller._closeRequested === false); 1741 | assert(stream._state === 'readable'); 1742 | 1743 | if (controller._queueTotalSize > 0) { 1744 | controller._closeRequested = true; 1745 | 1746 | return; 1747 | } 1748 | 1749 | if (controller._pendingPullIntos.length > 0) { 1750 | const firstPendingPullInto = controller._pendingPullIntos[0]; 1751 | if (firstPendingPullInto.bytesFilled > 0) { 1752 | const e = new TypeError('Insufficient bytes to fill elements in the given buffer'); 1753 | ReadableByteStreamControllerError(controller, e); 1754 | 1755 | throw e; 1756 | } 1757 | } 1758 | 1759 | ReadableStreamClose(stream); 1760 | } 1761 | 1762 | function ReadableByteStreamControllerEnqueue(controller, chunk) { 1763 | const stream = controller._controlledReadableStream; 1764 | 1765 | assert(controller._closeRequested === false); 1766 | assert(stream._state === 'readable'); 1767 | 1768 | const buffer = chunk.buffer; 1769 | const byteOffset = chunk.byteOffset; 1770 | const byteLength = chunk.byteLength; 1771 | const transferredBuffer = TransferArrayBuffer(buffer); 1772 | 1773 | if (ReadableStreamHasDefaultReader(stream) === true) { 1774 | if (ReadableStreamGetNumReadRequests(stream) === 0) { 1775 | ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); 1776 | } else { 1777 | assert(controller._queue.length === 0); 1778 | 1779 | const transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength); 1780 | ReadableStreamFulfillReadRequest(stream, transferredView, false); 1781 | } 1782 | } else if (ReadableStreamHasBYOBReader(stream) === true) { 1783 | // TODO: Ideally in this branch detaching should happen only if the buffer is not consumed fully. 1784 | ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); 1785 | ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); 1786 | } else { 1787 | assert(IsReadableStreamLocked(stream) === false, 'stream must not be locked'); 1788 | ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); 1789 | } 1790 | } 1791 | 1792 | function ReadableByteStreamControllerError(controller, e) { 1793 | const stream = controller._controlledReadableStream; 1794 | 1795 | assert(stream._state === 'readable'); 1796 | 1797 | ReadableByteStreamControllerClearPendingPullIntos(controller); 1798 | 1799 | ResetQueue(controller); 1800 | ReadableStreamError(stream, e); 1801 | } 1802 | 1803 | function ReadableByteStreamControllerGetDesiredSize(controller) { 1804 | const stream = controller._controlledReadableStream; 1805 | const state = stream._state; 1806 | 1807 | if (state === 'errored') { 1808 | return null; 1809 | } 1810 | if (state === 'closed') { 1811 | return 0; 1812 | } 1813 | 1814 | return controller._strategyHWM - controller._queueTotalSize; 1815 | } 1816 | 1817 | function ReadableByteStreamControllerRespond(controller, bytesWritten) { 1818 | bytesWritten = Number(bytesWritten); 1819 | if (IsFiniteNonNegativeNumber(bytesWritten) === false) { 1820 | throw new RangeError('bytesWritten must be a finite'); 1821 | } 1822 | 1823 | assert(controller._pendingPullIntos.length > 0); 1824 | 1825 | ReadableByteStreamControllerRespondInternal(controller, bytesWritten); 1826 | } 1827 | 1828 | function ReadableByteStreamControllerRespondWithNewView(controller, view) { 1829 | assert(controller._pendingPullIntos.length > 0); 1830 | 1831 | const firstDescriptor = controller._pendingPullIntos[0]; 1832 | 1833 | if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) { 1834 | throw new RangeError('The region specified by view does not match byobRequest'); 1835 | } 1836 | if (firstDescriptor.byteLength !== view.byteLength) { 1837 | throw new RangeError('The buffer of view has different capacity than byobRequest'); 1838 | } 1839 | 1840 | firstDescriptor.buffer = view.buffer; 1841 | 1842 | ReadableByteStreamControllerRespondInternal(controller, view.byteLength); 1843 | } 1844 | 1845 | // Helper functions for the ReadableStream. 1846 | 1847 | function streamBrandCheckException(name) { 1848 | return new TypeError(`ReadableStream.prototype.${name} can only be used on a ReadableStream`); 1849 | } 1850 | 1851 | // Helper functions for the readers. 1852 | 1853 | function readerLockException(name) { 1854 | return new TypeError('Cannot ' + name + ' a stream using a released reader'); 1855 | } 1856 | 1857 | // Helper functions for the ReadableStreamDefaultReader. 1858 | 1859 | function defaultReaderBrandCheckException(name) { 1860 | return new TypeError( 1861 | `ReadableStreamDefaultReader.prototype.${name} can only be used on a ReadableStreamDefaultReader`); 1862 | } 1863 | 1864 | function defaultReaderClosedPromiseInitialize(reader) { 1865 | reader._closedPromise = new Promise((resolve, reject) => { 1866 | reader._closedPromise_resolve = resolve; 1867 | reader._closedPromise_reject = reject; 1868 | }); 1869 | } 1870 | 1871 | function defaultReaderClosedPromiseInitializeAsRejected(reader, reason) { 1872 | reader._closedPromise = Promise.reject(reason); 1873 | reader._closedPromise_resolve = undefined; 1874 | reader._closedPromise_reject = undefined; 1875 | } 1876 | 1877 | function defaultReaderClosedPromiseInitializeAsResolved(reader) { 1878 | reader._closedPromise = Promise.resolve(undefined); 1879 | reader._closedPromise_resolve = undefined; 1880 | reader._closedPromise_reject = undefined; 1881 | } 1882 | 1883 | function defaultReaderClosedPromiseReject(reader, reason) { 1884 | assert(reader._closedPromise_resolve !== undefined); 1885 | assert(reader._closedPromise_reject !== undefined); 1886 | 1887 | reader._closedPromise_reject(reason); 1888 | reader._closedPromise_resolve = undefined; 1889 | reader._closedPromise_reject = undefined; 1890 | } 1891 | 1892 | function defaultReaderClosedPromiseResetToRejected(reader, reason) { 1893 | assert(reader._closedPromise_resolve === undefined); 1894 | assert(reader._closedPromise_reject === undefined); 1895 | 1896 | reader._closedPromise = Promise.reject(reason); 1897 | } 1898 | 1899 | function defaultReaderClosedPromiseResolve(reader) { 1900 | assert(reader._closedPromise_resolve !== undefined); 1901 | assert(reader._closedPromise_reject !== undefined); 1902 | 1903 | reader._closedPromise_resolve(undefined); 1904 | reader._closedPromise_resolve = undefined; 1905 | reader._closedPromise_reject = undefined; 1906 | } 1907 | 1908 | // Helper functions for the ReadableStreamDefaultReader. 1909 | 1910 | function byobReaderBrandCheckException(name) { 1911 | return new TypeError( 1912 | `ReadableStreamBYOBReader.prototype.${name} can only be used on a ReadableStreamBYOBReader`); 1913 | } 1914 | 1915 | // Helper functions for the ReadableStreamDefaultController. 1916 | 1917 | function defaultControllerBrandCheckException(name) { 1918 | return new TypeError( 1919 | `ReadableStreamDefaultController.prototype.${name} can only be used on a ReadableStreamDefaultController`); 1920 | } 1921 | 1922 | // Helper functions for the ReadableStreamBYOBRequest. 1923 | 1924 | function byobRequestBrandCheckException(name) { 1925 | return new TypeError( 1926 | `ReadableStreamBYOBRequest.prototype.${name} can only be used on a ReadableStreamBYOBRequest`); 1927 | } 1928 | 1929 | // Helper functions for the ReadableByteStreamController. 1930 | 1931 | function byteStreamControllerBrandCheckException(name) { 1932 | return new TypeError( 1933 | `ReadableByteStreamController.prototype.${name} can only be used on a ReadableByteStreamController`); 1934 | } 1935 | 1936 | // Helper function for ReadableStream pipeThrough 1937 | 1938 | function ifIsObjectAndHasAPromiseIsHandledInternalSlotSetPromiseIsHandledToTrue(promise) { 1939 | try { 1940 | // This relies on the brand-check that is enforced by Promise.prototype.then(). As with the rest of the reference 1941 | // implementation, it doesn't attempt to do the right thing if someone has modified the global environment. 1942 | Promise.prototype.then.call(promise, undefined, () => {}); 1943 | } catch (e) { 1944 | // The brand check failed, therefore the internal slot is not present and there's nothing further to do. 1945 | } 1946 | } 1947 | -------------------------------------------------------------------------------- /private/streams/transform-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const { InvokeOrNoop, PromiseInvokeOrPerformFallback, PromiseInvokeOrNoop, typeIsObject } = require('./helpers.js'); 4 | const { ReadableStream, ReadableStreamDefaultControllerClose, 5 | ReadableStreamDefaultControllerEnqueue, ReadableStreamDefaultControllerError, 6 | ReadableStreamDefaultControllerGetDesiredSize } = require('./readable-stream.js'); 7 | const { WritableStream, WritableStreamDefaultControllerError } = require('./writable-stream.js'); 8 | 9 | // Methods on the transform stream controller object 10 | 11 | function TransformStreamCloseReadable(transformStream) { 12 | // console.log('TransformStreamCloseReadable()'); 13 | 14 | if (transformStream._errored === true) { 15 | throw new TypeError('TransformStream is already errored'); 16 | } 17 | 18 | if (transformStream._readableClosed === true) { 19 | throw new TypeError('Readable side is already closed'); 20 | } 21 | 22 | TransformStreamCloseReadableInternal(transformStream); 23 | } 24 | 25 | function TransformStreamEnqueueToReadable(transformStream, chunk) { 26 | // console.log('TransformStreamEnqueueToReadable()'); 27 | 28 | if (transformStream._errored === true) { 29 | throw new TypeError('TransformStream is already errored'); 30 | } 31 | 32 | if (transformStream._readableClosed === true) { 33 | throw new TypeError('Readable side is already closed'); 34 | } 35 | 36 | // We throttle transformer.transform invocation based on the backpressure of the ReadableStream, but we still 37 | // accept TransformStreamEnqueueToReadable() calls. 38 | 39 | const controller = transformStream._readableController; 40 | 41 | try { 42 | ReadableStreamDefaultControllerEnqueue(controller, chunk); 43 | } catch (e) { 44 | // This happens when readableStrategy.size() throws. 45 | // The ReadableStream has already errored itself. 46 | transformStream._readableClosed = true; 47 | TransformStreamErrorIfNeeded(transformStream, e); 48 | 49 | throw transformStream._storedError; 50 | } 51 | 52 | const desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller); 53 | const maybeBackpressure = desiredSize <= 0; 54 | 55 | if (maybeBackpressure === true && transformStream._backpressure === false) { 56 | // This allows pull() again. When desiredSize is 0, it's possible that a pull() will happen immediately (but 57 | // asynchronously) after this because of pending read()s and set _backpressure back to false. 58 | // 59 | // If pull() could be called from inside enqueue(), then this logic would be wrong. This cannot happen 60 | // because there is always a promise pending from start() or pull() when _backpressure is false. 61 | TransformStreamSetBackpressure(transformStream, true); 62 | } 63 | } 64 | 65 | function TransformStreamError(transformStream, e) { 66 | if (transformStream._errored === true) { 67 | throw new TypeError('TransformStream is already errored'); 68 | } 69 | 70 | TransformStreamErrorInternal(transformStream, e); 71 | } 72 | 73 | // Abstract operations. 74 | 75 | function TransformStreamCloseReadableInternal(transformStream) { 76 | assert(transformStream._errored === false); 77 | assert(transformStream._readableClosed === false); 78 | 79 | try { 80 | ReadableStreamDefaultControllerClose(transformStream._readableController); 81 | } catch (e) { 82 | assert(false); 83 | } 84 | 85 | transformStream._readableClosed = true; 86 | } 87 | 88 | function TransformStreamErrorIfNeeded(transformStream, e) { 89 | if (transformStream._errored === false) { 90 | TransformStreamErrorInternal(transformStream, e); 91 | } 92 | } 93 | 94 | function TransformStreamErrorInternal(transformStream, e) { 95 | // console.log('TransformStreamErrorInternal()'); 96 | 97 | assert(transformStream._errored === false); 98 | 99 | transformStream._errored = true; 100 | transformStream._storedError = e; 101 | 102 | if (transformStream._writableDone === false) { 103 | WritableStreamDefaultControllerError(transformStream._writableController, e); 104 | } 105 | if (transformStream._readableClosed === false) { 106 | ReadableStreamDefaultControllerError(transformStream._readableController, e); 107 | } 108 | } 109 | 110 | // Used for preventing the next write() call on TransformStreamSink until there 111 | // is no longer backpressure. 112 | function TransformStreamReadableReadyPromise(transformStream) { 113 | assert(transformStream._backpressureChangePromise !== undefined, 114 | '_backpressureChangePromise should have been initialized'); 115 | 116 | if (transformStream._backpressure === false) { 117 | return Promise.resolve(); 118 | } 119 | 120 | assert(transformStream._backpressure === true, '_backpressure should have been initialized'); 121 | 122 | return transformStream._backpressureChangePromise; 123 | } 124 | 125 | function TransformStreamSetBackpressure(transformStream, backpressure) { 126 | // console.log(`TransformStreamSetBackpressure(${backpressure})`); 127 | 128 | // Passes also when called during construction. 129 | assert(transformStream._backpressure !== backpressure, 130 | 'TransformStreamSetBackpressure() should be called only when backpressure is changed'); 131 | 132 | if (transformStream._backpressureChangePromise !== undefined) { 133 | // The fulfillment value is just for a sanity check. 134 | transformStream._backpressureChangePromise_resolve(backpressure); 135 | } 136 | 137 | transformStream._backpressureChangePromise = new Promise(resolve => { 138 | transformStream._backpressureChangePromise_resolve = resolve; 139 | }); 140 | 141 | transformStream._backpressureChangePromise.then(resolution => { 142 | assert(resolution !== backpressure, 143 | '_backpressureChangePromise should be fulfilled only when backpressure is changed'); 144 | }); 145 | 146 | transformStream._backpressure = backpressure; 147 | } 148 | 149 | function TransformStreamDefaultTransform(chunk, transformStreamController) { 150 | const transformStream = transformStreamController._controlledTransformStream; 151 | TransformStreamEnqueueToReadable(transformStream, chunk); 152 | return Promise.resolve(); 153 | } 154 | 155 | function TransformStreamTransform(transformStream, chunk) { 156 | // console.log('TransformStreamTransform()'); 157 | 158 | assert(transformStream._errored === false); 159 | assert(transformStream._transforming === false); 160 | assert(transformStream._backpressure === false); 161 | 162 | transformStream._transforming = true; 163 | 164 | const transformer = transformStream._transformer; 165 | const controller = transformStream._transformStreamController; 166 | 167 | const transformPromise = PromiseInvokeOrPerformFallback(transformer, 'transform', [chunk, controller], 168 | TransformStreamDefaultTransform, [chunk, controller]); 169 | 170 | return transformPromise.then( 171 | () => { 172 | transformStream._transforming = false; 173 | 174 | return TransformStreamReadableReadyPromise(transformStream); 175 | }, 176 | e => { 177 | TransformStreamErrorIfNeeded(transformStream, e); 178 | return Promise.reject(e); 179 | }); 180 | } 181 | 182 | function IsTransformStreamDefaultController(x) { 183 | if (!typeIsObject(x)) { 184 | return false; 185 | } 186 | 187 | if (!Object.prototype.hasOwnProperty.call(x, '_controlledTransformStream')) { 188 | return false; 189 | } 190 | 191 | return true; 192 | } 193 | 194 | function IsTransformStream(x) { 195 | if (!typeIsObject(x)) { 196 | return false; 197 | } 198 | 199 | if (!Object.prototype.hasOwnProperty.call(x, '_transformStreamController')) { 200 | return false; 201 | } 202 | 203 | return true; 204 | } 205 | 206 | class TransformStreamSink { 207 | constructor(transformStream, startPromise) { 208 | this._transformStream = transformStream; 209 | this._startPromise = startPromise; 210 | } 211 | 212 | start(c) { 213 | const transformStream = this._transformStream; 214 | 215 | transformStream._writableController = c; 216 | 217 | return this._startPromise.then(() => TransformStreamReadableReadyPromise(transformStream)); 218 | } 219 | 220 | write(chunk) { 221 | // console.log('TransformStreamSink.write()'); 222 | 223 | const transformStream = this._transformStream; 224 | 225 | return TransformStreamTransform(transformStream, chunk); 226 | } 227 | 228 | abort() { 229 | const transformStream = this._transformStream; 230 | transformStream._writableDone = true; 231 | TransformStreamErrorInternal(transformStream, new TypeError('Writable side aborted')); 232 | } 233 | 234 | close() { 235 | // console.log('TransformStreamSink.close()'); 236 | 237 | const transformStream = this._transformStream; 238 | 239 | assert(transformStream._transforming === false); 240 | 241 | transformStream._writableDone = true; 242 | 243 | const flushPromise = PromiseInvokeOrNoop(transformStream._transformer, 244 | 'flush', [transformStream._transformStreamController]); 245 | // Return a promise that is fulfilled with undefined on success. 246 | return flushPromise.then(() => { 247 | if (transformStream._errored === true) { 248 | return Promise.reject(transformStream._storedError); 249 | } 250 | if (transformStream._readableClosed === false) { 251 | TransformStreamCloseReadableInternal(transformStream); 252 | } 253 | return Promise.resolve(); 254 | }).catch(r => { 255 | TransformStreamErrorIfNeeded(transformStream, r); 256 | return Promise.reject(transformStream._storedError); 257 | }); 258 | } 259 | } 260 | 261 | class TransformStreamSource { 262 | constructor(transformStream, startPromise) { 263 | this._transformStream = transformStream; 264 | this._startPromise = startPromise; 265 | } 266 | 267 | start(c) { 268 | const transformStream = this._transformStream; 269 | 270 | transformStream._readableController = c; 271 | 272 | return this._startPromise.then(() => { 273 | // Prevent the first pull() call until there is backpressure. 274 | 275 | assert(transformStream._backpressureChangePromise !== undefined, 276 | '_backpressureChangePromise should have been initialized'); 277 | 278 | if (transformStream._backpressure === true) { 279 | return Promise.resolve(); 280 | } 281 | 282 | assert(transformStream._backpressure === false, '_backpressure should have been initialized'); 283 | 284 | return transformStream._backpressureChangePromise; 285 | }); 286 | } 287 | 288 | pull() { 289 | // console.log('TransformStreamSource.pull()'); 290 | 291 | const transformStream = this._transformStream; 292 | 293 | // Invariant. Enforced by the promises returned by start() and pull(). 294 | assert(transformStream._backpressure === true, 'pull() should be never called while _backpressure is false'); 295 | 296 | assert(transformStream._backpressureChangePromise !== undefined, 297 | '_backpressureChangePromise should have been initialized'); 298 | 299 | TransformStreamSetBackpressure(transformStream, false); 300 | 301 | // Prevent the next pull() call until there is backpressure. 302 | return transformStream._backpressureChangePromise; 303 | } 304 | 305 | cancel() { 306 | const transformStream = this._transformStream; 307 | transformStream._readableClosed = true; 308 | TransformStreamErrorInternal(transformStream, new TypeError('Readable side canceled')); 309 | } 310 | } 311 | 312 | class TransformStreamDefaultController { 313 | constructor(transformStream) { 314 | if (IsTransformStream(transformStream) === false) { 315 | throw new TypeError('TransformStreamDefaultController can only be ' + 316 | 'constructed with a TransformStream instance'); 317 | } 318 | 319 | if (transformStream._transformStreamController !== undefined) { 320 | throw new TypeError('TransformStreamDefaultController instances can ' + 321 | 'only be created by the TransformStream constructor'); 322 | } 323 | 324 | this._controlledTransformStream = transformStream; 325 | } 326 | 327 | get desiredSize() { 328 | if (IsTransformStreamDefaultController(this) === false) { 329 | throw defaultControllerBrandCheckException('desiredSize'); 330 | } 331 | 332 | const transformStream = this._controlledTransformStream; 333 | const readableController = transformStream._readableController; 334 | 335 | return ReadableStreamDefaultControllerGetDesiredSize(readableController); 336 | } 337 | 338 | enqueue(chunk) { 339 | if (IsTransformStreamDefaultController(this) === false) { 340 | throw defaultControllerBrandCheckException('enqueue'); 341 | } 342 | 343 | TransformStreamEnqueueToReadable(this._controlledTransformStream, chunk); 344 | } 345 | 346 | close() { 347 | if (IsTransformStreamDefaultController(this) === false) { 348 | throw defaultControllerBrandCheckException('close'); 349 | } 350 | 351 | TransformStreamCloseReadable(this._controlledTransformStream); 352 | } 353 | 354 | error(reason) { 355 | if (IsTransformStreamDefaultController(this) === false) { 356 | throw defaultControllerBrandCheckException('error'); 357 | } 358 | 359 | TransformStreamError(this._controlledTransformStream, reason); 360 | } 361 | } 362 | 363 | class TransformStream { 364 | constructor(transformer = {}) { 365 | this._transformer = transformer; 366 | const { readableStrategy, writableStrategy } = transformer; 367 | 368 | this._transforming = false; 369 | this._errored = false; 370 | this._storedError = undefined; 371 | 372 | this._writableController = undefined; 373 | this._readableController = undefined; 374 | this._transformStreamController = undefined; 375 | 376 | this._writableDone = false; 377 | this._readableClosed = false; 378 | 379 | this._backpressure = undefined; 380 | this._backpressureChangePromise = undefined; 381 | this._backpressureChangePromise_resolve = undefined; 382 | 383 | this._transformStreamController = new TransformStreamDefaultController(this); 384 | 385 | let startPromise_resolve; 386 | const startPromise = new Promise(resolve => { 387 | startPromise_resolve = resolve; 388 | }); 389 | 390 | const source = new TransformStreamSource(this, startPromise); 391 | 392 | this._readable = new ReadableStream(source, readableStrategy); 393 | 394 | const sink = new TransformStreamSink(this, startPromise); 395 | 396 | this._writable = new WritableStream(sink, writableStrategy); 397 | 398 | assert(this._writableController !== undefined); 399 | assert(this._readableController !== undefined); 400 | 401 | const desiredSize = ReadableStreamDefaultControllerGetDesiredSize(this._readableController); 402 | // Set _backpressure based on desiredSize. As there is no read() at this point, we can just interpret 403 | // desiredSize being non-positive as backpressure. 404 | TransformStreamSetBackpressure(this, desiredSize <= 0); 405 | 406 | const transformStream = this; 407 | const startResult = InvokeOrNoop(transformer, 'start', 408 | [transformStream._transformStreamController]); 409 | startPromise_resolve(startResult); 410 | startPromise.catch(e => { 411 | // The underlyingSink and underlyingSource will error the readable and writable ends on their own. 412 | if (transformStream._errored === false) { 413 | transformStream._errored = true; 414 | transformStream._storedError = e; 415 | } 416 | }); 417 | } 418 | 419 | get readable() { 420 | if (IsTransformStream(this) === false) { 421 | throw streamBrandCheckException('readable'); 422 | } 423 | 424 | return this._readable; 425 | } 426 | 427 | get writable() { 428 | if (IsTransformStream(this) === false) { 429 | throw streamBrandCheckException('writable'); 430 | } 431 | 432 | return this._writable; 433 | } 434 | } 435 | 436 | module.exports = { TransformStream }; 437 | 438 | // Helper functions for the TransformStreamDefaultController. 439 | 440 | function defaultControllerBrandCheckException(name) { 441 | return new TypeError( 442 | `TransformStreamDefaultController.prototype.${name} can only be used on a TransformStreamDefaultController`); 443 | } 444 | 445 | // Helper functions for the TransformStream. 446 | 447 | function streamBrandCheckException(name) { 448 | return new TypeError( 449 | `TransformStream.prototype.${name} can only be used on a TransformStream`); 450 | } 451 | -------------------------------------------------------------------------------- /private/streams/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | 4 | exports.rethrowAssertionErrorRejection = e => { 5 | // Used throughout the reference implementation, as `.catch(rethrowAssertionErrorRejection)`, to ensure any errors 6 | // get shown. There are places in the spec where we do promise transformations and purposefully ignore or don't 7 | // expect any errors, but assertion errors are always problematic. 8 | if (e && e.constructor === assert.AssertionError) { 9 | setTimeout(() => { 10 | throw e; 11 | }, 0); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /private/streams/writable-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const { InvokeOrNoop, PromiseInvokeOrNoop, ValidateAndNormalizeQueuingStrategy, typeIsObject } = 4 | require('./helpers.js'); 5 | const { rethrowAssertionErrorRejection } = require('./utils.js'); 6 | const { DequeueValue, EnqueueValueWithSize, PeekQueueValue, ResetQueue } = require('./queue-with-sizes.js'); 7 | 8 | const StartSteps = Symbol('[[StartSteps]]'); 9 | const AbortSteps = Symbol('[[AbortSteps]]'); 10 | const ErrorSteps = Symbol('[[ErrorSteps]]'); 11 | 12 | class WritableStream { 13 | constructor(underlyingSink = {}, { size, highWaterMark = 1 } = {}) { 14 | this._state = 'writable'; 15 | 16 | // The error that will be reported by new method calls once the state becomes errored. Only set when [[state]] is 17 | // 'erroring' or 'errored'. May be set to an undefined value. 18 | this._storedError = undefined; 19 | 20 | this._writer = undefined; 21 | 22 | // Initialize to undefined first because the constructor of the controller checks this 23 | // variable to validate the caller. 24 | this._writableStreamController = undefined; 25 | 26 | // This queue is placed here instead of the writer class in order to allow for passing a writer to the next data 27 | // producer without waiting for the queued writes to finish. 28 | this._writeRequests = []; 29 | 30 | // Write requests are removed from _writeRequests when write() is called on the underlying sink. This prevents 31 | // them from being erroneously rejected on error. If a write() call is in-flight, the request is stored here. 32 | this._inFlightWriteRequest = undefined; 33 | 34 | // The promise that was returned from writer.close(). Stored here because it may be fulfilled after the writer 35 | // has been detached. 36 | this._closeRequest = undefined; 37 | 38 | // Close request is removed from _closeRequest when close() is called on the underlying sink. This prevents it 39 | // from being erroneously rejected on error. If a close() call is in-flight, the request is stored here. 40 | this._inFlightCloseRequest = undefined; 41 | 42 | // The promise that was returned from writer.abort(). This may also be fulfilled after the writer has detached. 43 | this._pendingAbortRequest = undefined; 44 | 45 | // The backpressure signal set by the controller. 46 | this._backpressure = false; 47 | 48 | const type = underlyingSink.type; 49 | 50 | if (type !== undefined) { 51 | throw new RangeError('Invalid type is specified'); 52 | } 53 | 54 | this._writableStreamController = new WritableStreamDefaultController(this, underlyingSink, size, highWaterMark); 55 | this._writableStreamController[StartSteps](); 56 | } 57 | 58 | get locked() { 59 | if (IsWritableStream(this) === false) { 60 | throw streamBrandCheckException('locked'); 61 | } 62 | 63 | return IsWritableStreamLocked(this); 64 | } 65 | 66 | abort(reason) { 67 | if (IsWritableStream(this) === false) { 68 | return Promise.reject(streamBrandCheckException('abort')); 69 | } 70 | 71 | if (IsWritableStreamLocked(this) === true) { 72 | return Promise.reject(new TypeError('Cannot abort a stream that already has a writer')); 73 | } 74 | 75 | return WritableStreamAbort(this, reason); 76 | } 77 | 78 | getWriter() { 79 | if (IsWritableStream(this) === false) { 80 | throw streamBrandCheckException('getWriter'); 81 | } 82 | 83 | return AcquireWritableStreamDefaultWriter(this); 84 | } 85 | } 86 | 87 | module.exports = { 88 | AcquireWritableStreamDefaultWriter, 89 | IsWritableStream, 90 | IsWritableStreamLocked, 91 | WritableStream, 92 | WritableStreamAbort, 93 | WritableStreamDefaultControllerError, 94 | WritableStreamDefaultWriterCloseWithErrorPropagation, 95 | WritableStreamDefaultWriterRelease, 96 | WritableStreamDefaultWriterWrite, 97 | WritableStreamCloseQueuedOrInFlight 98 | }; 99 | 100 | // Abstract operations for the WritableStream. 101 | 102 | function AcquireWritableStreamDefaultWriter(stream) { 103 | return new WritableStreamDefaultWriter(stream); 104 | } 105 | 106 | function IsWritableStream(x) { 107 | if (!typeIsObject(x)) { 108 | return false; 109 | } 110 | 111 | if (!Object.prototype.hasOwnProperty.call(x, '_writableStreamController')) { 112 | return false; 113 | } 114 | 115 | return true; 116 | } 117 | 118 | function IsWritableStreamLocked(stream) { 119 | assert(IsWritableStream(stream) === true, 'IsWritableStreamLocked should only be used on known writable streams'); 120 | 121 | if (stream._writer === undefined) { 122 | return false; 123 | } 124 | 125 | return true; 126 | } 127 | 128 | function WritableStreamAbort(stream, reason) { 129 | const state = stream._state; 130 | if (state === 'closed') { 131 | return Promise.resolve(undefined); 132 | } 133 | if (state === 'errored') { 134 | return Promise.reject(stream._storedError); 135 | } 136 | const error = new TypeError('Requested to abort'); 137 | if (stream._pendingAbortRequest !== undefined) { 138 | return Promise.reject(error); 139 | } 140 | 141 | assert(state === 'writable' || state === 'erroring', 'state must be writable or erroring'); 142 | 143 | let wasAlreadyErroring = false; 144 | if (state === 'erroring') { 145 | wasAlreadyErroring = true; 146 | // reason will not be used, so don't keep a reference to it. 147 | reason = undefined; 148 | } 149 | 150 | const promise = new Promise((resolve, reject) => { 151 | stream._pendingAbortRequest = { 152 | _resolve: resolve, 153 | _reject: reject, 154 | _reason: reason, 155 | _wasAlreadyErroring: wasAlreadyErroring 156 | }; 157 | }); 158 | 159 | if (wasAlreadyErroring === false) { 160 | WritableStreamStartErroring(stream, error); 161 | } 162 | 163 | return promise; 164 | } 165 | 166 | // WritableStream API exposed for controllers. 167 | 168 | function WritableStreamAddWriteRequest(stream) { 169 | assert(IsWritableStreamLocked(stream) === true); 170 | assert(stream._state === 'writable'); 171 | 172 | const promise = new Promise((resolve, reject) => { 173 | const writeRequest = { 174 | _resolve: resolve, 175 | _reject: reject 176 | }; 177 | 178 | stream._writeRequests.push(writeRequest); 179 | }); 180 | 181 | return promise; 182 | } 183 | 184 | function WritableStreamDealWithRejection(stream, error) { 185 | const state = stream._state; 186 | 187 | if (state === 'writable') { 188 | WritableStreamStartErroring(stream, error); 189 | return; 190 | } 191 | 192 | assert(state === 'erroring'); 193 | WritableStreamFinishErroring(stream); 194 | } 195 | 196 | function WritableStreamStartErroring(stream, reason) { 197 | assert(stream._storedError === undefined, 'stream._storedError === undefined'); 198 | assert(stream._state === 'writable', 'state must be writable'); 199 | 200 | const controller = stream._writableStreamController; 201 | assert(controller !== undefined, 'controller must not be undefined'); 202 | 203 | stream._state = 'erroring'; 204 | stream._storedError = reason; 205 | const writer = stream._writer; 206 | if (writer !== undefined) { 207 | WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); 208 | } 209 | 210 | if (WritableStreamHasOperationMarkedInFlight(stream) === false && controller._started === true) { 211 | WritableStreamFinishErroring(stream); 212 | } 213 | } 214 | 215 | function WritableStreamFinishErroring(stream) { 216 | assert(stream._state === 'erroring', 'stream._state === erroring'); 217 | assert(WritableStreamHasOperationMarkedInFlight(stream) === false, 218 | 'WritableStreamHasOperationMarkedInFlight(stream) === false'); 219 | stream._state = 'errored'; 220 | stream._writableStreamController[ErrorSteps](); 221 | 222 | const storedError = stream._storedError; 223 | for (const writeRequest of stream._writeRequests) { 224 | writeRequest._reject(storedError); 225 | } 226 | stream._writeRequests = []; 227 | 228 | if (stream._pendingAbortRequest === undefined) { 229 | WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); 230 | return; 231 | } 232 | 233 | const abortRequest = stream._pendingAbortRequest; 234 | stream._pendingAbortRequest = undefined; 235 | 236 | if (abortRequest._wasAlreadyErroring === true) { 237 | abortRequest._reject(storedError); 238 | WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); 239 | return; 240 | } 241 | 242 | const promise = stream._writableStreamController[AbortSteps](abortRequest._reason); 243 | promise.then( 244 | () => { 245 | abortRequest._resolve(); 246 | WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); 247 | }, 248 | reason => { 249 | abortRequest._reject(reason); 250 | WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); 251 | }); 252 | } 253 | 254 | function WritableStreamFinishInFlightWrite(stream) { 255 | assert(stream._inFlightWriteRequest !== undefined); 256 | stream._inFlightWriteRequest._resolve(undefined); 257 | stream._inFlightWriteRequest = undefined; 258 | } 259 | 260 | function WritableStreamFinishInFlightWriteWithError(stream, error) { 261 | assert(stream._inFlightWriteRequest !== undefined); 262 | stream._inFlightWriteRequest._reject(error); 263 | stream._inFlightWriteRequest = undefined; 264 | 265 | assert(stream._state === 'writable' || stream._state === 'erroring'); 266 | 267 | WritableStreamDealWithRejection(stream, error); 268 | } 269 | 270 | function WritableStreamFinishInFlightClose(stream) { 271 | assert(stream._inFlightCloseRequest !== undefined); 272 | stream._inFlightCloseRequest._resolve(undefined); 273 | stream._inFlightCloseRequest = undefined; 274 | 275 | const state = stream._state; 276 | 277 | assert(state === 'writable' || state === 'erroring'); 278 | 279 | if (state === 'erroring') { 280 | // The error was too late to do anything, so it is ignored. 281 | stream._storedError = undefined; 282 | if (stream._pendingAbortRequest !== undefined) { 283 | stream._pendingAbortRequest._resolve(); 284 | stream._pendingAbortRequest = undefined; 285 | } 286 | } 287 | 288 | stream._state = 'closed'; 289 | 290 | const writer = stream._writer; 291 | if (writer !== undefined) { 292 | defaultWriterClosedPromiseResolve(writer); 293 | } 294 | 295 | assert(stream._pendingAbortRequest === undefined, 'stream._pendingAbortRequest === undefined'); 296 | assert(stream._storedError === undefined, 'stream._storedError === undefined'); 297 | } 298 | 299 | function WritableStreamFinishInFlightCloseWithError(stream, error) { 300 | assert(stream._inFlightCloseRequest !== undefined); 301 | stream._inFlightCloseRequest._reject(error); 302 | stream._inFlightCloseRequest = undefined; 303 | 304 | assert(stream._state === 'writable' || stream._state === 'erroring'); 305 | 306 | // Never execute sink abort() after sink close(). 307 | if (stream._pendingAbortRequest !== undefined) { 308 | stream._pendingAbortRequest._reject(error); 309 | stream._pendingAbortRequest = undefined; 310 | } 311 | WritableStreamDealWithRejection(stream, error); 312 | } 313 | 314 | // TODO(ricea): Fix alphabetical order. 315 | function WritableStreamCloseQueuedOrInFlight(stream) { 316 | if (stream._closeRequest === undefined && stream._inFlightCloseRequest === undefined) { 317 | return false; 318 | } 319 | 320 | return true; 321 | } 322 | 323 | function WritableStreamHasOperationMarkedInFlight(stream) { 324 | if (stream._inFlightWriteRequest === undefined && stream._inFlightCloseRequest === undefined) { 325 | return false; 326 | } 327 | 328 | return true; 329 | } 330 | 331 | function WritableStreamMarkCloseRequestInFlight(stream) { 332 | assert(stream._inFlightCloseRequest === undefined); 333 | assert(stream._closeRequest !== undefined); 334 | stream._inFlightCloseRequest = stream._closeRequest; 335 | stream._closeRequest = undefined; 336 | } 337 | 338 | function WritableStreamMarkFirstWriteRequestInFlight(stream) { 339 | assert(stream._inFlightWriteRequest === undefined, 'there must be no pending write request'); 340 | assert(stream._writeRequests.length !== 0, 'writeRequests must not be empty'); 341 | stream._inFlightWriteRequest = stream._writeRequests.shift(); 342 | } 343 | 344 | function WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { 345 | assert(stream._state === 'errored', '_stream_.[[state]] is `"errored"`'); 346 | if (stream._closeRequest !== undefined) { 347 | assert(stream._inFlightCloseRequest === undefined); 348 | 349 | stream._closeRequest._reject(stream._storedError); 350 | stream._closeRequest = undefined; 351 | } 352 | const writer = stream._writer; 353 | if (writer !== undefined) { 354 | defaultWriterClosedPromiseReject(writer, stream._storedError); 355 | writer._closedPromise.catch(() => {}); 356 | } 357 | } 358 | 359 | function WritableStreamUpdateBackpressure(stream, backpressure) { 360 | assert(stream._state === 'writable'); 361 | assert(WritableStreamCloseQueuedOrInFlight(stream) === false); 362 | 363 | const writer = stream._writer; 364 | if (writer !== undefined && backpressure !== stream._backpressure) { 365 | if (backpressure === true) { 366 | defaultWriterReadyPromiseReset(writer); 367 | } else { 368 | assert(backpressure === false); 369 | 370 | defaultWriterReadyPromiseResolve(writer); 371 | } 372 | } 373 | 374 | stream._backpressure = backpressure; 375 | } 376 | 377 | class WritableStreamDefaultWriter { 378 | constructor(stream) { 379 | if (IsWritableStream(stream) === false) { 380 | throw new TypeError('WritableStreamDefaultWriter can only be constructed with a WritableStream instance'); 381 | } 382 | if (IsWritableStreamLocked(stream) === true) { 383 | throw new TypeError('This stream has already been locked for exclusive writing by another writer'); 384 | } 385 | 386 | this._ownerWritableStream = stream; 387 | stream._writer = this; 388 | 389 | const state = stream._state; 390 | 391 | if (state === 'writable') { 392 | if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._backpressure === true) { 393 | defaultWriterReadyPromiseInitialize(this); 394 | } else { 395 | defaultWriterReadyPromiseInitializeAsResolved(this); 396 | } 397 | 398 | defaultWriterClosedPromiseInitialize(this); 399 | } else if (state === 'erroring') { 400 | defaultWriterReadyPromiseInitializeAsRejected(this, stream._storedError); 401 | this._readyPromise.catch(() => {}); 402 | defaultWriterClosedPromiseInitialize(this); 403 | } else if (state === 'closed') { 404 | defaultWriterReadyPromiseInitializeAsResolved(this); 405 | defaultWriterClosedPromiseInitializeAsResolved(this); 406 | } else { 407 | assert(state === 'errored', 'state must be errored'); 408 | 409 | const storedError = stream._storedError; 410 | defaultWriterReadyPromiseInitializeAsRejected(this, storedError); 411 | this._readyPromise.catch(() => {}); 412 | defaultWriterClosedPromiseInitializeAsRejected(this, storedError); 413 | this._closedPromise.catch(() => {}); 414 | } 415 | } 416 | 417 | get closed() { 418 | if (IsWritableStreamDefaultWriter(this) === false) { 419 | return Promise.reject(defaultWriterBrandCheckException('closed')); 420 | } 421 | 422 | return this._closedPromise; 423 | } 424 | 425 | get desiredSize() { 426 | if (IsWritableStreamDefaultWriter(this) === false) { 427 | throw defaultWriterBrandCheckException('desiredSize'); 428 | } 429 | 430 | if (this._ownerWritableStream === undefined) { 431 | throw defaultWriterLockException('desiredSize'); 432 | } 433 | 434 | return WritableStreamDefaultWriterGetDesiredSize(this); 435 | } 436 | 437 | get ready() { 438 | if (IsWritableStreamDefaultWriter(this) === false) { 439 | return Promise.reject(defaultWriterBrandCheckException('ready')); 440 | } 441 | 442 | return this._readyPromise; 443 | } 444 | 445 | abort(reason) { 446 | if (IsWritableStreamDefaultWriter(this) === false) { 447 | return Promise.reject(defaultWriterBrandCheckException('abort')); 448 | } 449 | 450 | if (this._ownerWritableStream === undefined) { 451 | return Promise.reject(defaultWriterLockException('abort')); 452 | } 453 | 454 | return WritableStreamDefaultWriterAbort(this, reason); 455 | } 456 | 457 | close() { 458 | if (IsWritableStreamDefaultWriter(this) === false) { 459 | return Promise.reject(defaultWriterBrandCheckException('close')); 460 | } 461 | 462 | const stream = this._ownerWritableStream; 463 | 464 | if (stream === undefined) { 465 | return Promise.reject(defaultWriterLockException('close')); 466 | } 467 | 468 | if (WritableStreamCloseQueuedOrInFlight(stream) === true) { 469 | return Promise.reject(new TypeError('cannot close an already-closing stream')); 470 | } 471 | 472 | return WritableStreamDefaultWriterClose(this); 473 | } 474 | 475 | releaseLock() { 476 | if (IsWritableStreamDefaultWriter(this) === false) { 477 | throw defaultWriterBrandCheckException('releaseLock'); 478 | } 479 | 480 | const stream = this._ownerWritableStream; 481 | 482 | if (stream === undefined) { 483 | return; 484 | } 485 | 486 | assert(stream._writer !== undefined); 487 | 488 | WritableStreamDefaultWriterRelease(this); 489 | } 490 | 491 | write(chunk) { 492 | if (IsWritableStreamDefaultWriter(this) === false) { 493 | return Promise.reject(defaultWriterBrandCheckException('write')); 494 | } 495 | 496 | if (this._ownerWritableStream === undefined) { 497 | return Promise.reject(defaultWriterLockException('write to')); 498 | } 499 | 500 | return WritableStreamDefaultWriterWrite(this, chunk); 501 | } 502 | } 503 | 504 | // Abstract operations for the WritableStreamDefaultWriter. 505 | 506 | function IsWritableStreamDefaultWriter(x) { 507 | if (!typeIsObject(x)) { 508 | return false; 509 | } 510 | 511 | if (!Object.prototype.hasOwnProperty.call(x, '_ownerWritableStream')) { 512 | return false; 513 | } 514 | 515 | return true; 516 | } 517 | 518 | // A client of WritableStreamDefaultWriter may use these functions directly to bypass state check. 519 | 520 | function WritableStreamDefaultWriterAbort(writer, reason) { 521 | const stream = writer._ownerWritableStream; 522 | 523 | assert(stream !== undefined); 524 | 525 | return WritableStreamAbort(stream, reason); 526 | } 527 | 528 | function WritableStreamDefaultWriterClose(writer) { 529 | const stream = writer._ownerWritableStream; 530 | 531 | assert(stream !== undefined); 532 | 533 | const state = stream._state; 534 | if (state === 'closed' || state === 'errored') { 535 | return Promise.reject(new TypeError( 536 | `The stream (in ${state} state) is not in the writable state and cannot be closed`)); 537 | } 538 | 539 | assert(state === 'writable' || state === 'erroring'); 540 | assert(WritableStreamCloseQueuedOrInFlight(stream) === false); 541 | 542 | const promise = new Promise((resolve, reject) => { 543 | const closeRequest = { 544 | _resolve: resolve, 545 | _reject: reject 546 | }; 547 | 548 | stream._closeRequest = closeRequest; 549 | }); 550 | 551 | if (stream._backpressure === true && state === 'writable') { 552 | defaultWriterReadyPromiseResolve(writer); 553 | } 554 | 555 | WritableStreamDefaultControllerClose(stream._writableStreamController); 556 | 557 | return promise; 558 | } 559 | 560 | 561 | function WritableStreamDefaultWriterCloseWithErrorPropagation(writer) { 562 | const stream = writer._ownerWritableStream; 563 | 564 | assert(stream !== undefined); 565 | 566 | const state = stream._state; 567 | if (WritableStreamCloseQueuedOrInFlight(stream) === true || state === 'closed') { 568 | return Promise.resolve(); 569 | } 570 | 571 | if (state === 'errored') { 572 | return Promise.reject(stream._storedError); 573 | } 574 | 575 | assert(state === 'writable' || state === 'erroring'); 576 | 577 | return WritableStreamDefaultWriterClose(writer); 578 | } 579 | 580 | function WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) { 581 | if (writer._closedPromiseState === 'pending') { 582 | defaultWriterClosedPromiseReject(writer, error); 583 | } else { 584 | defaultWriterClosedPromiseResetToRejected(writer, error); 585 | } 586 | writer._closedPromise.catch(() => {}); 587 | } 588 | 589 | function WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) { 590 | if (writer._readyPromiseState === 'pending') { 591 | defaultWriterReadyPromiseReject(writer, error); 592 | } else { 593 | defaultWriterReadyPromiseResetToRejected(writer, error); 594 | } 595 | writer._readyPromise.catch(() => {}); 596 | } 597 | 598 | function WritableStreamDefaultWriterGetDesiredSize(writer) { 599 | const stream = writer._ownerWritableStream; 600 | const state = stream._state; 601 | 602 | if (state === 'errored' || state === 'erroring') { 603 | return null; 604 | } 605 | 606 | if (state === 'closed') { 607 | return 0; 608 | } 609 | 610 | return WritableStreamDefaultControllerGetDesiredSize(stream._writableStreamController); 611 | } 612 | 613 | function WritableStreamDefaultWriterRelease(writer) { 614 | const stream = writer._ownerWritableStream; 615 | assert(stream !== undefined); 616 | assert(stream._writer === writer); 617 | 618 | const releasedError = new TypeError( 619 | 'Writer was released and can no longer be used to monitor the stream\'s closedness'); 620 | 621 | WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError); 622 | 623 | // The state transitions to "errored" before the sink abort() method runs, but the writer.closed promise is not 624 | // rejected until afterwards. This means that simply testing state will not work. 625 | WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError); 626 | 627 | stream._writer = undefined; 628 | writer._ownerWritableStream = undefined; 629 | } 630 | 631 | function WritableStreamDefaultWriterWrite(writer, chunk) { 632 | const stream = writer._ownerWritableStream; 633 | 634 | assert(stream !== undefined); 635 | 636 | const controller = stream._writableStreamController; 637 | 638 | const chunkSize = WritableStreamDefaultControllerGetChunkSize(controller, chunk); 639 | 640 | if (stream !== writer._ownerWritableStream) { 641 | return Promise.reject(defaultWriterLockException('write to')); 642 | } 643 | 644 | const state = stream._state; 645 | if (state === 'errored') { 646 | return Promise.reject(stream._storedError); 647 | } 648 | if (WritableStreamCloseQueuedOrInFlight(stream) === true || state === 'closed') { 649 | return Promise.reject(new TypeError('The stream is closing or closed and cannot be written to')); 650 | } 651 | if (state === 'erroring') { 652 | return Promise.reject(stream._storedError); 653 | } 654 | 655 | assert(state === 'writable'); 656 | 657 | const promise = WritableStreamAddWriteRequest(stream); 658 | 659 | WritableStreamDefaultControllerWrite(controller, chunk, chunkSize); 660 | 661 | return promise; 662 | } 663 | 664 | class WritableStreamDefaultController { 665 | constructor(stream, underlyingSink, size, highWaterMark) { 666 | if (IsWritableStream(stream) === false) { 667 | throw new TypeError('WritableStreamDefaultController can only be constructed with a WritableStream instance'); 668 | } 669 | 670 | if (stream._writableStreamController !== undefined) { 671 | throw new TypeError( 672 | 'WritableStreamDefaultController instances can only be created by the WritableStream constructor'); 673 | } 674 | 675 | this._controlledWritableStream = stream; 676 | 677 | this._underlyingSink = underlyingSink; 678 | 679 | // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly. 680 | this._queue = undefined; 681 | this._queueTotalSize = undefined; 682 | ResetQueue(this); 683 | 684 | this._started = false; 685 | 686 | const normalizedStrategy = ValidateAndNormalizeQueuingStrategy(size, highWaterMark); 687 | this._strategySize = normalizedStrategy.size; 688 | this._strategyHWM = normalizedStrategy.highWaterMark; 689 | 690 | const backpressure = WritableStreamDefaultControllerGetBackpressure(this); 691 | WritableStreamUpdateBackpressure(stream, backpressure); 692 | } 693 | 694 | error(e) { 695 | if (IsWritableStreamDefaultController(this) === false) { 696 | throw new TypeError( 697 | 'WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController'); 698 | } 699 | const state = this._controlledWritableStream._state; 700 | if (state !== 'writable') { 701 | // The stream is closed, errored or will be soon. The sink can't do anything useful if it gets an error here, so 702 | // just treat it as a no-op. 703 | return; 704 | } 705 | 706 | WritableStreamDefaultControllerError(this, e); 707 | } 708 | 709 | [AbortSteps](reason) { 710 | return PromiseInvokeOrNoop(this._underlyingSink, 'abort', [reason]); 711 | } 712 | 713 | [ErrorSteps]() { 714 | ResetQueue(this); 715 | } 716 | 717 | [StartSteps]() { 718 | const startResult = InvokeOrNoop(this._underlyingSink, 'start', [this]); 719 | const stream = this._controlledWritableStream; 720 | 721 | Promise.resolve(startResult).then( 722 | () => { 723 | assert(stream._state === 'writable' || stream._state === 'erroring'); 724 | this._started = true; 725 | WritableStreamDefaultControllerAdvanceQueueIfNeeded(this); 726 | }, 727 | r => { 728 | assert(stream._state === 'writable' || stream._state === 'erroring'); 729 | this._started = true; 730 | WritableStreamDealWithRejection(stream, r); 731 | } 732 | ) 733 | .catch(rethrowAssertionErrorRejection); 734 | } 735 | } 736 | 737 | // Abstract operations implementing interface required by the WritableStream. 738 | 739 | function WritableStreamDefaultControllerClose(controller) { 740 | EnqueueValueWithSize(controller, 'close', 0); 741 | WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); 742 | } 743 | 744 | function WritableStreamDefaultControllerGetChunkSize(controller, chunk) { 745 | const strategySize = controller._strategySize; 746 | 747 | if (strategySize === undefined) { 748 | return 1; 749 | } 750 | 751 | try { 752 | return strategySize(chunk); 753 | } catch (chunkSizeE) { 754 | WritableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE); 755 | return 1; 756 | } 757 | } 758 | 759 | function WritableStreamDefaultControllerGetDesiredSize(controller) { 760 | return controller._strategyHWM - controller._queueTotalSize; 761 | } 762 | 763 | function WritableStreamDefaultControllerWrite(controller, chunk, chunkSize) { 764 | const writeRecord = { chunk }; 765 | 766 | try { 767 | EnqueueValueWithSize(controller, writeRecord, chunkSize); 768 | } catch (enqueueE) { 769 | WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueE); 770 | return; 771 | } 772 | 773 | const stream = controller._controlledWritableStream; 774 | if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._state === 'writable') { 775 | const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); 776 | WritableStreamUpdateBackpressure(stream, backpressure); 777 | } 778 | 779 | WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); 780 | } 781 | 782 | // Abstract operations for the WritableStreamDefaultController. 783 | 784 | function IsWritableStreamDefaultController(x) { 785 | if (!typeIsObject(x)) { 786 | return false; 787 | } 788 | 789 | if (!Object.prototype.hasOwnProperty.call(x, '_underlyingSink')) { 790 | return false; 791 | } 792 | 793 | return true; 794 | } 795 | 796 | function WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller) { 797 | const stream = controller._controlledWritableStream; 798 | 799 | if (controller._started === false) { 800 | return; 801 | } 802 | 803 | if (stream._inFlightWriteRequest !== undefined) { 804 | return; 805 | } 806 | 807 | const state = stream._state; 808 | if (state === 'closed' || state === 'errored') { 809 | return; 810 | } 811 | if (state === 'erroring') { 812 | WritableStreamFinishErroring(stream); 813 | return; 814 | } 815 | 816 | if (controller._queue.length === 0) { 817 | return; 818 | } 819 | 820 | const writeRecord = PeekQueueValue(controller); 821 | if (writeRecord === 'close') { 822 | WritableStreamDefaultControllerProcessClose(controller); 823 | } else { 824 | WritableStreamDefaultControllerProcessWrite(controller, writeRecord.chunk); 825 | } 826 | } 827 | 828 | function WritableStreamDefaultControllerErrorIfNeeded(controller, error) { 829 | if (controller._controlledWritableStream._state === 'writable') { 830 | WritableStreamDefaultControllerError(controller, error); 831 | } 832 | } 833 | 834 | function WritableStreamDefaultControllerProcessClose(controller) { 835 | const stream = controller._controlledWritableStream; 836 | 837 | WritableStreamMarkCloseRequestInFlight(stream); 838 | 839 | DequeueValue(controller); 840 | assert(controller._queue.length === 0, 'queue must be empty once the final write record is dequeued'); 841 | 842 | const sinkClosePromise = PromiseInvokeOrNoop(controller._underlyingSink, 'close', []); 843 | sinkClosePromise.then( 844 | () => { 845 | WritableStreamFinishInFlightClose(stream); 846 | }, 847 | reason => { 848 | WritableStreamFinishInFlightCloseWithError(stream, reason); 849 | } 850 | ) 851 | .catch(rethrowAssertionErrorRejection); 852 | } 853 | 854 | function WritableStreamDefaultControllerProcessWrite(controller, chunk) { 855 | const stream = controller._controlledWritableStream; 856 | 857 | WritableStreamMarkFirstWriteRequestInFlight(stream); 858 | 859 | const sinkWritePromise = PromiseInvokeOrNoop(controller._underlyingSink, 'write', [chunk, controller]); 860 | sinkWritePromise.then( 861 | () => { 862 | WritableStreamFinishInFlightWrite(stream); 863 | 864 | const state = stream._state; 865 | assert(state === 'writable' || state === 'erroring'); 866 | 867 | DequeueValue(controller); 868 | 869 | if (WritableStreamCloseQueuedOrInFlight(stream) === false && state === 'writable') { 870 | const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); 871 | WritableStreamUpdateBackpressure(stream, backpressure); 872 | } 873 | 874 | WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); 875 | }, 876 | reason => { 877 | WritableStreamFinishInFlightWriteWithError(stream, reason); 878 | } 879 | ) 880 | .catch(rethrowAssertionErrorRejection); 881 | } 882 | 883 | function WritableStreamDefaultControllerGetBackpressure(controller) { 884 | const desiredSize = WritableStreamDefaultControllerGetDesiredSize(controller); 885 | return desiredSize <= 0; 886 | } 887 | 888 | // A client of WritableStreamDefaultController may use these functions directly to bypass state check. 889 | 890 | function WritableStreamDefaultControllerError(controller, error) { 891 | const stream = controller._controlledWritableStream; 892 | 893 | assert(stream._state === 'writable'); 894 | 895 | WritableStreamStartErroring(stream, error); 896 | } 897 | 898 | // Helper functions for the WritableStream. 899 | 900 | function streamBrandCheckException(name) { 901 | return new TypeError(`WritableStream.prototype.${name} can only be used on a WritableStream`); 902 | } 903 | 904 | // Helper functions for the WritableStreamDefaultWriter. 905 | 906 | function defaultWriterBrandCheckException(name) { 907 | return new TypeError( 908 | `WritableStreamDefaultWriter.prototype.${name} can only be used on a WritableStreamDefaultWriter`); 909 | } 910 | 911 | function defaultWriterLockException(name) { 912 | return new TypeError('Cannot ' + name + ' a stream using a released writer'); 913 | } 914 | 915 | function defaultWriterClosedPromiseInitialize(writer) { 916 | writer._closedPromise = new Promise((resolve, reject) => { 917 | writer._closedPromise_resolve = resolve; 918 | writer._closedPromise_reject = reject; 919 | writer._closedPromiseState = 'pending'; 920 | }); 921 | } 922 | 923 | function defaultWriterClosedPromiseInitializeAsRejected(writer, reason) { 924 | writer._closedPromise = Promise.reject(reason); 925 | writer._closedPromise_resolve = undefined; 926 | writer._closedPromise_reject = undefined; 927 | writer._closedPromiseState = 'rejected'; 928 | } 929 | 930 | function defaultWriterClosedPromiseInitializeAsResolved(writer) { 931 | writer._closedPromise = Promise.resolve(undefined); 932 | writer._closedPromise_resolve = undefined; 933 | writer._closedPromise_reject = undefined; 934 | writer._closedPromiseState = 'resolved'; 935 | } 936 | 937 | function defaultWriterClosedPromiseReject(writer, reason) { 938 | assert(writer._closedPromise_resolve !== undefined, 'writer._closedPromise_resolve !== undefined'); 939 | assert(writer._closedPromise_reject !== undefined, 'writer._closedPromise_reject !== undefined'); 940 | assert(writer._closedPromiseState === 'pending', 'writer._closedPromiseState is pending'); 941 | 942 | writer._closedPromise_reject(reason); 943 | writer._closedPromise_resolve = undefined; 944 | writer._closedPromise_reject = undefined; 945 | writer._closedPromiseState = 'rejected'; 946 | } 947 | 948 | function defaultWriterClosedPromiseResetToRejected(writer, reason) { 949 | assert(writer._closedPromise_resolve === undefined, 'writer._closedPromise_resolve === undefined'); 950 | assert(writer._closedPromise_reject === undefined, 'writer._closedPromise_reject === undefined'); 951 | assert(writer._closedPromiseState !== 'pending', 'writer._closedPromiseState is not pending'); 952 | 953 | writer._closedPromise = Promise.reject(reason); 954 | writer._closedPromiseState = 'rejected'; 955 | } 956 | 957 | function defaultWriterClosedPromiseResolve(writer) { 958 | assert(writer._closedPromise_resolve !== undefined, 'writer._closedPromise_resolve !== undefined'); 959 | assert(writer._closedPromise_reject !== undefined, 'writer._closedPromise_reject !== undefined'); 960 | assert(writer._closedPromiseState === 'pending', 'writer._closedPromiseState is pending'); 961 | 962 | writer._closedPromise_resolve(undefined); 963 | writer._closedPromise_resolve = undefined; 964 | writer._closedPromise_reject = undefined; 965 | writer._closedPromiseState = 'resolved'; 966 | } 967 | 968 | function defaultWriterReadyPromiseInitialize(writer) { 969 | writer._readyPromise = new Promise((resolve, reject) => { 970 | writer._readyPromise_resolve = resolve; 971 | writer._readyPromise_reject = reject; 972 | }); 973 | writer._readyPromiseState = 'pending'; 974 | } 975 | 976 | function defaultWriterReadyPromiseInitializeAsRejected(writer, reason) { 977 | writer._readyPromise = Promise.reject(reason); 978 | writer._readyPromise_resolve = undefined; 979 | writer._readyPromise_reject = undefined; 980 | writer._readyPromiseState = 'rejected'; 981 | } 982 | 983 | function defaultWriterReadyPromiseInitializeAsResolved(writer) { 984 | writer._readyPromise = Promise.resolve(undefined); 985 | writer._readyPromise_resolve = undefined; 986 | writer._readyPromise_reject = undefined; 987 | writer._readyPromiseState = 'fulfilled'; 988 | } 989 | 990 | function defaultWriterReadyPromiseReject(writer, reason) { 991 | assert(writer._readyPromise_resolve !== undefined, 'writer._readyPromise_resolve !== undefined'); 992 | assert(writer._readyPromise_reject !== undefined, 'writer._readyPromise_reject !== undefined'); 993 | 994 | writer._readyPromise_reject(reason); 995 | writer._readyPromise_resolve = undefined; 996 | writer._readyPromise_reject = undefined; 997 | writer._readyPromiseState = 'rejected'; 998 | } 999 | 1000 | function defaultWriterReadyPromiseReset(writer) { 1001 | assert(writer._readyPromise_resolve === undefined, 'writer._readyPromise_resolve === undefined'); 1002 | assert(writer._readyPromise_reject === undefined, 'writer._readyPromise_reject === undefined'); 1003 | 1004 | writer._readyPromise = new Promise((resolve, reject) => { 1005 | writer._readyPromise_resolve = resolve; 1006 | writer._readyPromise_reject = reject; 1007 | }); 1008 | writer._readyPromiseState = 'pending'; 1009 | } 1010 | 1011 | function defaultWriterReadyPromiseResetToRejected(writer, reason) { 1012 | assert(writer._readyPromise_resolve === undefined, 'writer._readyPromise_resolve === undefined'); 1013 | assert(writer._readyPromise_reject === undefined, 'writer._readyPromise_reject === undefined'); 1014 | 1015 | writer._readyPromise = Promise.reject(reason); 1016 | writer._readyPromiseState = 'rejected'; 1017 | } 1018 | 1019 | function defaultWriterReadyPromiseResolve(writer) { 1020 | assert(writer._readyPromise_resolve !== undefined, 'writer._readyPromise_resolve !== undefined'); 1021 | assert(writer._readyPromise_reject !== undefined, 'writer._readyPromise_reject !== undefined'); 1022 | 1023 | writer._readyPromise_resolve(undefined); 1024 | writer._readyPromise_resolve = undefined; 1025 | writer._readyPromise_reject = undefined; 1026 | writer._readyPromiseState = 'fulfilled'; 1027 | } 1028 | -------------------------------------------------------------------------------- /public/assets/templates/body.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | A Feed Reader 4 |

5 |
6 | $$ 7 | for(let column of it.columns) { 8 | yield P(column[1].then(() => '

' + column[0].name + '

')); 9 | yield P(column[1].then(items => (items != undefined) ? items.map(item => '

'+ item.title +' (#)

'+ item.pubDate +'
').join('') : '')); 10 | yield P('
'); 11 | } 12 | $$ 13 |
14 | 15 | -------------------------------------------------------------------------------- /public/assets/templates/foot.html: -------------------------------------------------------------------------------- 1 | 7 | 108 | -------------------------------------------------------------------------------- /public/assets/templates/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Feed Deck 5 | 6 | 7 | 8 | 21 | 92 | -------------------------------------------------------------------------------- /public/assets/templates/item.html: -------------------------------------------------------------------------------- 1 |
2 |

(#)

3 |
4 |
-------------------------------------------------------------------------------- /public/data/columns.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Chrome Releases", 4 | "href": "https://chromereleases.googleblog.com/", 5 | "feedUrl": "https://chromereleases.googleblog.com/feeds/posts/default?alt=rss" 6 | }, 7 | { 8 | "name": "Webkit Blog", 9 | "href": "https://webkit.org/blog/", 10 | "feedUrl": "https://webkit.org/feed/" 11 | }, 12 | { 13 | "name": "Paul Kinlan", 14 | "href": "https://paul.kinlan.me/", 15 | "feedUrl": "https://paul.kinlan.me/index.xml" 16 | }, 17 | { 18 | "name": "Paul Lewis", 19 | "href": "https://aerotwist.com/", 20 | "feedUrl": "https://aerotwist.com/blog/feed/" 21 | }, 22 | { 23 | "name": "Jake Archibald", 24 | "href": "https://jakearchibald.com/", 25 | "feedUrl": "https://jakearchibald.com/posts.rss" 26 | }, 27 | { 28 | "name": "Web Fundamentals", 29 | "href": "https://developers.google.com/web/updates", 30 | "feedUrl": "https://developers.google.com/web/updates/rss.xml" 31 | }, 32 | { 33 | "name": "Rob Dodson (Medium)", 34 | "href": "https://medium.com/@robdodson", 35 | "feedUrl": "https://medium.com/feed/@robdodson" 36 | }, 37 | { 38 | "name": "Addy Osmani (Medium)", 39 | "href": "https://medium.com/@addyosmani", 40 | "feedUrl": "https://medium.com/feed/@addyosmani" 41 | }, 42 | { 43 | "name": "Sam Thorogood (Medium)", 44 | "href": "https://medium.com/@samthor", 45 | "feedUrl": "https://medium.com/feed/@samthor" 46 | }, 47 | { 48 | "name": "Andre Bandarra (Medium)", 49 | "href": "https://medium.com/@andreban", 50 | "feedUrl": "https://medium.com/feed/@andreban" 51 | }, 52 | { 53 | "name": "Mustafa Kurtuldu (Medium)", 54 | "href": "https://medium.com/@mustafa_x", 55 | "feedUrl": "https://medium.com/feed/@mustafa_x" 56 | }, 57 | { 58 | "name": "Dave Gash (Medium)", 59 | "href": "https://medium.com/@davidagash", 60 | "feedUrl": "https://medium.com/feed/@davidagash" 61 | }, 62 | { 63 | "name": "Matt Gaunt (Medium)", 64 | "href": "https://medium.com/@gauntface", 65 | "feedUrl": "https://medium.com/feed/@gauntface" 66 | }, 67 | { 68 | "name": "Alex Russell (Medium)", 69 | "href": "https://medium.com/@slightlylate", 70 | "feedUrl": "https://medium.com/feed/@slightlylate" 71 | } 72 | ] -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Feeddeck", 3 | "name": "Feeddeck", 4 | "theme_color": "#fff", 5 | "background_color": "#fff", 6 | "icons": [ 7 | { 8 | "src": "https://cdn.glitch.com/d7915b95-ca73-4f83-b935-551cba915e01%2Fpwa-512.png?1495987074284", 9 | "type": "image/png", 10 | "sizes": "192x192" 11 | }, 12 | { 13 | "src": "https://cdn.glitch.com/d7915b95-ca73-4f83-b935-551cba915e01%2Fpwa-512.png?1495987074284", 14 | "type": "image/png", 15 | "sizes": "512x512" 16 | } 17 | ], 18 | "start_url": "/", 19 | "display": "standalone" 20 | } -------------------------------------------------------------------------------- /public/scripts/DOMParser.js: -------------------------------------------------------------------------------- 1 | (function(){require=function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=0){var lastIndex=list.length-1;while(i0},lookupPrefix:function(namespaceURI){var el=this;while(el){var map=el._nsMap;if(map){for(var n in map){if(map[n]==namespaceURI){return n}}}el=el.nodeType==ATTRIBUTE_NODE?el.ownerDocument:el.parentNode}return null},lookupNamespaceURI:function(prefix){var el=this;while(el){var map=el._nsMap;if(map){if(prefix in map){return map[prefix]}}el=el.nodeType==ATTRIBUTE_NODE?el.ownerDocument:el.parentNode}return null},isDefaultNamespace:function(namespaceURI){var prefix=this.lookupPrefix(namespaceURI);return prefix==null}};function _xmlEncoder(c){return c=="<"&&"<"||c==">"&&">"||c=="&"&&"&"||c=='"'&&"""||"&#"+c.charCodeAt()+";"}copy(NodeType,Node);copy(NodeType,Node.prototype);function _visitNode(node,callback){if(callback(node)){return true}if(node=node.firstChild){do{if(_visitNode(node,callback)){return true}}while(node=node.nextSibling)}}function Document(){}function _onAddAttribute(doc,el,newAttr){doc&&doc._inc++;var ns=newAttr.namespaceURI;if(ns=="http://www.w3.org/2000/xmlns/"){el._nsMap[newAttr.prefix?newAttr.localName:""]=newAttr.value}}function _onRemoveAttribute(doc,el,newAttr,remove){doc&&doc._inc++;var ns=newAttr.namespaceURI;if(ns=="http://www.w3.org/2000/xmlns/"){delete el._nsMap[newAttr.prefix?newAttr.localName:""]}}function _onUpdateChild(doc,el,newChild){if(doc&&doc._inc){doc._inc++;var cs=el.childNodes;if(newChild){cs[cs.length++]=newChild}else{var child=el.firstChild;var i=0;while(child){cs[i++]=child;child=child.nextSibling}cs.length=i}}}function _removeChild(parentNode,child){var previous=child.previousSibling;var next=child.nextSibling;if(previous){previous.nextSibling=next}else{parentNode.firstChild=next}if(next){next.previousSibling=previous}else{parentNode.lastChild=previous}_onUpdateChild(parentNode.ownerDocument,parentNode);return child}function _insertBefore(parentNode,newChild,nextChild){var cp=newChild.parentNode;if(cp){cp.removeChild(newChild)}if(newChild.nodeType===DOCUMENT_FRAGMENT_NODE){var newFirst=newChild.firstChild;if(newFirst==null){return newChild}var newLast=newChild.lastChild}else{newFirst=newLast=newChild}var pre=nextChild?nextChild.previousSibling:parentNode.lastChild;newFirst.previousSibling=pre;newLast.nextSibling=nextChild;if(pre){pre.nextSibling=newFirst}else{parentNode.firstChild=newFirst}if(nextChild==null){parentNode.lastChild=newLast}else{nextChild.previousSibling=newLast}do{newFirst.parentNode=parentNode}while(newFirst!==newLast&&(newFirst=newFirst.nextSibling));_onUpdateChild(parentNode.ownerDocument||parentNode,parentNode);if(newChild.nodeType==DOCUMENT_FRAGMENT_NODE){newChild.firstChild=newChild.lastChild=null}return newChild}function _appendSingleChild(parentNode,newChild){var cp=newChild.parentNode;if(cp){var pre=parentNode.lastChild;cp.removeChild(newChild);var pre=parentNode.lastChild}var pre=parentNode.lastChild;newChild.parentNode=parentNode;newChild.previousSibling=pre;newChild.nextSibling=null;if(pre){pre.nextSibling=newChild}else{parentNode.firstChild=newChild}parentNode.lastChild=newChild;_onUpdateChild(parentNode.ownerDocument,parentNode,newChild);return newChild}Document.prototype={nodeName:"#document",nodeType:DOCUMENT_NODE,doctype:null,documentElement:null,_inc:1,insertBefore:function(newChild,refChild){if(newChild.nodeType==DOCUMENT_FRAGMENT_NODE){var child=newChild.firstChild;while(child){var next=child.nextSibling;this.insertBefore(child,refChild);child=next}return newChild}if(this.documentElement==null&&newChild.nodeType==ELEMENT_NODE){this.documentElement=newChild}return _insertBefore(this,newChild,refChild),newChild.ownerDocument=this,newChild},removeChild:function(oldChild){if(this.documentElement==oldChild){this.documentElement=null}return _removeChild(this,oldChild)},importNode:function(importedNode,deep){return importNode(this,importedNode,deep)},getElementById:function(id){var rtv=null;_visitNode(this.documentElement,function(node){if(node.nodeType==ELEMENT_NODE){if(node.getAttribute("id")==id){rtv=node;return true}}});return rtv},createElement:function(tagName){var node=new Element;node.ownerDocument=this;node.nodeName=tagName;node.tagName=tagName;node.childNodes=new NodeList;var attrs=node.attributes=new NamedNodeMap;attrs._ownerElement=node;return node},createDocumentFragment:function(){var node=new DocumentFragment;node.ownerDocument=this;node.childNodes=new NodeList;return node},createTextNode:function(data){var node=new Text;node.ownerDocument=this;node.appendData(data);return node},createComment:function(data){var node=new Comment;node.ownerDocument=this;node.appendData(data);return node},createCDATASection:function(data){var node=new CDATASection;node.ownerDocument=this;node.appendData(data);return node},createProcessingInstruction:function(target,data){var node=new ProcessingInstruction;node.ownerDocument=this;node.tagName=node.target=target;node.nodeValue=node.data=data;return node},createAttribute:function(name){var node=new Attr;node.ownerDocument=this;node.name=name;node.nodeName=name;node.localName=name;node.specified=true;return node},createEntityReference:function(name){var node=new EntityReference;node.ownerDocument=this;node.nodeName=name;return node},createElementNS:function(namespaceURI,qualifiedName){var node=new Element;var pl=qualifiedName.split(":");var attrs=node.attributes=new NamedNodeMap;node.childNodes=new NodeList;node.ownerDocument=this;node.nodeName=qualifiedName;node.tagName=qualifiedName;node.namespaceURI=namespaceURI;if(pl.length==2){node.prefix=pl[0];node.localName=pl[1]}else{node.localName=qualifiedName}attrs._ownerElement=node;return node},createAttributeNS:function(namespaceURI,qualifiedName){var node=new Attr;var pl=qualifiedName.split(":");node.ownerDocument=this;node.nodeName=qualifiedName;node.name=qualifiedName;node.namespaceURI=namespaceURI;node.specified=true;if(pl.length==2){node.prefix=pl[0];node.localName=pl[1]}else{node.localName=qualifiedName}return node}};_extends(Document,Node);function Element(){this._nsMap={}}Element.prototype={nodeType:ELEMENT_NODE,hasAttribute:function(name){return this.getAttributeNode(name)!=null},getAttribute:function(name){var attr=this.getAttributeNode(name);return attr&&attr.value||""},getAttributeNode:function(name){return this.attributes.getNamedItem(name)},setAttribute:function(name,value){var attr=this.ownerDocument.createAttribute(name);attr.value=attr.nodeValue=""+value;this.setAttributeNode(attr)},removeAttribute:function(name){var attr=this.getAttributeNode(name);attr&&this.removeAttributeNode(attr)},appendChild:function(newChild){if(newChild.nodeType===DOCUMENT_FRAGMENT_NODE){return this.insertBefore(newChild,null)}else{return _appendSingleChild(this,newChild)}},setAttributeNode:function(newAttr){return this.attributes.setNamedItem(newAttr)},setAttributeNodeNS:function(newAttr){return this.attributes.setNamedItemNS(newAttr)},removeAttributeNode:function(oldAttr){return this.attributes.removeNamedItem(oldAttr.nodeName)},removeAttributeNS:function(namespaceURI,localName){var old=this.getAttributeNodeNS(namespaceURI,localName);old&&this.removeAttributeNode(old)},hasAttributeNS:function(namespaceURI,localName){return this.getAttributeNodeNS(namespaceURI,localName)!=null},getAttributeNS:function(namespaceURI,localName){var attr=this.getAttributeNodeNS(namespaceURI,localName);return attr&&attr.value||""},setAttributeNS:function(namespaceURI,qualifiedName,value){var attr=this.ownerDocument.createAttributeNS(namespaceURI,qualifiedName);attr.value=attr.nodeValue=""+value;this.setAttributeNode(attr)},getAttributeNodeNS:function(namespaceURI,localName){return this.attributes.getNamedItemNS(namespaceURI,localName)},getElementsByTagName:function(tagName){return new LiveNodeList(this,function(base){var ls=[];_visitNode(base,function(node){if(node!==base&&node.nodeType==ELEMENT_NODE&&(tagName==="*"||node.tagName==tagName)){ls.push(node)}});return ls})},getElementsByTagNameNS:function(namespaceURI,localName){return new LiveNodeList(this,function(base){var ls=[];_visitNode(base,function(node){if(node!==base&&node.nodeType===ELEMENT_NODE&&(namespaceURI==="*"||node.namespaceURI===namespaceURI)&&(localName==="*"||node.localName==localName)){ls.push(node)}});return ls})}};Document.prototype.getElementsByTagName=Element.prototype.getElementsByTagName;Document.prototype.getElementsByTagNameNS=Element.prototype.getElementsByTagNameNS;_extends(Element,Node);function Attr(){}Attr.prototype.nodeType=ATTRIBUTE_NODE;_extends(Attr,Node);function CharacterData(){}CharacterData.prototype={data:"",substringData:function(offset,count){return this.data.substring(offset,offset+count)},appendData:function(text){text=this.data+text;this.nodeValue=this.data=text;this.length=text.length},insertData:function(offset,text){this.replaceData(offset,0,text)},appendChild:function(newChild){throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR])},deleteData:function(offset,count){this.replaceData(offset,count,"")},replaceData:function(offset,count,text){var start=this.data.substring(0,offset);var end=this.data.substring(offset+count);text=start+text+end;this.nodeValue=this.data=text;this.length=text.length}};_extends(CharacterData,Node);function Text(){}Text.prototype={nodeName:"#text",nodeType:TEXT_NODE,splitText:function(offset){var text=this.data;var newText=text.substring(offset);text=text.substring(0,offset);this.data=this.nodeValue=text;this.length=text.length;var newNode=this.ownerDocument.createTextNode(newText);if(this.parentNode){this.parentNode.insertBefore(newNode,this.nextSibling)}return newNode}};_extends(Text,CharacterData);function Comment(){}Comment.prototype={nodeName:"#comment",nodeType:COMMENT_NODE};_extends(Comment,CharacterData);function CDATASection(){}CDATASection.prototype={nodeName:"#cdata-section",nodeType:CDATA_SECTION_NODE};_extends(CDATASection,CharacterData);function DocumentType(){}DocumentType.prototype.nodeType=DOCUMENT_TYPE_NODE;_extends(DocumentType,Node);function Notation(){}Notation.prototype.nodeType=NOTATION_NODE;_extends(Notation,Node);function Entity(){}Entity.prototype.nodeType=ENTITY_NODE;_extends(Entity,Node);function EntityReference(){}EntityReference.prototype.nodeType=ENTITY_REFERENCE_NODE;_extends(EntityReference,Node);function DocumentFragment(){}DocumentFragment.prototype.nodeName="#document-fragment";DocumentFragment.prototype.nodeType=DOCUMENT_FRAGMENT_NODE;_extends(DocumentFragment,Node);function ProcessingInstruction(){}ProcessingInstruction.prototype.nodeType=PROCESSING_INSTRUCTION_NODE;_extends(ProcessingInstruction,Node);function XMLSerializer(){}XMLSerializer.prototype.serializeToString=function(node,isHtml,nodeFilter){return nodeSerializeToString.call(node,isHtml,nodeFilter)};Node.prototype.toString=nodeSerializeToString;function nodeSerializeToString(isHtml,nodeFilter){var buf=[];var refNode=this.nodeType==9&&this.documentElement||this;var prefix=refNode.prefix;var uri=refNode.namespaceURI;if(uri&&prefix==null){var prefix=refNode.lookupPrefix(uri);if(prefix==null){var visibleNamespaces=[{namespace:uri,prefix:null}]}}serializeToString(this,buf,isHtml,nodeFilter,visibleNamespaces);return buf.join("")}function needNamespaceDefine(node,isHTML,visibleNamespaces){var prefix=node.prefix||"";var uri=node.namespaceURI;if(!prefix&&!uri){return false}if(prefix==="xml"&&uri==="http://www.w3.org/XML/1998/namespace"||uri=="http://www.w3.org/2000/xmlns/"){return false}var i=visibleNamespaces.length;while(i--){var ns=visibleNamespaces[i];if(ns.prefix==prefix){return ns.namespace!=uri}}return true}function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){if(nodeFilter){node=nodeFilter(node);if(node){if(typeof node=="string"){buf.push(node);return}}else{return}}switch(node.nodeType){case ELEMENT_NODE:if(!visibleNamespaces)visibleNamespaces=[];var startVisibleNamespaces=visibleNamespaces.length;var attrs=node.attributes;var len=attrs.length;var child=node.firstChild;var nodeName=node.tagName;isHTML=htmlns===node.namespaceURI||isHTML;buf.push("<",nodeName);for(var i=0;i");if(isHTML&&/^script$/i.test(nodeName)){while(child){if(child.data){buf.push(child.data)}else{serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces)}child=child.nextSibling}}else{while(child){serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces);child=child.nextSibling}}buf.push("")}else{buf.push("/>")}return;case DOCUMENT_NODE:case DOCUMENT_FRAGMENT_NODE:var child=node.firstChild;while(child){serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces);child=child.nextSibling}return;case ATTRIBUTE_NODE:return buf.push(" ",node.name,'="',node.value.replace(/[<&"]/g,_xmlEncoder),'"');case TEXT_NODE:return buf.push(node.data.replace(/[<&]/g,_xmlEncoder));case CDATA_SECTION_NODE:return buf.push("");case COMMENT_NODE:return buf.push("");case DOCUMENT_TYPE_NODE:var pubid=node.publicId;var sysid=node.systemId;buf.push("')}else if(sysid&&sysid!="."){buf.push(' SYSTEM "',sysid,'">')}else{var sub=node.internalSubset;if(sub){buf.push(" [",sub,"]")}buf.push(">")}return;case PROCESSING_INSTRUCTION_NODE:return buf.push("");case ENTITY_REFERENCE_NODE:return buf.push("&",node.nodeName,";");default:buf.push("??",node.nodeName)}}function importNode(doc,node,deep){var node2;switch(node.nodeType){case ELEMENT_NODE:node2=node.cloneNode(false);node2.ownerDocument=doc;case DOCUMENT_FRAGMENT_NODE:break;case ATTRIBUTE_NODE:deep=true;break}if(!node2){node2=node.cloneNode(false)}node2.ownerDocument=doc;node2.parentNode=null;if(deep){var child=node.firstChild;while(child){node2.appendChild(importNode(doc,child,deep));child=child.nextSibling}}return node2}function cloneNode(doc,node,deep){var node2=new node.constructor;for(var n in node){var v=node[n];if(typeof v!="object"){if(v!=node2[n]){node2[n]=v}}}if(node.childNodes){node2.childNodes=new NodeList}node2.ownerDocument=doc;switch(node2.nodeType){case ELEMENT_NODE:var attrs=node.attributes;var attrs2=node2.attributes=new NamedNodeMap;var len=attrs.length;attrs2._ownerElement=node2;for(var i=0;i",amp:"&",quot:'"',apos:"'",Agrave:"À",Aacute:"Á",Acirc:"Â",Atilde:"Ã",Auml:"Ä",Aring:"Å",AElig:"Æ",Ccedil:"Ç",Egrave:"È",Eacute:"É",Ecirc:"Ê",Euml:"Ë",Igrave:"Ì",Iacute:"Í",Icirc:"Î",Iuml:"Ï",ETH:"Ð",Ntilde:"Ñ",Ograve:"Ò",Oacute:"Ó",Ocirc:"Ô",Otilde:"Õ",Ouml:"Ö",Oslash:"Ø",Ugrave:"Ù",Uacute:"Ú",Ucirc:"Û",Uuml:"Ü",Yacute:"Ý",THORN:"Þ",szlig:"ß",agrave:"à",aacute:"á",acirc:"â",atilde:"ã",auml:"ä",aring:"å",aelig:"æ",ccedil:"ç",egrave:"è",eacute:"é",ecirc:"ê",euml:"ë",igrave:"ì",iacute:"í",icirc:"î",iuml:"ï",eth:"ð",ntilde:"ñ",ograve:"ò",oacute:"ó",ocirc:"ô",otilde:"õ",ouml:"ö",oslash:"ø",ugrave:"ù",uacute:"ú",ucirc:"û",uuml:"ü",yacute:"ý",thorn:"þ",yuml:"ÿ",nbsp:" ",iexcl:"¡",cent:"¢",pound:"£",curren:"¤",yen:"¥",brvbar:"¦",sect:"§",uml:"¨",copy:"©",ordf:"ª",laquo:"«",not:"¬",shy:"­­",reg:"®",macr:"¯",deg:"°",plusmn:"±",sup2:"²",sup3:"³",acute:"´",micro:"µ",para:"¶",middot:"·",cedil:"¸",sup1:"¹",ordm:"º",raquo:"»",frac14:"¼",frac12:"½",frac34:"¾",iquest:"¿",times:"×",divide:"÷",forall:"∀",part:"∂",exist:"∃",empty:"∅",nabla:"∇",isin:"∈",notin:"∉",ni:"∋",prod:"∏",sum:"∑",minus:"−",lowast:"∗",radic:"√",prop:"∝",infin:"∞",ang:"∠",and:"∧",or:"∨",cap:"∩",cup:"∪","int":"∫",there4:"∴",sim:"∼",cong:"≅",asymp:"≈",ne:"≠",equiv:"≡",le:"≤",ge:"≥",sub:"⊂",sup:"⊃",nsub:"⊄",sube:"⊆",supe:"⊇",oplus:"⊕",otimes:"⊗",perp:"⊥",sdot:"⋅",Alpha:"Α",Beta:"Β",Gamma:"Γ",Delta:"Δ",Epsilon:"Ε",Zeta:"Ζ",Eta:"Η",Theta:"Θ",Iota:"Ι",Kappa:"Κ",Lambda:"Λ",Mu:"Μ",Nu:"Ν",Xi:"Ξ",Omicron:"Ο",Pi:"Π",Rho:"Ρ",Sigma:"Σ",Tau:"Τ",Upsilon:"Υ",Phi:"Φ",Chi:"Χ",Psi:"Ψ",Omega:"Ω",alpha:"α",beta:"β",gamma:"γ",delta:"δ",epsilon:"ε",zeta:"ζ",eta:"η",theta:"θ",iota:"ι",kappa:"κ",lambda:"λ",mu:"μ",nu:"ν",xi:"ξ",omicron:"ο",pi:"π",rho:"ρ",sigmaf:"ς",sigma:"σ",tau:"τ",upsilon:"υ",phi:"φ",chi:"χ",psi:"ψ",omega:"ω",thetasym:"ϑ",upsih:"ϒ",piv:"ϖ",OElig:"Œ",oelig:"œ",Scaron:"Š",scaron:"š",Yuml:"Ÿ",fnof:"ƒ",circ:"ˆ",tilde:"˜",ensp:" ",emsp:" ",thinsp:" ",zwnj:"‌",zwj:"‍",lrm:"‎",rlm:"‏",ndash:"–",mdash:"—",lsquo:"‘",rsquo:"’",sbquo:"‚",ldquo:"“",rdquo:"”",bdquo:"„",dagger:"†",Dagger:"‡",bull:"•",hellip:"…",permil:"‰",prime:"′",Prime:"″",lsaquo:"‹",rsaquo:"›",oline:"‾",euro:"€",trade:"™",larr:"←",uarr:"↑",rarr:"→",darr:"↓",harr:"↔",crarr:"↵",lceil:"⌈",rceil:"⌉",lfloor:"⌊",rfloor:"⌋",loz:"◊",spades:"♠",clubs:"♣",hearts:"♥",diams:"♦"}},{}],3:[function(require,module,exports){var nameStartChar=/[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/;var nameChar=new RegExp("[\\-\\.0-9"+nameStartChar.source.slice(1,-1)+"\\u00B7\\u0300-\\u036F\\u203F-\\u2040]");var tagNamePattern=new RegExp("^"+nameStartChar.source+nameChar.source+"*(?::"+nameStartChar.source+nameChar.source+"*)?$");var S_TAG=0;var S_ATTR=1;var S_ATTR_SPACE=2;var S_EQ=3;var S_ATTR_NOQUOT_VALUE=4;var S_ATTR_END=5;var S_TAG_SPACE=6;var S_TAG_CLOSE=7;function XMLReader(){}XMLReader.prototype={parse:function(source,defaultNSMap,entityMap){var domBuilder=this.domBuilder;domBuilder.startDocument();_copy(defaultNSMap,defaultNSMap={});parse(source,defaultNSMap,entityMap,domBuilder,this.errorHandler);domBuilder.endDocument()}};function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){function fixedFromCharCode(code){if(code>65535){code-=65536;var surrogate1=55296+(code>>10),surrogate2=56320+(code&1023);return String.fromCharCode(surrogate1,surrogate2)}else{return String.fromCharCode(code)}}function entityReplacer(a){var k=a.slice(1,-1);if(k in entityMap){return entityMap[k]}else if(k.charAt(0)==="#"){return fixedFromCharCode(parseInt(k.substr(1).replace("x","0x")))}else{errorHandler.error("entity not found:"+a);return a}}function appendText(end){if(end>start){var xt=source.substring(start,end).replace(/&#?\w+;/g,entityReplacer);locator&&position(start);domBuilder.characters(xt,0,end-start);start=end}}function position(p,m){while(p>=lineEnd&&(m=linePattern.exec(source))){lineStart=m.index;lineEnd=lineStart+m[0].length;locator.lineNumber++}locator.columnNumber=p-lineStart+1}var lineStart=0;var lineEnd=0;var linePattern=/.*(?:\r\n?|\n)|.*$/g;var locator=domBuilder.locator;var parseStack=[{currentNSMap:defaultNSMapCopy}];var closeMap={};var start=0;while(true){try{var tagStart=source.indexOf("<",start);if(tagStart<0){if(!source.substr(start).match(/^\s*$/)){var doc=domBuilder.doc;var text=doc.createTextNode(source.substr(start));doc.appendChild(text);domBuilder.currentElement=text}return}if(tagStart>start){appendText(tagStart)}switch(source.charAt(tagStart+1)){case"/":var end=source.indexOf(">",tagStart+3);var tagName=source.substring(tagStart+2,end);var config=parseStack.pop();if(end<0){tagName=source.substring(tagStart+2).replace(/[\s<].*/,"");errorHandler.error("end tag name: "+tagName+" is not complete:"+config.tagName);end=tagStart+1+tagName.length}else if(tagName.match(/\sstart){start=end}else{appendText(Math.max(tagStart,start)+1)}}}function copyLocator(f,t){t.lineNumber=f.lineNumber;t.columnNumber=f.columnNumber;return t}function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,errorHandler){var attrName;var value;var p=++start;var s=S_TAG;while(true){var c=source.charAt(p);switch(c){case"=":if(s===S_ATTR){attrName=source.slice(start,p);s=S_EQ}else if(s===S_ATTR_SPACE){s=S_EQ}else{throw new Error("attribute equal must after attrName")}break;case"'":case'"':if(s===S_EQ||s===S_ATTR){if(s===S_ATTR){errorHandler.warning('attribute value must after "="');attrName=source.slice(start,p)}start=p+1;p=source.indexOf(c,start);if(p>0){value=source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);el.add(attrName,value,start-1);s=S_ATTR_END}else{throw new Error("attribute value no end '"+c+"' match")}}else if(s==S_ATTR_NOQUOT_VALUE){value=source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);el.add(attrName,value,start);errorHandler.warning('attribute "'+attrName+'" missed start quot('+c+")!!");start=p+1; 2 | s=S_ATTR_END}else{throw new Error('attribute value must after "="')}break;case"/":switch(s){case S_TAG:el.setTagName(source.slice(start,p));case S_ATTR_END:case S_TAG_SPACE:case S_TAG_CLOSE:s=S_TAG_CLOSE;el.closed=true;case S_ATTR_NOQUOT_VALUE:case S_ATTR:case S_ATTR_SPACE:break;default:throw new Error("attribute invalid close char('/')")}break;case"":errorHandler.error("unexpected end of input");if(s==S_TAG){el.setTagName(source.slice(start,p))}return p;case">":switch(s){case S_TAG:el.setTagName(source.slice(start,p));case S_ATTR_END:case S_TAG_SPACE:case S_TAG_CLOSE:break;case S_ATTR_NOQUOT_VALUE:case S_ATTR:value=source.slice(start,p);if(value.slice(-1)==="/"){el.closed=true;value=value.slice(0,-1)}case S_ATTR_SPACE:if(s===S_ATTR_SPACE){value=attrName}if(s==S_ATTR_NOQUOT_VALUE){errorHandler.warning('attribute "'+value+'" missed quot(")!!');el.add(attrName,value.replace(/&#?\w+;/g,entityReplacer),start)}else{if(currentNSMap[""]!=="http://www.w3.org/1999/xhtml"||!value.match(/^(?:disabled|checked|selected)$/i)){errorHandler.warning('attribute "'+value+'" missed value!! "'+value+'" instead!!')}el.add(value,value,start)}break;case S_EQ:throw new Error("attribute value missed!!")}return p;case"€":c=" ";default:if(c<=" "){switch(s){case S_TAG:el.setTagName(source.slice(start,p));s=S_TAG_SPACE;break;case S_ATTR:attrName=source.slice(start,p);s=S_ATTR_SPACE;break;case S_ATTR_NOQUOT_VALUE:var value=source.slice(start,p).replace(/&#?\w+;/g,entityReplacer);errorHandler.warning('attribute "'+value+'" missed quot(")!!');el.add(attrName,value,start);case S_ATTR_END:s=S_TAG_SPACE;break}}else{switch(s){case S_ATTR_SPACE:var tagName=el.tagName;if(currentNSMap[""]!=="http://www.w3.org/1999/xhtml"||!attrName.match(/^(?:disabled|checked|selected)$/i)){errorHandler.warning('attribute "'+attrName+'" missed value!! "'+attrName+'" instead2!!')}el.add(attrName,attrName,start);start=p;s=S_ATTR;break;case S_ATTR_END:errorHandler.warning('attribute space is required"'+attrName+'"!!');case S_TAG_SPACE:s=S_ATTR;start=p;break;case S_EQ:s=S_ATTR_NOQUOT_VALUE;start=p;break;case S_TAG_CLOSE:throw new Error("elements closed character '/' and '>' must be connected to")}}}p++}}function appendElement(el,domBuilder,currentNSMap){var tagName=el.tagName;var localNSMap=null;var i=el.length;while(i--){var a=el[i];var qName=a.qName;var value=a.value;var nsp=qName.indexOf(":");if(nsp>0){var prefix=a.prefix=qName.slice(0,nsp);var localName=qName.slice(nsp+1);var nsPrefix=prefix==="xmlns"&&localName}else{localName=qName;prefix=null;nsPrefix=qName==="xmlns"&&""}a.localName=localName;if(nsPrefix!==false){if(localNSMap==null){localNSMap={};_copy(currentNSMap,currentNSMap={})}currentNSMap[nsPrefix]=localNSMap[nsPrefix]=value;a.uri="http://www.w3.org/2000/xmlns/";domBuilder.startPrefixMapping(nsPrefix,value)}}var i=el.length;while(i--){a=el[i];var prefix=a.prefix;if(prefix){if(prefix==="xml"){a.uri="http://www.w3.org/XML/1998/namespace"}if(prefix!=="xmlns"){a.uri=currentNSMap[prefix||""]}}}var nsp=tagName.indexOf(":");if(nsp>0){prefix=el.prefix=tagName.slice(0,nsp);localName=el.localName=tagName.slice(nsp+1)}else{prefix=null;localName=el.localName=tagName}var ns=el.uri=currentNSMap[prefix||""];domBuilder.startElement(ns,localName,tagName,el);if(el.closed){domBuilder.endElement(ns,localName,tagName);if(localNSMap){for(prefix in localNSMap){domBuilder.endPrefixMapping(prefix)}}}else{el.currentNSMap=currentNSMap;el.localNSMap=localNSMap;return true}}function parseHtmlSpecialContent(source,elStartEnd,tagName,entityReplacer,domBuilder){if(/^(?:script|textarea)$/i.test(tagName)){var elEndStart=source.indexOf("",elStartEnd);var text=source.substring(elStartEnd+1,elEndStart);if(/[&<]/.test(text)){if(/^script$/i.test(tagName)){domBuilder.characters(text,0,text.length);return elEndStart}text=text.replace(/&#?\w+;/g,entityReplacer);domBuilder.characters(text,0,text.length);return elEndStart}}return elStartEnd+1}function fixSelfClosed(source,elStartEnd,tagName,closeMap){var pos=closeMap[tagName];if(pos==null){pos=source.lastIndexOf("");if(pos",start+4);if(end>start){domBuilder.comment(source,start+4,end-start-4);return end+3}else{errorHandler.error("Unclosed comment");return-1}}else{return-1}default:if(source.substr(start+3,6)=="CDATA["){var end=source.indexOf("]]>",start+9);domBuilder.startCDATA();domBuilder.characters(source,start+9,end-start-9);domBuilder.endCDATA();return end+3}var matchs=split(source,start);var len=matchs.length;if(len>1&&/!doctype/i.test(matchs[0][0])){var name=matchs[1][0];var pubid=len>3&&/^public$/i.test(matchs[2][0])&&matchs[3][0];var sysid=len>4&&matchs[4][0];var lastMatch=matchs[len-1];domBuilder.startDTD(name,pubid&&pubid.replace(/^(['"])(.*?)\1$/,"$2"),sysid&&sysid.replace(/^(['"])(.*?)\1$/,"$2"));domBuilder.endDTD();return lastMatch.index+lastMatch[0].length}}return-1}function parseInstruction(source,start,domBuilder){var end=source.indexOf("?>",start);if(end){var match=source.substring(start,end).match(/^<\?(\S*)\s*([\s\S]*?)\s*$/);if(match){var len=match[0].length;domBuilder.processingInstruction(match[1],match[2]);return end+2}else{return-1}}return-1}function ElementAttributes(source){}ElementAttributes.prototype={setTagName:function(tagName){if(!tagNamePattern.test(tagName)){throw new Error("invalid tagName:"+tagName)}this.tagName=tagName},add:function(qName,value,offset){if(!tagNamePattern.test(qName)){throw new Error("invalid attribute:"+qName)}this[this.length++]={qName:qName,value:value,offset:offset}},length:0,getLocalName:function(i){return this[i].localName},getLocator:function(i){return this[i].locator},getQName:function(i){return this[i].qName},getURI:function(i){return this[i].uri},getValue:function(i){return this[i].value}};function _set_proto_(thiz,parent){thiz.__proto__=parent;return thiz}if(!(_set_proto_({},_set_proto_.prototype)instanceof _set_proto_)){_set_proto_=function(thiz,parent){function p(){}p.prototype=parent;p=new p;for(parent in thiz){p[parent]=thiz[parent]}return p}}function split(source,start){var match;var buf=[];var reg=/'[^']+'|"[^"]+"|[^\s<>\/=]+=?|(\/?\s*>|<)/g;reg.lastIndex=start;reg.exec(source);while(match=reg.exec(source)){buf.push(match);if(match[1])return buf}}exports.XMLReader=XMLReader},{}],"xmldom-alpha":[function(require,module,exports){function DOMParser(options){this.options=options||{locator:{}}}DOMParser.prototype.parseFromString=function(source,mimeType){var options=this.options;var sax=new XMLReader;var domBuilder=options.domBuilder||new DOMHandler;var errorHandler=options.errorHandler;var locator=options.locator;var defaultNSMap=options.xmlns||{};var isHTML=/\/x?html?$/.test(mimeType);var entityMap=isHTML?htmlEntity.entityMap:{lt:"<",gt:">",amp:"&",quot:'"',apos:"'"};if(locator){domBuilder.setDocumentLocator(locator)}sax.errorHandler=buildErrorHandler(errorHandler,domBuilder,locator);sax.domBuilder=options.domBuilder||domBuilder;if(isHTML){defaultNSMap[""]="http://www.w3.org/1999/xhtml"}defaultNSMap.xml=defaultNSMap.xml||"http://www.w3.org/XML/1998/namespace";if(source){sax.parse(source,defaultNSMap,entityMap)}else{sax.errorHandler.error("invalid doc source")}return domBuilder.doc};function buildErrorHandler(errorImpl,domBuilder,locator){if(!errorImpl){if(domBuilder instanceof DOMHandler){return domBuilder}errorImpl=domBuilder}var errorHandler={};var isCallback=errorImpl instanceof Function;locator=locator||{};function build(key){var fn=errorImpl[key];if(!fn&&isCallback){fn=errorImpl.length==2?function(msg){errorImpl(key,msg)}:errorImpl}errorHandler[key]=fn&&function(msg){fn("[xmldom "+key+"] "+msg+_locator(locator))}||function(){}}build("warning");build("error");build("fatalError");return errorHandler}function DOMHandler(){this.cdata=false}function position(locator,node){node.lineNumber=locator.lineNumber;node.columnNumber=locator.columnNumber}DOMHandler.prototype={startDocument:function(){this.doc=(new DOMImplementation).createDocument(null,null,null);if(this.locator){this.doc.documentURI=this.locator.systemId}},startElement:function(namespaceURI,localName,qName,attrs){var doc=this.doc;var el=doc.createElementNS(namespaceURI,qName||localName);var len=attrs.length;appendElement(this,el);this.currentElement=el;this.locator&&position(this.locator,el);for(var i=0;i=start+length||start){return new java.lang.String(chars,start,length)+""}return chars}}"endDTD,startEntity,endEntity,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,resolveEntity,getExternalSubset,notationDecl,unparsedEntityDecl".replace(/\w+/g,function(key){DOMHandler.prototype[key]=function(){return null}});function appendElement(hander,node){if(!hander.currentElement){hander.doc.appendChild(node)}else{hander.currentElement.appendChild(node)}}var htmlEntity=require("./entities");var XMLReader=require("./sax").XMLReader;var DOMImplementation=exports.DOMImplementation=require("./dom").DOMImplementation;exports.XMLSerializer=require("./dom").XMLSerializer;exports.DOMParser=DOMParser},{"./dom":1,"./entities":2,"./sax":3}]},{},[]);require("xmldom-alpha")})(); -------------------------------------------------------------------------------- /public/scripts/dot.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * 3 | * Copyright 2016 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | (function (root, factory) { 19 | if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { 20 | factory(exports); 21 | } else { 22 | factory((root.doT = {})); 23 | } 24 | }(this, function (exports) { 25 | "use strict"; 26 | 27 | Object.assign(exports, { 28 | version: "1.1.1", 29 | templateSettings: { 30 | evaluate: /\{\{(([^\}]+|\\.)+)\}\}/g, 31 | interpolate: /\{\{=\s*([^\}]+)\}\}/g, 32 | stream: /\{\{~\s*([^\}]+)\}\}/g, 33 | conditional: /\{\{\?(\?)?\s*([^\}]*)?\}\}/g, 34 | node: typeof(process) === 'object', 35 | varname: "it", 36 | } 37 | }); 38 | 39 | function unescape(code) { 40 | return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, " "); 41 | } 42 | 43 | exports.compile = function(tmpl, c, def) { 44 | c = Object.assign({}, exports.templateSettings, c); 45 | var helpers = 46 | "var P=Promise.resolve.bind(Promise);" + 47 | "function* f(p,a,b){yield p.then(v=>(a=v?a:b)&&'');yield* (a||(_=>[]))();};"; 48 | var streamToGenerator; 49 | if (c.node) { 50 | streamToGenerator = 51 | `var s=r=>{ 52 | var d=!1,l,b=[]; 53 | r.then(r=>{ 54 | r.on('end',_=>{d=!0;l&&l()}); 55 | r.on('data',c=>(l&&(v=>{var t=l;l=null;t(v)})||(d=>b.push(d)))(c)); 56 | }); 57 | return i={next:_=>({done:b.length===0&&d,value:P(b.shift()||new Promise(r=>l=r))}),[Symbol.iterator]:_=>i};};`; 58 | } else { 59 | streamToGenerator = 60 | `var s=r=>{ 61 | r=r.then(l=>l.getReader()); 62 | var d=!1; 63 | return i={next:_=>({done:d,value:r.then(r=>r.read()).then(v=>{d=v.done;return P(v.value)})}),[Symbol.iterator]:_=>i}; 64 | };`; 65 | } 66 | 67 | tmpl = helpers + streamToGenerator + 68 | "var g=function*(){yield P('" 69 | + tmpl 70 | .replace(/'|\\/g, "\\$&") 71 | .replace(c.interpolate, function(_, code) { 72 | return "');yield P(" + unescape(code) + ");yield P('"; 73 | }) 74 | .replace(c.conditional, function(_, els, code) { 75 | if (code && !els) { // {{?}} === if 76 | return "');yield* f(P(" + unescape(code) + "),function*(){yield P('" 77 | } else if (!code && els) { // {{??}} === else 78 | return "')},function*(){yield P('"; 79 | } else { // {{?}} === "endif" 80 | return "')});yield P('"; 81 | } 82 | }) 83 | .replace(c.stream, function(_, code) { 84 | return "');yield* s(P(" + unescape(code) + "));yield P('"; 85 | }) 86 | .replace(c.evaluate, function(_, code) { 87 | return "');" + unescape(code) + ";yield P('"; 88 | }) 89 | .replace(/\n/g, "\\n") 90 | .replace(/\t/g, '\\t') 91 | .replace(/\r/g, "\\r") 92 | + "');}();"; 93 | 94 | if(c.node) { 95 | tmpl += 96 | `var r = new R({read:function f() { 97 | var d=g.next(); 98 | if(d.done) return r.push(null); 99 | P(d.value).then(v=>{if(v)return r.push(Buffer.from(v));else f()}); 100 | }}); 101 | return r; 102 | `; 103 | } else { 104 | tmpl += 105 | `var e=new TextEncoder(); 106 | return new ReadableStream({ 107 | pull: c=>{ 108 | var v=g.next(); 109 | if(v.done)return c.close(); 110 | v.value.then(d=>{ 111 | if(typeof(d)=="string")d=e.encode(d); 112 | d&&c.enqueue(d); 113 | }); 114 | return v.value; 115 | }});`; 116 | } 117 | 118 | try { 119 | if (c.noEval) return tmpl; 120 | if (c.node) { 121 | const f = new Function(c.varname, 'R', tmpl); 122 | return it => f(it, require('stream').Readable); 123 | } 124 | return new Function(c.varname, tmpl); 125 | } catch (e) { 126 | console.log("Could not create a template function: " + tmpl); 127 | throw e; 128 | } 129 | }; 130 | })); -------------------------------------------------------------------------------- /public/scripts/platform/common.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined' && module.exports) { 2 | var doT = require('../dot.js'); 3 | let node = require('./node.js'); 4 | var loadTemplate = node.loadTemplate; 5 | var streamToString = node.streamToString; 6 | var compileTemplate = node.compileTemplate; 7 | var ReadableStream = require('../../../private/streams/readable-stream.js').ReadableStream; 8 | var WritableStream = require('../../../private/streams/writable-stream.js').WritableStream; 9 | var TransformStream = require('../../../private/streams/transform-stream.js').TransformStream; 10 | } 11 | 12 | let templates = {}; 13 | 14 | function getCompiledTemplate(template) { 15 | if(template in templates) return Promise.resolve(templates[template]); 16 | return compileTemplate(template).then(templateFunction => templates[template] = templateFunction); 17 | } 18 | 19 | function streamToString(stream) { 20 | const reader = stream.getReader(); 21 | let buffer = new Uint8Array(); 22 | let resolve; 23 | let reject; 24 | 25 | const promise = new Promise((res, rej) => { 26 | resolve=res; 27 | reject=rej; 28 | }); 29 | 30 | function pull() { 31 | return reader.read().then(({value, done}) => { 32 | if(done) { 33 | const decoder = new TextDecoder(); 34 | return resolve(decoder.decode(buffer)); 35 | } 36 | 37 | let newBuffer = new Uint8Array(buffer.length + value.length); 38 | newBuffer.set(buffer); 39 | newBuffer.set(value, buffer.length); 40 | buffer = newBuffer; 41 | 42 | return pull(); 43 | }, e => reject(e)); 44 | } 45 | 46 | pull(); 47 | 48 | return promise; 49 | } 50 | 51 | var ConcatStream = function() { 52 | let readableController; 53 | this.readable = new ReadableStream({ 54 | start(controller) { 55 | readableController = controller; 56 | }, 57 | abort(reason) { 58 | this.writable.abort(reason); 59 | } 60 | }); 61 | this.writable = new WritableStream({ 62 | write(chunks) { 63 | readableController.enqueue(chunks); 64 | }, 65 | close() { 66 | readableController.close(); 67 | }, 68 | abort(reason) { 69 | readableController.error(new Error(reason)); 70 | } 71 | }) 72 | }; 73 | 74 | if (typeof module !== 'undefined' && module.exports) { 75 | module.exports = { 76 | ConcatStream: ConcatStream, 77 | getCompiledTemplate: getCompiledTemplate, 78 | compileTemplate: compileTemplate 79 | } 80 | } -------------------------------------------------------------------------------- /public/scripts/platform/node.js: -------------------------------------------------------------------------------- 1 | const doT = require('../dot.js'); 2 | const fs = require('fs'); 3 | const fetch = require('node-fetch'); 4 | const Response = fetch.Response; 5 | const TextDecoder = require('text-encoding').TextDecoder; 6 | const Readable = require('stream').Readable; 7 | 8 | const ReadableStream = require('../../../private/streams/readable-stream.js').ReadableStream; 9 | const WritableStream = require('../../../private/streams/writable-stream.js').WritableStream; 10 | const TransformStream = require('../../../private/streams/transform-stream.js').TransformStream; 11 | 12 | /* 13 | This file is basically me futzing about with Streams between Node and WhatWG 14 | */ 15 | function streamToString(stream) { 16 | const reader = stream.getReader(); 17 | let buffer = new Uint8Array(); 18 | let resolve; 19 | let reject; 20 | 21 | const promise = new Promise((res, rej) => { 22 | resolve=res; 23 | reject=rej; 24 | }); 25 | 26 | function pull() { 27 | return reader.read().then(({value, done}) => { 28 | if(done) { 29 | const decoder = new TextDecoder(); 30 | return resolve(decoder.decode(buffer)); 31 | } 32 | 33 | let newBuffer = new Uint8Array(buffer.length + value.length); 34 | newBuffer.set(buffer); 35 | newBuffer.set(value, buffer.length); 36 | buffer = newBuffer; 37 | 38 | return pull(); 39 | }, e => reject(e)); 40 | } 41 | 42 | pull(); 43 | 44 | return promise; 45 | } 46 | 47 | const sendStream = (stream, last, res) => { 48 | stream.on('data', (data) => { 49 | res.write(data); 50 | }); 51 | 52 | return new Promise((resolve, reject)=> { 53 | stream.on('end', () => { 54 | if(last) { res.end(); } 55 | resolve(); 56 | }); 57 | 58 | stream.on('error', () => { 59 | res.end(); 60 | reject(); 61 | }) 62 | }); 63 | }; 64 | 65 | const nodeReadStreamToWhatWGReadableStream = (stream) => { 66 | 67 | return new ReadableStream({ 68 | start(controller) { 69 | stream.on('data', data => { 70 | controller.enqueue(data) 71 | }); 72 | stream.on('error', (error) => controller.abort(error)) 73 | stream.on('end', () => { 74 | controller.close(); 75 | }) 76 | } 77 | }); 78 | }; 79 | 80 | class FromWhatWGReadableStream extends Readable { 81 | constructor(options, whatwgStream) { 82 | super(options); 83 | const streamReader = whatwgStream.getReader(); 84 | const outStream = this; 85 | 86 | function pump() { 87 | return streamReader.read().then(({ value, done }) => { 88 | if (done) { 89 | outStream.push(null); 90 | return; 91 | } 92 | 93 | outStream.push(value.toString()); 94 | return pump(); 95 | }); 96 | } 97 | 98 | pump(); 99 | } 100 | 101 | _read(size) { 102 | 103 | } 104 | } 105 | 106 | const loadTemplate = (path) => { 107 | return Promise.resolve(nodeReadStreamToWhatWGReadableStream(fs.createReadStream(path))); 108 | }; 109 | 110 | const loadData = (path) => { 111 | return Promise.resolve(new Response(fs.createReadStream(path))); 112 | }; 113 | 114 | function compileTemplate(path) { 115 | return loadTemplate(path) 116 | .then(stream => streamToString(stream)) 117 | .then(template => { 118 | const f = doT.compile(template, {node: true, evaluate: /\$\$(([^\$]+|\\.)+)\$\$/g}); 119 | return (data) => nodeReadStreamToWhatWGReadableStream(f(data)); 120 | }); 121 | } 122 | 123 | const responseToExpressStream = (expressResponse, fetchResponseStream) => { 124 | const stream = new FromWhatWGReadableStream({}, fetchResponseStream); 125 | stream.pipe(expressResponse, {end:true}); 126 | }; 127 | 128 | module.exports = { 129 | compileTemplate: compileTemplate, 130 | FromWhatWGReadableStream: FromWhatWGReadableStream, 131 | loadTemplate: loadTemplate, 132 | loadData: loadData, 133 | responseToExpressStream: responseToExpressStream, 134 | streamToString: streamToString, 135 | sendStream: sendStream 136 | }; -------------------------------------------------------------------------------- /public/scripts/platform/web.js: -------------------------------------------------------------------------------- 1 | /* 2 | The web versions of loading the data. 3 | */ 4 | 5 | var loadTemplate = (path) => { 6 | // Always return the cached asset, before hitting the network as a fallback 7 | return caches.match(new Request(path)) 8 | .then(response => { 9 | return response || fetch(new Request(path)) 10 | }) 11 | .then(response => response.body); 12 | }; 13 | 14 | var loadData = (path) => { 15 | const request = new Request(path); 16 | // Always return the cached asset, before hitting the network as a fallback 17 | return caches.open('data').then((cache) => { 18 | return cache.match(request.clone()).then(response => { 19 | const networkResource = fetch(path).then((networkResponse) => { 20 | cache.put(path, networkResponse.clone()); 21 | return networkResponse; 22 | }) 23 | .catch(error => {}); 24 | 25 | return response || networkResource; 26 | }) 27 | }) 28 | } 29 | 30 | function compileTemplate(path) { 31 | return loadTemplate(path) 32 | .then(stream => streamToString(stream)) 33 | .then(template => doT.compile(template, {node: false, evaluate: /\$\$(([^\$]+|\\.)+)\$\$/g})); 34 | } -------------------------------------------------------------------------------- /public/scripts/router.js: -------------------------------------------------------------------------------- 1 | const FetchRouter = function() { 2 | const _routes = { 3 | get: [], 4 | post: [] 5 | }; 6 | 7 | this.parseRoute = function(path) { 8 | this.parseGroups = function(loc) { 9 | var nameRegexp = new RegExp(":([^/.\\\\]+)", "g"); 10 | var newRegexp = "" + loc; 11 | var groups = {}; 12 | var matches = null; 13 | var i = 0; 14 | 15 | // Find the places to edit. 16 | while(matches = nameRegexp.exec(loc)) { 17 | groups[matches[1]] = i++; 18 | newRegexp = newRegexp.replace(matches[0], "([^/.\\\\]+)"); 19 | } 20 | 21 | //newRegexp += "$"; // Only do a full string match 22 | 23 | return { "groups" : groups, "regexp": new RegExp(newRegexp)}; 24 | }; 25 | 26 | return this.parseGroups(path); 27 | }; 28 | 29 | var matchRoute = function(url, type) { 30 | var route = null; 31 | const filteredType = type.toLowerCase(); 32 | 33 | if(filteredType in _routes === false) { 34 | return ; // Reject 35 | } 36 | 37 | for(let i = 0; route = _routes[filteredType][i]; i ++) { 38 | const urlMatchProperty = route.options.urlMatchProperty 39 | const urlPart = (urlMatchProperty in url) ? url[urlMatchProperty] : url.toString(); 40 | const routeMatch = route.regex.regexp.exec(urlPart); 41 | 42 | if(!!routeMatch == false) continue; 43 | 44 | var params = {}; 45 | for(var g in route.regex.groups) { 46 | var group = route.regex.groups[g]; 47 | params[g] = routeMatch[group + 1]; 48 | } 49 | 50 | route.params = params; 51 | 52 | return route; 53 | } 54 | 55 | return; 56 | }; 57 | 58 | this.registerRoute = function(method, route, handler, options) { 59 | let regex; 60 | 61 | if(route instanceof RegExp) { 62 | regex = {regexp: route}; 63 | } 64 | else if (typeof(route) === "string") { 65 | regex = this.parseRoute(route); 66 | } 67 | 68 | _routes[method].push({regex: regex, callback: handler, options: options || {} }); 69 | }; 70 | 71 | this.get = function(route, handler, options) { 72 | this.registerRoute("get", route, handler, options); 73 | }; 74 | 75 | this.post = function(route, handler, options) { 76 | this.registerRoute("get", route, handler, options); 77 | }; 78 | 79 | this.findRoute = function(url, type) { 80 | return matchRoute(url, type); 81 | }; 82 | }; 83 | 84 | const router = new FetchRouter(); 85 | 86 | self.addEventListener('fetch', function(event) { 87 | const request = event.request; 88 | const url = new URL(event.request.url); 89 | const method = event.request.method; 90 | 91 | const executor = router.findRoute(url, method); 92 | 93 | if(executor) { 94 | executor.callback(event, { params: executor.params }); 95 | } 96 | }); 97 | 98 | /* 99 | thoughts: 100 | want to intercept scheme origin port path 101 | want to be able just to manage the requests for path 102 | */ -------------------------------------------------------------------------------- /public/scripts/routes/index.js: -------------------------------------------------------------------------------- 1 | const routes = {}; 2 | 3 | if (typeof module !== 'undefined' && module.exports) { 4 | const rootHandler = require('./root.js'); 5 | const proxyHandler = require('./proxy.js'); 6 | 7 | module.exports = { 8 | root: rootHandler.handler, 9 | proxy: proxyHandler.handler 10 | } 11 | } -------------------------------------------------------------------------------- /public/scripts/routes/proxy.js: -------------------------------------------------------------------------------- 1 | const proxyHandler = (dataPath, assetPath, request) => { 2 | /* 3 | Go out to the networks. 4 | */ 5 | const url = parseUrl(request); 6 | 7 | var fetchPromise = fetch(url).then(networkResponse => { 8 | if(networkResponse.ok) 9 | return caches.open('data') 10 | .then(cache => (!!cache) ? cache.put(request, networkResponse.clone()) : undefined) 11 | .then(_ => networkResponse); 12 | return networkResponse; 13 | }).catch(error => { 14 | console.log("Fetch Error", error); 15 | throw error; 16 | }); 17 | 18 | return caches.open('data').then(cache => { 19 | // There is no cache, just hit the network. 20 | if (cache === undefined) return fetchPromise; 21 | 22 | return cache.match(request.clone()).then(response => { 23 | // Return the cache or the fetch if not there. 24 | return response || fetchPromise; 25 | }); 26 | }).catch(error => { 27 | console.log("Error in SW", error); 28 | throw error; 29 | }); 30 | } 31 | 32 | if (typeof module !== 'undefined' && module.exports) { 33 | var platform = require('../../scripts/platform/node.js'); 34 | var common = require('../../scripts/platform/common.js'); 35 | var loadTemplate = platform.loadTemplate; 36 | var loadData = platform.loadData; 37 | var getCompiledTemplate = common.getCompiledTemplate; 38 | var ConcatStream = common.ConcatStream; 39 | var fetch = require('node-fetch'); 40 | var Request = fetch.Request; 41 | var Response = fetch.Response; 42 | 43 | var parseUrl = request => request.query.url; 44 | 45 | // Really need a Cache API on the server..... 46 | caches = new (function() { 47 | this.open = () => { 48 | return Promise.resolve(undefined); 49 | }; 50 | }); 51 | 52 | module.exports = { 53 | handler: proxyHandler 54 | } 55 | } 56 | else { 57 | routes['proxy'] = proxyHandler; 58 | 59 | var parseUrl = request => request.url; //new URL(request.url).searchParams.get('url'); 60 | } -------------------------------------------------------------------------------- /public/scripts/routes/root.js: -------------------------------------------------------------------------------- 1 | // handler 2 | const root = (dataPath, assetPath) => { 3 | 4 | let columnData = loadData(`${dataPath}columns.json`).then(r => r.json()); 5 | 6 | 7 | let headTemplate = getCompiledTemplate(`${assetPath}templates/head.html`); 8 | let bodyTemplate = getCompiledTemplate(`${assetPath}templates/body.html`); 9 | let itemTemplate = getCompiledTemplate(`${assetPath}templates/item.html`); 10 | 11 | let jsonFeedData = fetchCachedFeedData(columnData, itemTemplate); 12 | 13 | /* 14 | * Render the head from the cache or network 15 | * Render the body. 16 | * Body has template that brings in config to work out what to render 17 | * If we have data cached let's bring that in. 18 | * Render the footer - contains JS to data bind client request. 19 | */ 20 | 21 | const headStream = headTemplate.then(render => render({ columns: columnData })); 22 | const bodyStream = jsonFeedData.then(columns => bodyTemplate.then(render => render({ columns: columns }))); 23 | const footStream = loadTemplate(`${assetPath}templates/foot.html`); 24 | 25 | let concatStream = new ConcatStream; 26 | 27 | headStream.then(stream => stream.pipeTo(concatStream.writable, { preventClose:true })) 28 | .then(() => bodyStream) 29 | .then(stream => stream.pipeTo(concatStream.writable, { preventClose: true })) 30 | .then(() => footStream) 31 | .then(stream => stream.pipeTo(concatStream.writable)); 32 | 33 | return Promise.resolve(new Response(concatStream.readable, { status: "200" })) 34 | } 35 | 36 | 37 | // Helpers 38 | const fetchCachedFeedData = (columnData, itemTemplate) => { 39 | // Return a promise that resolves to a map of column id => cached data. 40 | const resolveCache = (cache, url) => (!!cache) ? cache.match(new Request(url)).then(response => (!!response) ? response.text() : undefined) : Promise.resolve(); 41 | const mapColumnsToCache = (cache, columns) => columns.map(column => [column, resolveCache(cache, `https://chromestatus-deck.glitch.me/proxy?url=${column.feedUrl}`)]); 42 | const mapCacheToTemplate = (columns) => columns.map(column => [column[0], column[1].then(items => convertFeedItemsToJSON(items))]); 43 | 44 | return caches.open('data') 45 | .then(cache => columnData.then(columns => mapColumnsToCache(cache, columns))) 46 | .then(columns => mapCacheToTemplate(columns)); 47 | }; 48 | 49 | const findNode = (tagName, nodes) => { 50 | return Array.prototype.find.call(nodes, n => n.tagName == tagName); 51 | } 52 | 53 | const findNodes = (tagName, nodes) => { 54 | return Array.prototype.filter.call(nodes, n => n.tagName == tagName); 55 | } 56 | 57 | const sanitize = (str) => { 58 | const tagsToReplace = { 59 | '&': '&', 60 | '<': '<', 61 | '>': '>' 62 | }; 63 | return str.replace(/[&<>]/g, (tag) => tagsToReplace[tag] || tag); 64 | } 65 | 66 | const convertFeedItemsToJSON = (feedText) => { 67 | if(feedText === undefined) return []; 68 | 69 | const parser = new DOMParser(); 70 | const feed = parser.parseFromString(feedText,'application/xml'); 71 | const documentElement = feed.documentElement; 72 | 73 | if(documentElement.nodeName === 'rss') { 74 | const channel = findNode('channel', documentElement.childNodes); 75 | const items = findNodes('item', channel.childNodes); 76 | return items.map(item => convertRSSItemToJSON(item)); 77 | } 78 | else if(documentElement.nodeName === 'feed') { 79 | const entrys = findNodes('entry', documentElement.childNodes); 80 | return entrys.map(entry => convertAtomItemToJSON(entry)); 81 | } 82 | else { 83 | return []; 84 | } 85 | } 86 | 87 | const convertAtomItemToJSON = (item) => { 88 | const getElementText = (tagName) => { 89 | const elements = findNodes(tagName, item.childNodes); 90 | if(elements && elements.length > 0) { 91 | return elements[0].textContent; 92 | } 93 | 94 | return ""; 95 | } 96 | 97 | const getElementAttribute = (tagName, attribute) => { 98 | const elements = findNodes(tagName, item.childNodes); 99 | if(elements && elements.length > 0) { 100 | const href = elements[0].attributes.getNamedItem('href'); 101 | return (href !== undefined) ? href.value : ""; 102 | } 103 | 104 | return ""; 105 | } 106 | 107 | const title = getElementText("title"); 108 | const description = getElementText("summary"); 109 | const guid = getElementText("id"); 110 | const pubDate = getElementText("updated"); 111 | const author = getElementText("author"); 112 | const link = getElementAttribute("link", "href"); 113 | 114 | return {"title": sanitize(title), "guid": guid, "description": description, "pubDate": pubDate, "author": author, "link": link}; 115 | }; 116 | 117 | const convertRSSItemToJSON = (item) => { 118 | const getElementText = (tagName) => { 119 | const elements = findNodes(tagName, item.childNodes); 120 | if(elements && elements.length > 0) { 121 | return elements[0].textContent; 122 | } 123 | 124 | return ""; 125 | } 126 | 127 | const title = getElementText("title"); 128 | const description = getElementText("description"); 129 | const guid = getElementText("guid"); 130 | const pubDate = getElementText("pubDate"); 131 | const author = getElementText("author"); 132 | const link = getElementText("link"); 133 | 134 | return {"title": title, "guid": guid, "description": description, "pubDate": pubDate, "author": author, "link": link}; 135 | }; 136 | 137 | let DOMParser = require('xmldom-alpha').DOMParser; 138 | 139 | if (typeof module !== 'undefined' && module.exports) { 140 | var platform = require('../../scripts/platform/node.js'); 141 | var common = require('../../scripts/platform/common.js'); 142 | var loadTemplate = platform.loadTemplate; 143 | var loadData = platform.loadData; 144 | var getCompiledTemplate = common.getCompiledTemplate; 145 | var ConcatStream = common.ConcatStream; 146 | const fetch = require('node-fetch'); 147 | var Request = fetch.Request; 148 | var Response = fetch.Response; 149 | 150 | // Really need a Cache API on the server..... 151 | caches = new (function() { 152 | this.open = () => { 153 | return Promise.resolve(undefined); 154 | }; 155 | }); 156 | 157 | module.exports = { 158 | handler: root 159 | } 160 | } 161 | else { 162 | routes['root'] = root; 163 | } -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | /* VERSION: 0.0.2 */ 2 | const cacheBust = '?' + Date.now(); // dirty hack for the install phase... saves me versioning at buildtime... if SW dies then this doesn't work as well...don't judge me 3 | importScripts(`/scripts/router.js`); 4 | importScripts(`/scripts/dot.js`); 5 | importScripts(`/scripts/DOMParser.js${cacheBust}`); 6 | importScripts(`/scripts/platform/web.js${cacheBust}`); 7 | importScripts(`/scripts/platform/common.js${cacheBust}`); 8 | importScripts(`/scripts/routes/index.js${cacheBust}`); 9 | importScripts(`/scripts/routes/root.js${cacheBust}`); 10 | importScripts(`/scripts/routes/proxy.js${cacheBust}`); 11 | 12 | const assetPath = '/assets/'; 13 | const dataPath = '/data/' 14 | 15 | var ASSET_CACHE_NAME = 'assest-cache'; 16 | var assetsToCache = [ 17 | `${assetPath}templates/head.html`, 18 | `${assetPath}templates/foot.html`, 19 | `${assetPath}templates/body.html`, 20 | ]; 21 | 22 | self.addEventListener('install', (e) => { 23 | // Perform install steps 24 | e.waitUntil( 25 | caches.open(ASSET_CACHE_NAME) 26 | .then((cache) => { 27 | console.log(`${ASSET_CACHE_NAME}: Opened cache`); 28 | return cache.addAll(assetsToCache); 29 | }) 30 | .then(() => self.skipWaiting()) 31 | ); 32 | }); 33 | 34 | self.addEventListener('activate', function(event) { 35 | event.waitUntil(self.clients.claim()); 36 | }); 37 | 38 | getCompiledTemplate(`${assetPath}templates/body.html`); 39 | 40 | /* 41 | Router logic. 42 | */ 43 | 44 | // The proxy server '/proxy' 45 | router.get(`${self.location.origin}/proxy`, (e) => { 46 | e.respondWith(routes['proxy'](dataPath, assetPath, e.request)); 47 | }, {urlMatchProperty: 'href'}); 48 | 49 | // The root '/' 50 | router.get(`${self.location.origin}/$`, (e) => { 51 | e.respondWith(routes['root'](dataPath, assetPath)); 52 | }, {urlMatchProperty: 'href'}); 53 | 54 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const common = require('./public/scripts/platform/common.js'); 3 | const node = require('./public/scripts/platform/node.js'); 4 | const routes = require('./public/scripts/routes/index.js') 5 | 6 | const app = express(); 7 | const getCompiledTemplate = common.getCompiledTemplate; 8 | 9 | const assetPath = 'public/assets/'; 10 | const dataPath = 'public/data/'; 11 | 12 | app.all('*', (req, res, next) => { 13 | // protocol check, if http, redirect to https 14 | if(req.get('X-Forwarded-Proto').indexOf("https") == 0) { 15 | return next(); 16 | } else { 17 | res.redirect('https://' + req.hostname + req.url); 18 | } 19 | }); 20 | 21 | getCompiledTemplate(`${assetPath}templates/head.html`); 22 | getCompiledTemplate(`${assetPath}templates/body.html`); 23 | 24 | app.get('/', (req, res, next) => { 25 | routes['root'](dataPath, assetPath) 26 | .then(response => { 27 | node.responseToExpressStream(res, response.body) 28 | }); 29 | }); 30 | 31 | app.get('/proxy', (req, res, next) => { 32 | routes['proxy'](dataPath, assetPath, req) 33 | .then(response => response.body.pipe(res, {end: true})); 34 | }); 35 | 36 | /* 37 | Start the app 38 | */ 39 | app.use(express.static('public')); 40 | app.listen(8080); --------------------------------------------------------------------------------