Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement streams by adopting the reference implementation #3200

Draft
wants to merge 64 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3bfba25
Merge commit '0774e365e859ccffe8431441564ddd7bf202c0cf' as 'lib/jsdom…
ninevra May 27, 2021
0774e36
Squashed 'lib/jsdom/living/streams/' content from commit 033c6d90
ninevra May 27, 2021
f58e402
Remove unneeded parts of streams repo
ninevra May 27, 2021
7b5de92
Begin adapting to the outside-of-jsdom environment
ninevra May 27, 2021
5e03c08
Register streams
ninevra May 27, 2021
393edff
Rewrite imports
ninevra May 27, 2021
d067e8d
Use jsdom's AbortSignal
ninevra May 27, 2021
1f92525
Enable streams web-platform-tests
ninevra May 27, 2021
97e1dee
Globally disable jsshell tests
ninevra May 21, 2021
95f8a3d
Pass through globalObject to abstract ops
ninevra May 27, 2021
066a88c
Mark some tests failing
ninevra May 27, 2021
8969474
Use wrappers for AbortSignal, DOMException
ninevra May 27, 2021
a107800
Always initialize _globalObject if necessary
ninevra May 27, 2021
e544546
Save globalObject in InitializeReadableStream
ninevra May 27, 2021
942da52
Save globalObject in InitializeWritableStream
ninevra May 27, 2021
1093ce0
Save globalObject in SetUpReadableByteStreamController
ninevra May 27, 2021
6f5f24b
Use Uint8Arrays and ArrayBuffers from the globalObject
ninevra May 27, 2021
554b1ac
Mark transferable tests as fail-slow for now
ninevra May 27, 2021
eafe528
Mark outdated tests failing for now
ninevra May 28, 2021
476e47a
Mark remaining failing tests
ninevra May 28, 2021
0f06828
Fix lint issues
ninevra May 28, 2021
bec5fd1
Don't fake transfering buffers
ninevra Jun 2, 2021
f3ac927
Flatten streams directory hierarchy
ninevra Jun 2, 2021
4073fbd
Revert "Rewrite imports"
ninevra Jun 2, 2021
544c638
Reregister streams
ninevra Jun 2, 2021
c4a2865
Squashed 'lib/jsdom/living/streams/' changes from 033c6d90..8a7d92b5
ninevra Jun 2, 2021
5c22cd2
Merge commit 'c4a2865a2032c2ee4dc1767b8c769c7482bf531f' into streams-…
ninevra Jun 2, 2021
b1f75bd
Update web-platform-tests
ninevra Jun 2, 2021
3607e6e
(Partially) update to-run for new web-platform-tests
ninevra Jun 2, 2021
35588a0
Update to-run for streams web-platform-tests
ninevra Jun 2, 2021
16fa7d4
Cite the vm bug causing patched-global failures
ninevra Jun 3, 2021
3a64fc0
Remove unused code
ninevra Jun 3, 2021
f9f8c73
Remove reference implementation's licensing
ninevra Jun 7, 2021
e97f8c3
Revert "Update web-platform-tests"
ninevra Jun 7, 2021
7d20c24
Revert "(Partially) update to-run for new web-platform-tests"
ninevra Jun 7, 2021
60569b1
Revert "Update to-run for streams web-platform-tests"
ninevra Jun 7, 2021
26ad931
Mark an incidentally fixed xhr test!
ninevra Jun 7, 2021
2b3849c
Begin fixing style
ninevra Jun 7, 2021
15fcd00
Change capitalization of abstract operations
ninevra Jun 7, 2021
cd7f18f
Capitalize ViewConstructor
ninevra Jun 7, 2021
8e3a327
Fix function style
ninevra Jun 7, 2021
fba4fb5
Fix line length
ninevra Jun 7, 2021
ac6b8b9
Merge branch 'master' into streams-reference-subtree-move
ninevra Jun 16, 2021
99864ae
Revert "Revert "Update to-run for streams web-platform-tests""
ninevra Jun 16, 2021
4243cb4
Mark a test dependent on free function postMessage
ninevra Jun 16, 2021
6901cf3
Disable readable byte streams
ninevra Jun 16, 2021
6afefd0
Remove manual AbortSignal typechecking
ninevra Jun 16, 2021
f2f4c67
Use jsdom's mixin util
ninevra Jun 17, 2021
8625950
Remove uponFulfillment
ninevra Jun 17, 2021
68b3b87
Remove uponRejection
ninevra Jun 17, 2021
8dfadd7
Remove transformPromiseWith
ninevra Jun 17, 2021
f47c653
Remove uponPromise
ninevra Jun 17, 2021
7588448
Remove performPromiseThen
ninevra Jun 17, 2021
c73a20e
Remove waitForAllPromise
ninevra Jun 17, 2021
beaac8f
Remove the promise sidetable
ninevra Jun 17, 2021
38f2007
Remove assertions
ninevra Jun 17, 2021
4ce3283
Rename webidl.js to promises.js
ninevra Jun 17, 2021
3e2cb56
Reenable tests blocked by nodejs/node#38918
ninevra Jun 28, 2021
5f574da
Predefine globals in the VM to work around the bug
ninevra Jun 28, 2021
0104cb5
Fix bug in readableStreamTee
ninevra Jun 28, 2021
b84ebcb
Fix lint errors
ninevra Jun 28, 2021
0bfe33f
Predefine global properties as non-enumerable
ninevra Jun 28, 2021
3f05836
Use this instead of globalThis for node 10 support
ninevra Jun 29, 2021
c254c7b
Remove unneed support for resetQueue asserts
ninevra Jul 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/jsdom/browser/Window.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const webIDLConversions = require("webidl-conversions");
const { CSSStyleDeclaration } = require("cssstyle");
const { Performance: RawPerformance } = require("w3c-hr-time");
const notImplemented = require("./not-implemented");
const { installInterfaces } = require("../living/interfaces");
const { installInterfaces, defineVMGlobals } = require("../living/interfaces");
const { define, mixin } = require("../utils");
const Element = require("../living/generated/Element");
const EventTarget = require("../living/generated/EventTarget");
Expand Down Expand Up @@ -105,6 +105,9 @@ function setupWindow(windowInstance, { runScripts }) {
if (runScripts === "outside-only" || runScripts === "dangerously") {
contextifyWindow(windowInstance);

// Workaround for https://github.com/nodejs/node/issues/38918
defineVMGlobals(windowInstance);

// Without this, these globals will only appear to scripts running inside the context using vm.runScript; they will
// not appear to scripts running from the outside, including to JSDOM implementation code.
for (const [globalName, globalPropDesc] of jsGlobalEntriesToInstall) {
Expand Down
31 changes: 30 additions & 1 deletion lib/jsdom/living/interfaces.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable global-require */
"use strict";

const vm = require("vm");

const style = require("../level2/style");
const xpath = require("../level3/xpath");

Expand Down Expand Up @@ -185,7 +187,18 @@ const generatedInterfaces = {

Headers: require("./generated/Headers"),
AbortController: require("./generated/AbortController"),
AbortSignal: require("./generated/AbortSignal")
AbortSignal: require("./generated/AbortSignal"),

ByteLengthQueuingStrategy: require("./generated/ByteLengthQueuingStrategy"),
CountQueuingStrategy: require("./generated/CountQueuingStrategy"),
ReadableStream: require("./generated/ReadableStream"),
ReadableStreamDefaultController: require("./generated/ReadableStreamDefaultController"),
ReadableStreamDefaultReader: require("./generated/ReadableStreamDefaultReader"),
TransformStream: require("./generated/TransformStream"),
TransformStreamDefaultController: require("./generated/TransformStreamDefaultController"),
WritableStream: require("./generated/WritableStream"),
WritableStreamDefaultController: require("./generated/WritableStreamDefaultController"),
WritableStreamDefaultWriter: require("./generated/WritableStreamDefaultWriter")
};

function install(window, name, interfaceConstructor) {
Expand All @@ -196,6 +209,22 @@ function install(window, name, interfaceConstructor) {
});
}

function defineVMGlobal(context, name) {
vm.runInContext(`Object.defineProperty(this, "${name}", {
configurable: true,
writable: true
})`, context);
}

// Predefines interface global variables in the VM, to work around
// https://github.com/nodejs/node/issues/38918
exports.defineVMGlobals = window => {
for (const generatedInterfaceName of Object.keys(generatedInterfaces)) {
defineVMGlobal(window, generatedInterfaceName);
}
defineVMGlobal(window, "HTMLDocument");
};

exports.installInterfaces = (window, globalNames) => {
// Install generated interface.
for (const generatedInterface of Object.values(generatedInterfaces)) {
Expand Down
27 changes: 27 additions & 0 deletions lib/jsdom/living/streams/ByteLengthQueuingStrategy-impl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use strict";

const sizeFunctionWeakMap = new WeakMap();

exports.implementation = class ByteLengthQueuingStrategyImpl {
constructor(globalObject, [{ highWaterMark }]) {
this._globalObject = globalObject;
this.highWaterMark = highWaterMark;
}

get size() {
initializeSizeFunction(this._globalObject);
return sizeFunctionWeakMap.get(this._globalObject);
}
};

function initializeSizeFunction(globalObject) {
if (sizeFunctionWeakMap.has(globalObject)) {
return;
}

// We need to set the 'name' property:
// eslint-disable-next-line prefer-arrow-callback
Comment on lines +22 to +23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW you can do

const size = chunk => chunk.byteLength;
sizeFunctionWeakMap.set(globalObject, size);

The size function's name property will be automatically set.


Also, do we need to create the function in globalObject, to ensure the prototype is correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is straight from the reference impl. Personally I have a mild preference for function size() {} over const size = , I think it makes what's going on a little more clear.

I think you're right about the prototype, but that's a jsdom-wide problem, isn't it? E.g. Object.getPrototypeOf(window.Headers) !== window.Function.prototype. Not sure it's worth hacking around that issue here specifically, unless there's a really simple fix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using function size() { } is important because per spec the result has a .prototype property, whereas arrow functions do not. (I wonder if that's tested.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ninevra

I think you're right about the prototype, but that's a jsdom-wide problem, isn't it?

Ah yes, indeed.


@domenic Printing

new ByteLengthQueuingStrategy({ highWaterMark: 100 }).size.prototype

gives undefined in Chrome and Firefox. The Streams spec calls CreateBuiltinFunction without calling MakeConstructor, which is the abstract op that adds the prototype property. So it would appear that we should in fact use arrow functions after all

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sizeFunctionWeakMap.set(globalObject, function size(chunk) {
return chunk.byteLength;
});
}
7 changes: 7 additions & 0 deletions lib/jsdom/living/streams/ByteLengthQueuingStrategy.webidl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[Exposed=(Window,Worker,Worklet)]
interface ByteLengthQueuingStrategy {
constructor(QueuingStrategyInit init);

readonly attribute unrestricted double highWaterMark;
readonly attribute Function size;
};
27 changes: 27 additions & 0 deletions lib/jsdom/living/streams/CountQueuingStrategy-impl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use strict";

const sizeFunctionWeakMap = new WeakMap();

exports.implementation = class CountQueuingStrategyImpl {
constructor(globalObject, [{ highWaterMark }]) {
this._globalObject = globalObject;
this.highWaterMark = highWaterMark;
}

get size() {
initializeSizeFunction(this._globalObject);
return sizeFunctionWeakMap.get(this._globalObject);
}
};

function initializeSizeFunction(globalObject) {
if (sizeFunctionWeakMap.has(globalObject)) {
return;
}

// We need to set the 'name' property:
// eslint-disable-next-line prefer-arrow-callback
sizeFunctionWeakMap.set(globalObject, function size() {
return 1;
});
}
7 changes: 7 additions & 0 deletions lib/jsdom/living/streams/CountQueuingStrategy.webidl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[Exposed=(Window,Worker,Worklet)]
interface CountQueuingStrategy {
constructor(QueuingStrategyInit init);

readonly attribute unrestricted double highWaterMark;
readonly attribute Function size;
};
6 changes: 6 additions & 0 deletions lib/jsdom/living/streams/QueuingStrategy.webidl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dictionary QueuingStrategy {
unrestricted double highWaterMark;
QueuingStrategySize size;
};

callback QueuingStrategySize = unrestricted double (optional any chunk);
3 changes: 3 additions & 0 deletions lib/jsdom/living/streams/QueuingStrategyInit.webidl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dictionary QueuingStrategyInit {
required unrestricted double highWaterMark;
};
123 changes: 123 additions & 0 deletions lib/jsdom/living/streams/ReadableByteStreamController-impl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use strict";
const { CancelSteps, PullSteps } = require("./abstract-ops/internal-methods.js");
const { resetQueue } = require("./abstract-ops/queue-with-sizes.js");
const aos = require("./abstract-ops/readable-streams.js");

const ReadableStreamBYOBRequest = require("../generated/ReadableStreamBYOBRequest.js");

exports.implementation = class ReadableByteStreamControllerImpl {
get byobRequest() {
if (this._byobRequest === null && this._pendingPullIntos.length > 0) {
const firstDescriptor = this._pendingPullIntos[0];
const view = new this._globalObject.Uint8Array(
firstDescriptor.buffer,
firstDescriptor.byteOffset + firstDescriptor.bytesFilled,
firstDescriptor.byteLength - firstDescriptor.bytesFilled
);

const byobRequest = ReadableStreamBYOBRequest.new(this._globalObject);
byobRequest._controller = this;
byobRequest._view = view;
this._byobRequest = byobRequest;
}

return this._byobRequest;
}

get desiredSize() {
return aos.readableByteStreamControllerGetDesiredSize(this);
}

close() {
if (this._closeRequested === true) {
throw new TypeError("The stream has already been closed; do not close it again!");
}

const state = this._stream._state;
if (state !== "readable") {
throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`);
}

aos.readableByteStreamControllerClose(this);
}

enqueue(chunk) {
if (chunk.byteLength === 0) {
throw new TypeError("chunk must have non-zero byteLength");
}
if (chunk.buffer.byteLength === 0) {
throw new TypeError("chunk's buffer must have non-zero byteLength");
}

if (this._closeRequested === true) {
throw new TypeError("stream is closed or draining");
}

const state = this._stream._state;
if (state !== "readable") {
throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`);
}

aos.readableByteStreamControllerEnqueue(this._globalObject, this, chunk);
}

error(e) {
aos.readableByteStreamControllerError(this, e);
}

[CancelSteps](reason) {
aos.readableByteStreamControllerClearPendingPullIntos(this);

resetQueue(this);

const result = this._cancelAlgorithm(reason);
aos.readableByteStreamControllerClearAlgorithms(this);
return result;
}

[PullSteps](readRequest) {
const stream = this._stream;
// Assert: aos.readableStreamHasDefaultReader(stream) === true

if (this._queueTotalSize > 0) {
// Assert: aos.readableStreamGetNumReadRequests(stream) === 0

const entry = this._queue.shift();
this._queueTotalSize -= entry.byteLength;

aos.readableByteStreamControllerHandleQueueDrain(this);

const view = new this._globalObject.Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength);

readRequest.chunkSteps(view);
return;
}

const autoAllocateChunkSize = this._autoAllocateChunkSize;
if (autoAllocateChunkSize !== undefined) {
let buffer;
try {
buffer = new this._globalObject.ArrayBuffer(autoAllocateChunkSize);
} catch (bufferE) {
readRequest.errorSteps(bufferE);
return;
}

const pullIntoDescriptor = {
buffer,
bufferByteLength: autoAllocateChunkSize,
byteOffset: 0,
byteLength: autoAllocateChunkSize,
bytesFilled: 0,
elementSize: 1,
ViewConstructor: this._globalObject.Uint8Array,
readerType: "default"
};

this._pendingPullIntos.push(pullIntoDescriptor);
}

aos.readableStreamAddReadRequest(stream, readRequest);
aos.readableByteStreamControllerCallPullIfNeeded(this);
}
};
9 changes: 9 additions & 0 deletions lib/jsdom/living/streams/ReadableByteStreamController.webidl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Exposed=(Window,Worker,Worklet)]
interface ReadableByteStreamController {
readonly attribute ReadableStreamBYOBRequest? byobRequest;
readonly attribute unrestricted double? desiredSize;

void close();
void enqueue(ArrayBufferView chunk);
void error(optional any e);
};