1741 lines
51 KiB
JavaScript
1741 lines
51 KiB
JavaScript
// Because we test that the global error handler is called at various times.
|
|
setup({allow_uncaught_exception: true});
|
|
|
|
test(() => {
|
|
assert_equals(typeof Observable.from, "function",
|
|
"Observable.from() is a function");
|
|
}, "from(): Observable.from() is a function");
|
|
|
|
test(() => {
|
|
assert_throws_js(TypeError, () => Observable.from(10),
|
|
"Number cannot convert to an Observable");
|
|
assert_throws_js(TypeError, () => Observable.from(true),
|
|
"Boolean cannot convert to an Observable");
|
|
assert_throws_js(TypeError, () => Observable.from("String"),
|
|
"String cannot convert to an Observable");
|
|
assert_throws_js(TypeError, () => Observable.from({a: 10}),
|
|
"Object cannot convert to an Observable");
|
|
assert_throws_js(TypeError, () => Observable.from(Symbol.iterator),
|
|
"Bare Symbol.iterator cannot convert to an Observable");
|
|
assert_throws_js(TypeError, () => Observable.from(Promise),
|
|
"Promise constructor cannot convert to an Observable");
|
|
}, "from(): Failed conversions");
|
|
|
|
test(() => {
|
|
const target = new EventTarget();
|
|
const observable = target.when('custom');
|
|
const from_observable = Observable.from(observable);
|
|
assert_equals(observable, from_observable);
|
|
}, "from(): Given an observable, it returns that exact observable");
|
|
|
|
test(() => {
|
|
let completeCalled = false;
|
|
const results = [];
|
|
const array = [1, 2, 3, 'a', new Date(), 15, [12]];
|
|
const observable = Observable.from(array);
|
|
observable.subscribe({
|
|
next: v => results.push(v),
|
|
error: e => assert_unreached('error is not called'),
|
|
complete: () => completeCalled = true
|
|
});
|
|
|
|
assert_array_equals(results, array);
|
|
assert_true(completeCalled);
|
|
}, "from(): Given an array");
|
|
|
|
test(() => {
|
|
const iterable = {
|
|
[Symbol.iterator]() {
|
|
let n = 0;
|
|
return {
|
|
next() {
|
|
n++;
|
|
if (n <= 3) {
|
|
return { value: n, done: false };
|
|
}
|
|
return { value: undefined, done: true };
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const observable = Observable.from(iterable);
|
|
|
|
assert_true(observable instanceof Observable, "Observable.from() returns an Observable");
|
|
|
|
const results = [];
|
|
|
|
observable.subscribe({
|
|
next: (value) => results.push(value),
|
|
error: () => assert_unreached("should not error"),
|
|
complete: () => results.push("complete"),
|
|
});
|
|
|
|
assert_array_equals(results, [1, 2, 3, "complete"],
|
|
"Subscription pushes iterable values out to Observable");
|
|
|
|
// A second subscription should restart iteration.
|
|
observable.subscribe({
|
|
next: (value) => results.push(value),
|
|
error: () => assert_unreached("should not error"),
|
|
complete: () => results.push("complete2"),
|
|
});
|
|
|
|
assert_array_equals(results, [1, 2, 3, "complete", 1, 2, 3, "complete2"],
|
|
"Subscribing again causes another fresh iteration on an un-exhausted iterable");
|
|
}, "from(): Iterable converts to Observable");
|
|
|
|
// This test, and the variants below it, test the web-observable side-effects of
|
|
// converting an iterable object to an Observable. Specifically, it tracks
|
|
// exactly when the %Symbol.iterator% method is *retrieved* from the object,
|
|
// invoked, and what its error-throwing side-effects are.
|
|
//
|
|
// Even more specifically, we assert that the %Symbol.iterator% method is
|
|
// retrieved a single time when converting to an Observable, and then again when
|
|
// subscribing to the converted Observable. This makes it possible for the
|
|
// %Symbol.iterator% method getter to change return values in between conversion
|
|
// and subscription. See https://github.com/WICG/observable/issues/127 for
|
|
// related discussion.
|
|
test(() => {
|
|
const results = [];
|
|
|
|
const iterable = {
|
|
get [Symbol.iterator]() {
|
|
results.push("[Symbol.iterator] method GETTER");
|
|
return function() {
|
|
results.push("[Symbol.iterator implementation]");
|
|
return {
|
|
get next() {
|
|
results.push("next() method GETTER");
|
|
return function() {
|
|
results.push("next() implementation");
|
|
return {value: undefined, done: true};
|
|
};
|
|
},
|
|
};
|
|
};
|
|
},
|
|
};
|
|
|
|
const observable = Observable.from(iterable);
|
|
assert_array_equals(results, ["[Symbol.iterator] method GETTER"]);
|
|
|
|
let thrownError = null;
|
|
observable.subscribe();
|
|
assert_array_equals(results, [
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator implementation]",
|
|
"next() method GETTER",
|
|
"next() implementation"
|
|
]);
|
|
}, "from(): [Symbol.iterator] side-effects (one observable)");
|
|
|
|
// This tests that once `Observable.from()` detects a non-null and non-undefined
|
|
// `[Symbol.iterator]` property, we've committed to converting as an iterable.
|
|
// If the value of that property is then not callable, we don't silently move on
|
|
// to the next conversion type — we throw a TypeError.
|
|
//
|
|
// That's because that's what TC39's `GetMethod()` [1] calls for, which is what
|
|
// `Observable.from()` first uses in the iterable conversion branch [2].
|
|
//
|
|
// [1]: https://tc39.es/ecma262/multipage/abstract-operations.html#sec-getmethod
|
|
// [2]: http://wicg.github.io/observable/#from-iterable-conversion
|
|
test(() => {
|
|
let results = [];
|
|
const iterable = {
|
|
[Symbol.iterator]: 10,
|
|
};
|
|
|
|
let errorThrown = null;
|
|
try {
|
|
Observable.from(iterable);
|
|
} catch(e) {
|
|
errorThrown = e;
|
|
}
|
|
|
|
assert_true(errorThrown instanceof TypeError);
|
|
}, "from(): [Symbol.iterator] not callable");
|
|
|
|
test(() => {
|
|
let results = [];
|
|
const iterable = {
|
|
calledOnce: false,
|
|
get [Symbol.iterator]() {
|
|
if (this.calledOnce) {
|
|
// Return a non-callable primitive the second time `@@iterator` is
|
|
// called.
|
|
return 10;
|
|
}
|
|
|
|
this.calledOnce = true;
|
|
return this.validImplementation;
|
|
},
|
|
validImplementation: () => {
|
|
return {
|
|
next() { return {done: true}; }
|
|
}
|
|
}
|
|
};
|
|
|
|
let errorThrown = null;
|
|
|
|
const observable = Observable.from(iterable);
|
|
observable.subscribe({
|
|
next: v => results.push("should not be called"),
|
|
error: e => {
|
|
errorThrown = e;
|
|
results.push(e);
|
|
},
|
|
});
|
|
|
|
assert_array_equals(results, [errorThrown],
|
|
"An error was plumbed through the Observable");
|
|
assert_true(errorThrown instanceof TypeError);
|
|
}, "from(): [Symbol.iterator] not callable AFTER SUBSCRIBE throws");
|
|
|
|
test(() => {
|
|
let results = [];
|
|
const iterable = {
|
|
calledOnce: false,
|
|
validImplementation: () => {
|
|
return {
|
|
next() { return {done: true}; }
|
|
}
|
|
},
|
|
get [Symbol.iterator]() {
|
|
if (this.calledOnce) {
|
|
// Return null the second time `@@iterator` is called.
|
|
return null;
|
|
}
|
|
|
|
this.calledOnce = true;
|
|
return this.validImplementation;
|
|
}
|
|
};
|
|
|
|
let errorThrown = null;
|
|
|
|
const observable = Observable.from(iterable);
|
|
observable.subscribe({
|
|
next: v => results.push("should not be called"),
|
|
error: e => {
|
|
errorThrown = e;
|
|
results.push(e);
|
|
},
|
|
});
|
|
|
|
assert_array_equals(results, [errorThrown],
|
|
"An error was plumbed through the Observable");
|
|
assert_true(errorThrown instanceof TypeError);
|
|
}, "from(): [Symbol.iterator] returns null AFTER SUBSCRIBE throws");
|
|
|
|
test(() => {
|
|
let results = [];
|
|
const customError = new Error("@@iterator override error");
|
|
|
|
const iterable = {
|
|
numTimesCalled: 0,
|
|
|
|
// The first time this getter is called, it returns a legitimate function
|
|
// that, when called, returns an iterator. Every other time it returns an
|
|
// error-throwing function that does not return an iterator.
|
|
get [Symbol.iterator]() {
|
|
this.numTimesCalled++;
|
|
results.push("[Symbol.iterator] method GETTER");
|
|
|
|
if (this.numTimesCalled === 1) {
|
|
return this.validIteratorImplementation;
|
|
} else {
|
|
return this.errorThrowingIteratorImplementation;
|
|
}
|
|
},
|
|
|
|
validIteratorImplementation: function() {
|
|
results.push("[Symbol.iterator implementation]");
|
|
return {
|
|
get next() {
|
|
results.push("next() method GETTER");
|
|
return function() {
|
|
results.push("next() implementation");
|
|
return {value: undefined, done: true};
|
|
}
|
|
}
|
|
};
|
|
},
|
|
errorThrowingIteratorImplementation: function() {
|
|
results.push("Error-throwing [Symbol.iterator] implementation");
|
|
throw customError;
|
|
},
|
|
};
|
|
|
|
const observable = Observable.from(iterable);
|
|
assert_array_equals(results, [
|
|
"[Symbol.iterator] method GETTER",
|
|
]);
|
|
|
|
// Override iterable's `[Symbol.iterator]` protocol with an error-throwing
|
|
// function. We assert that on subscription, this method (the new `@@iterator`
|
|
// implementation), is called because only the raw JS object gets stored in
|
|
// the Observable that results in conversion. This raw value must get
|
|
// re-converted to an iterable once iteration is about to start.
|
|
|
|
let thrownError = null;
|
|
observable.subscribe({
|
|
error: e => thrownError = e,
|
|
});
|
|
|
|
assert_equals(thrownError, customError,
|
|
"Error thrown from next() is passed to the error() handler");
|
|
assert_array_equals(results, [
|
|
// Old:
|
|
"[Symbol.iterator] method GETTER",
|
|
// New:
|
|
"[Symbol.iterator] method GETTER",
|
|
"Error-throwing [Symbol.iterator] implementation"
|
|
]);
|
|
}, "from(): [Symbol.iterator] is not cached");
|
|
|
|
// Similar to the above test, but with more Observables!
|
|
test(() => {
|
|
const results = [];
|
|
let numTimesSymbolIteratorCalled = 0;
|
|
let numTimesNextCalled = 0;
|
|
|
|
const iterable = {
|
|
get [Symbol.iterator]() {
|
|
results.push("[Symbol.iterator] method GETTER");
|
|
return this.internalIteratorImplementation;
|
|
},
|
|
set [Symbol.iterator](func) {
|
|
this.internalIteratorImplementation = func;
|
|
},
|
|
|
|
internalIteratorImplementation: function() {
|
|
results.push("[Symbol.iterator] implementation");
|
|
return {
|
|
get next() {
|
|
results.push("next() method GETTER");
|
|
return function() {
|
|
results.push("next() implementation");
|
|
return {value: undefined, done: true};
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const obs1 = Observable.from(iterable);
|
|
const obs2 = Observable.from(iterable);
|
|
const obs3 = Observable.from(iterable);
|
|
const obs4 = Observable.from(obs3);
|
|
assert_equals(obs3, obs4);
|
|
|
|
assert_array_equals(results, [
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] method GETTER",
|
|
]);
|
|
|
|
obs1.subscribe();
|
|
assert_array_equals(results, [
|
|
// Old:
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] method GETTER",
|
|
// New:
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] implementation",
|
|
"next() method GETTER",
|
|
"next() implementation",
|
|
]);
|
|
|
|
iterable[Symbol.iterator] = () => {
|
|
results.push("Error-throwing [Symbol.iterator] implementation");
|
|
throw new Error('Symbol.iterator override error');
|
|
};
|
|
|
|
let errorCount = 0;
|
|
|
|
const observer = {error: e => errorCount++};
|
|
obs2.subscribe(observer);
|
|
obs3.subscribe(observer);
|
|
obs4.subscribe(observer);
|
|
assert_equals(errorCount, 3,
|
|
"Error-throwing `@@iterator` implementation is called once per " +
|
|
"subscription");
|
|
|
|
assert_array_equals(results, [
|
|
// Old:
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] method GETTER",
|
|
"[Symbol.iterator] implementation",
|
|
"next() method GETTER",
|
|
"next() implementation",
|
|
// New:
|
|
"[Symbol.iterator] method GETTER",
|
|
"Error-throwing [Symbol.iterator] implementation",
|
|
"[Symbol.iterator] method GETTER",
|
|
"Error-throwing [Symbol.iterator] implementation",
|
|
"[Symbol.iterator] method GETTER",
|
|
"Error-throwing [Symbol.iterator] implementation",
|
|
]);
|
|
}, "from(): [Symbol.iterator] side-effects (many observables)");
|
|
|
|
test(() => {
|
|
const customError = new Error('@@iterator next() error');
|
|
const iterable = {
|
|
[Symbol.iterator]() {
|
|
return {
|
|
next() {
|
|
throw customError;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
let thrownError = null;
|
|
Observable.from(iterable).subscribe({
|
|
error: e => thrownError = e,
|
|
});
|
|
|
|
assert_equals(thrownError, customError,
|
|
"Error thrown from next() is passed to the error() handler");
|
|
}, "from(): [Symbol.iterator] next() throws error");
|
|
|
|
promise_test(async () => {
|
|
const promise = Promise.resolve('value');
|
|
const observable = Observable.from(promise);
|
|
|
|
assert_true(observable instanceof Observable, "Converts to Observable");
|
|
|
|
const results = [];
|
|
|
|
observable.subscribe({
|
|
next: (value) => results.push(value),
|
|
error: () => assert_unreached("error() is not called"),
|
|
complete: () => results.push("complete()"),
|
|
});
|
|
|
|
assert_array_equals(results, [], "Observable does not emit synchronously");
|
|
|
|
await promise;
|
|
|
|
assert_array_equals(results, ["value", "complete()"],
|
|
"Observable emits and completes after Promise resolves");
|
|
}, "from(): Converts Promise to Observable");
|
|
|
|
promise_test(async t => {
|
|
let unhandledRejectionHandlerCalled = false;
|
|
const unhandledRejectionHandler = () => {
|
|
unhandledRejectionHandlerCalled = true;
|
|
};
|
|
|
|
self.addEventListener("unhandledrejection", unhandledRejectionHandler);
|
|
t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler));
|
|
|
|
const promise = Promise.reject("reason");
|
|
const observable = Observable.from(promise);
|
|
|
|
assert_true(observable instanceof Observable, "Converts to Observable");
|
|
|
|
const results = [];
|
|
|
|
observable.subscribe({
|
|
next: (value) => assert_unreached("next() not called"),
|
|
error: (error) => results.push(error),
|
|
complete: () => assert_unreached("complete() not called"),
|
|
});
|
|
|
|
assert_array_equals(results, [], "Observable does not emit synchronously");
|
|
|
|
let catchBlockEntered = false;
|
|
try {
|
|
await promise;
|
|
} catch {
|
|
catchBlockEntered = true;
|
|
}
|
|
|
|
assert_true(catchBlockEntered, "Catch block entered");
|
|
assert_false(unhandledRejectionHandlerCalled, "No unhandledrejection event");
|
|
assert_array_equals(results, ["reason"],
|
|
"Observable emits error() after Promise rejects");
|
|
}, "from(): Converts rejected Promise to Observable. No " +
|
|
"`unhandledrejection` event when error is handled by subscription");
|
|
|
|
promise_test(async t => {
|
|
let unhandledRejectionHandlerCalled = false;
|
|
const unhandledRejectionHandler = () => {
|
|
unhandledRejectionHandlerCalled = true;
|
|
};
|
|
|
|
self.addEventListener("unhandledrejection", unhandledRejectionHandler);
|
|
t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler));
|
|
|
|
let errorReported = null;
|
|
self.addEventListener("error", e => errorReported = e, { once: true });
|
|
|
|
let catchBlockEntered = false;
|
|
try {
|
|
const promise = Promise.reject("custom reason");
|
|
const observable = Observable.from(promise);
|
|
|
|
observable.subscribe();
|
|
await promise;
|
|
} catch {
|
|
catchBlockEntered = true;
|
|
}
|
|
|
|
assert_true(catchBlockEntered, "Catch block entered");
|
|
assert_false(unhandledRejectionHandlerCalled,
|
|
"No unhandledrejection event, because error got reported to global");
|
|
assert_not_equals(errorReported, null, "Error was reported to the global");
|
|
|
|
assert_true(errorReported.message.includes("custom reason"),
|
|
"Error message matches");
|
|
assert_equals(errorReported.lineno, 0, "Error lineno is 0");
|
|
assert_equals(errorReported.colno, 0, "Error lineno is 0");
|
|
assert_equals(errorReported.error, "custom reason",
|
|
"Error object is equivalent");
|
|
}, "from(): Rejections not handled by subscription are reported to the " +
|
|
"global, and still not sent as an unhandledrejection event");
|
|
|
|
test(() => {
|
|
const results = [];
|
|
const observable = new Observable(subscriber => {
|
|
subscriber.next('from Observable');
|
|
subscriber.complete();
|
|
});
|
|
|
|
observable[Symbol.iterator] = () => {
|
|
results.push('Symbol.iterator() called');
|
|
return {
|
|
next() {
|
|
return {value: 'from @@iterator', done: true};
|
|
}
|
|
};
|
|
};
|
|
|
|
Observable.from(observable).subscribe({
|
|
next: v => results.push(v),
|
|
complete: () => results.push("complete"),
|
|
});
|
|
|
|
assert_array_equals(results, ["from Observable", "complete"]);
|
|
}, "from(): Observable that implements @@iterator protocol gets converted " +
|
|
"as an Observable, not iterator");
|
|
|
|
test(() => {
|
|
const results = [];
|
|
const promise = new Promise(resolve => {
|
|
resolve('from Promise');
|
|
});
|
|
|
|
promise[Symbol.iterator] = () => {
|
|
let done = false;
|
|
return {
|
|
next() {
|
|
if (!done) {
|
|
done = true;
|
|
return {value: 'from @@iterator', done: false};
|
|
} else {
|
|
return {value: undefined, done: true};
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
Observable.from(promise).subscribe({
|
|
next: v => results.push(v),
|
|
complete: () => results.push("complete"),
|
|
});
|
|
|
|
assert_array_equals(results, ["from @@iterator", "complete"]);
|
|
}, "from(): Promise that implements @@iterator protocol gets converted as " +
|
|
"an iterable, not Promise");
|
|
|
|
// When the [Symbol.iterator] method on a given object is undefined, we don't
|
|
// try to convert the object to an Observable via the iterable protocol. The
|
|
// Observable specification *also* does the same thing if the [Symbol.iterator]
|
|
// method is *null*. That is, in that case we also skip the conversion via
|
|
// iterable protocol, and continue to try and convert the object as another type
|
|
// (in this case, a Promise).
|
|
promise_test(async () => {
|
|
const promise = new Promise(resolve => resolve('from Promise'));
|
|
assert_equals(promise[Symbol.iterator], undefined);
|
|
promise[Symbol.iterator] = null;
|
|
assert_equals(promise[Symbol.iterator], null);
|
|
|
|
const value = await new Promise(resolve => {
|
|
Observable.from(promise).subscribe(value => resolve(value));
|
|
});
|
|
|
|
assert_equals(value, 'from Promise');
|
|
}, "from(): Promise whose [Symbol.iterator] returns null converts as Promise");
|
|
|
|
// This is a more sensitive test, which asserts that even just trying to reach
|
|
// for the [Symbol.iterator] method on an object whose *getter* for the
|
|
// [Symbol.iterator] method throws an error, results in `Observable#from()`
|
|
// rethrowing that error.
|
|
test(() => {
|
|
const error = new Error('thrown from @@iterator getter');
|
|
const obj = {
|
|
get [Symbol.iterator]() {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
try {
|
|
Observable.from(obj);
|
|
assert_unreached("from() conversion throws");
|
|
} catch(e) {
|
|
assert_equals(e, error);
|
|
}
|
|
}, "from(): Rethrows the error when Converting an object whose @@iterator " +
|
|
"method *getter* throws an error");
|
|
|
|
// This test exercises the line of spec prose that says:
|
|
//
|
|
// "If |asyncIteratorMethodRecord|'s [[Value]] is undefined or null, then jump
|
|
// to the step labeled 'From iterable'."
|
|
test(() => {
|
|
const sync_iterable = {
|
|
[Symbol.asyncIterator]: null,
|
|
[Symbol.iterator]() {
|
|
return {
|
|
value: 0,
|
|
next() {
|
|
if (this.value === 2)
|
|
return {value: undefined, done: true};
|
|
else
|
|
return {value: this.value++, done: false};
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
const results = [];
|
|
const source = Observable.from(sync_iterable).subscribe(v => results.push(v));
|
|
assert_array_equals(results, [0, 1]);
|
|
}, "from(): Async iterable protocol null, converts as iterator");
|
|
|
|
promise_test(async t => {
|
|
const results = [];
|
|
const async_iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
results.push("[Symbol.asyncIterator]() invoked");
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
return new Promise(resolve => {
|
|
t.step_timeout(() => {
|
|
resolve({
|
|
value: this.val,
|
|
done: this.val++ === 4 ? true : false,
|
|
});
|
|
}, 400);
|
|
});
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const source = Observable.from(async_iterable);
|
|
assert_array_equals(results, []);
|
|
|
|
await new Promise(resolve => {
|
|
source.subscribe({
|
|
next: v => {
|
|
results.push(`Observing ${v}`);
|
|
queueMicrotask(() => results.push(`next() microtask interleaving (v=${v})`));
|
|
},
|
|
complete: () => {
|
|
results.push('complete()');
|
|
resolve();
|
|
},
|
|
});
|
|
});
|
|
|
|
assert_array_equals(results, [
|
|
"[Symbol.asyncIterator]() invoked",
|
|
"Observing 0",
|
|
"next() microtask interleaving (v=0)",
|
|
"Observing 1",
|
|
"next() microtask interleaving (v=1)",
|
|
"Observing 2",
|
|
"next() microtask interleaving (v=2)",
|
|
"Observing 3",
|
|
"next() microtask interleaving (v=3)",
|
|
"complete()",
|
|
]);
|
|
}, "from(): Asynchronous iterable conversion");
|
|
|
|
// This test is a more chaotic version of the above. It ensures that a single
|
|
// Observable can handle multiple in-flight subscriptions to the same underlying
|
|
// async iterable without the two subscriptions competing. It asserts that the
|
|
// asynchronous values are pushed to the observers in the correct order.
|
|
promise_test(async t => {
|
|
const async_iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
// Returns a Promise that resolves in a random amount of time less
|
|
// than a second.
|
|
return new Promise(resolve => {
|
|
t.step_timeout(() => resolve({
|
|
value: this.val,
|
|
done: this.val++ === 4 ? true : false,
|
|
}), 200);
|
|
});
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const results = [];
|
|
const source = Observable.from(async_iterable);
|
|
|
|
const promise = new Promise(resolve => {
|
|
source.subscribe({
|
|
next: v => {
|
|
results.push(`${v}-first-sub`);
|
|
|
|
// Half-way through the first subscription, start another subscription.
|
|
if (v === 0) {
|
|
source.subscribe({
|
|
next: v => results.push(`${v}-second-sub`),
|
|
complete: () => {
|
|
results.push('complete-second-sub');
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
complete: () => {
|
|
results.push('complete-first-sub');
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
await promise;
|
|
assert_array_equals(results, [
|
|
'0-first-sub',
|
|
|
|
'1-first-sub',
|
|
'1-second-sub',
|
|
|
|
'2-first-sub',
|
|
'2-second-sub',
|
|
|
|
'3-first-sub',
|
|
'3-second-sub',
|
|
|
|
'complete-first-sub',
|
|
'complete-second-sub',
|
|
]);
|
|
}, "from(): Asynchronous iterable multiple in-flight subscriptions");
|
|
// This test is like the above, ensuring that multiple subscriptions to the same
|
|
// sync-iterable-converted-Observable can exist at a time. Since sync iterables
|
|
// push all of their values to the Observable synchronously, the way to do this
|
|
// is subscribe to the sync iterable Observable *inside* the next handler of the
|
|
// same Observable.
|
|
test(() => {
|
|
const results = [];
|
|
|
|
const array = [1, 2, 3, 4, 5];
|
|
const source = Observable.from(array);
|
|
source.subscribe({
|
|
next: v => {
|
|
results.push(`${v}-first-sub`);
|
|
if (v === 3) {
|
|
// Pushes all 5 values to `results` right after the first instance of `3`.
|
|
source.subscribe({
|
|
next: v => results.push(`${v}-second-sub`),
|
|
complete: () => results.push('complete-second-sub'),
|
|
});
|
|
}
|
|
},
|
|
complete: () => results.push('complete-first-sub'),
|
|
});
|
|
|
|
assert_array_equals(results, [
|
|
// These values are pushed when there is only a single subscription.
|
|
'1-first-sub', '2-first-sub', '3-first-sub',
|
|
// These values are pushed in the correct order, for two subscriptions.
|
|
'4-first-sub', '4-second-sub',
|
|
'5-first-sub', '5-second-sub',
|
|
'complete-first-sub', 'complete-second-sub',
|
|
]);
|
|
}, "from(): Sync iterable multiple in-flight subscriptions");
|
|
|
|
promise_test(async () => {
|
|
const async_generator = async function*() {
|
|
yield 1;
|
|
yield 2;
|
|
yield 3;
|
|
};
|
|
|
|
const results = [];
|
|
const source = Observable.from(async_generator());
|
|
|
|
const subscribeFunction = function(resolve) {
|
|
source.subscribe({
|
|
next: v => results.push(v),
|
|
complete: () => resolve(),
|
|
});
|
|
}
|
|
await new Promise(subscribeFunction);
|
|
assert_array_equals(results, [1, 2, 3]);
|
|
await new Promise(subscribeFunction);
|
|
assert_array_equals(results, [1, 2, 3]);
|
|
}, "from(): Asynchronous generator conversion: can only be used once");
|
|
|
|
// The value returned by an async iterator object's `next()` method is supposed
|
|
// to be a Promise. But this requirement "isn't enforced": see [1]. Therefore,
|
|
// the Observable spec unconditionally wraps the return value in a resolved
|
|
// Promise, as is standard practice [2].
|
|
//
|
|
// This test ensures that even if the object returned from an async iterator's
|
|
// `next()` method is a synchronously-available object with `done: true`
|
|
// (instead of a Promise), the `done` property is STILL not retrieved
|
|
// synchronously. In other words, we test that the Promise-wrapping is
|
|
// implemented.
|
|
//
|
|
// [1]: https://tc39.es/ecma262/#table-async-iterator-r
|
|
// [2]: https://matrixlogs.bakkot.com/WHATWG/2024-08-30#L30
|
|
promise_test(async () => {
|
|
const results = [];
|
|
|
|
const async_iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
return {
|
|
next() {
|
|
return {
|
|
value: undefined,
|
|
get done() {
|
|
results.push('done() GETTER called');
|
|
return true;
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const source = Observable.from(async_iterable);
|
|
assert_array_equals(results, []);
|
|
|
|
queueMicrotask(() => results.push('Microtask queued before subscription'));
|
|
source.subscribe();
|
|
assert_array_equals(results, []);
|
|
|
|
await Promise.resolve();
|
|
assert_array_equals(results, [
|
|
"Microtask queued before subscription",
|
|
"done() GETTER called",
|
|
]);
|
|
}, "from(): Promise-wrapping semantics of IteratorResult interface");
|
|
|
|
// Errors thrown from [Symbol.asyncIterator] are propagated to the observer
|
|
// synchronously. This is because in language constructs (i.e., for-await of
|
|
// loops) that invoke [Symbol.asyncIterator]() that throw errors, the errors are
|
|
// synchronously propagated to script outside of the loop, and are catchable.
|
|
// Observables follow this precedent.
|
|
test(() => {
|
|
const error = new Error("[Symbol.asyncIterator] error");
|
|
const results = [];
|
|
const async_iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
results.push("[Symbol.asyncIterator]() invoked");
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
Observable.from(async_iterable).subscribe({
|
|
error: e => results.push(e),
|
|
});
|
|
|
|
assert_array_equals(results, [
|
|
"[Symbol.asyncIterator]() invoked",
|
|
error,
|
|
]);
|
|
}, "from(): Errors thrown in Symbol.asyncIterator() are propagated synchronously");
|
|
|
|
// AsyncIterable: next() throws exception instead of return Promise. Any errors
|
|
// that occur during the retrieval of `next()` always result in a rejected
|
|
// Promise. Therefore, the error makes it to the Observer with microtask timing.
|
|
promise_test(async () => {
|
|
const nextError = new Error('next error');
|
|
const async_iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
return {
|
|
get next() {
|
|
throw nextError;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
const results = [];
|
|
Observable.from(async_iterable).subscribe({
|
|
error: e => results.push(e),
|
|
});
|
|
|
|
assert_array_equals(results, []);
|
|
// Wait one microtask since the error will be propagated through a rejected
|
|
// Promise managed by the async iterable conversion semantics.
|
|
await Promise.resolve();
|
|
assert_array_equals(results, [nextError]);
|
|
}, "from(): Errors thrown in async iterator's next() GETTER are propagated " +
|
|
"in a microtask");
|
|
promise_test(async () => {
|
|
const nextError = new Error('next error');
|
|
const async_iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
return {
|
|
next() {
|
|
throw nextError;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
const results = [];
|
|
Observable.from(async_iterable).subscribe({
|
|
error: e => results.push(e),
|
|
});
|
|
|
|
assert_array_equals(results, []);
|
|
await Promise.resolve();
|
|
assert_array_equals(results, [nextError]);
|
|
}, "from(): Errors thrown in async iterator's next() are propagated in a microtask");
|
|
|
|
test(() => {
|
|
const results = [];
|
|
const iterable = {
|
|
[Symbol.iterator]() {
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
results.push(`IteratorRecord#next() pushing ${this.val}`);
|
|
return {
|
|
value: this.val,
|
|
done: this.val++ === 10 ? true : false,
|
|
};
|
|
},
|
|
return() {
|
|
results.push(`IteratorRecord#return() called with this.val=${this.val}`);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const ac = new AbortController();
|
|
Observable.from(iterable).subscribe(v => {
|
|
results.push(`Observing ${v}`);
|
|
if (v === 3) {
|
|
ac.abort();
|
|
}
|
|
}, {signal: ac.signal});
|
|
|
|
assert_array_equals(results, [
|
|
"IteratorRecord#next() pushing 0",
|
|
"Observing 0",
|
|
"IteratorRecord#next() pushing 1",
|
|
"Observing 1",
|
|
"IteratorRecord#next() pushing 2",
|
|
"Observing 2",
|
|
"IteratorRecord#next() pushing 3",
|
|
"Observing 3",
|
|
"IteratorRecord#return() called with this.val=4",
|
|
]);
|
|
}, "from(): Aborting sync iterable midway through iteration both stops iteration " +
|
|
"and invokes `IteratorRecord#return()");
|
|
// Like the above test, but for async iterables.
|
|
promise_test(async t => {
|
|
const results = [];
|
|
const iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
results.push(`IteratorRecord#next() pushing ${this.val}`);
|
|
return {
|
|
value: this.val,
|
|
done: this.val++ === 10 ? true : false,
|
|
};
|
|
},
|
|
return(reason) {
|
|
results.push(`IteratorRecord#return() called with reason=${reason}`);
|
|
return {done: true};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const ac = new AbortController();
|
|
await new Promise(resolve => {
|
|
Observable.from(iterable).subscribe(v => {
|
|
results.push(`Observing ${v}`);
|
|
if (v === 3) {
|
|
ac.abort(`Aborting because v=${v}`);
|
|
resolve();
|
|
}
|
|
}, {signal: ac.signal});
|
|
});
|
|
|
|
assert_array_equals(results, [
|
|
"IteratorRecord#next() pushing 0",
|
|
"Observing 0",
|
|
"IteratorRecord#next() pushing 1",
|
|
"Observing 1",
|
|
"IteratorRecord#next() pushing 2",
|
|
"Observing 2",
|
|
"IteratorRecord#next() pushing 3",
|
|
"Observing 3",
|
|
"IteratorRecord#return() called with reason=Aborting because v=3",
|
|
]);
|
|
}, "from(): Aborting async iterable midway through iteration both stops iteration " +
|
|
"and invokes `IteratorRecord#return()");
|
|
|
|
test(() => {
|
|
const iterable = {
|
|
[Symbol.iterator]() {
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
return {value: this.val, done: this.val++ === 10 ? true : false};
|
|
},
|
|
// Not returning an Object results in a TypeError being thrown.
|
|
return(reason) {},
|
|
};
|
|
},
|
|
};
|
|
|
|
let thrownError = null;
|
|
const ac = new AbortController();
|
|
Observable.from(iterable).subscribe(v => {
|
|
if (v === 3) {
|
|
try {
|
|
ac.abort(`Aborting because v=${v}`);
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
}
|
|
}, {signal: ac.signal});
|
|
|
|
assert_not_equals(thrownError, null, "abort() threw an Error");
|
|
assert_true(thrownError instanceof TypeError);
|
|
assert_true(thrownError.message.includes('return()'));
|
|
assert_true(thrownError.message.includes('Object'));
|
|
}, "from(): Sync iterable: `Iterator#return()` must return an Object, or an " +
|
|
"error is thrown");
|
|
// This test is just like the above but for async iterables. It asserts that a
|
|
// Promise is rejected when `return()` does not return an Object.
|
|
promise_test(async t => {
|
|
const iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
return {value: this.val, done: this.val++ === 10 ? true : false};
|
|
},
|
|
// Not returning an Object results in a rejected Promise.
|
|
return(reason) {},
|
|
};
|
|
},
|
|
};
|
|
|
|
const unhandled_rejection_promise = new Promise((resolve, reject) => {
|
|
const unhandled_rejection_handler = e => resolve(e.reason);
|
|
self.addEventListener("unhandledrejection", unhandled_rejection_handler);
|
|
t.add_cleanup(() =>
|
|
self.removeEventListener("unhandledrejection", unhandled_rejection_handler));
|
|
|
|
t.step_timeout(() => reject('Timeout'), 3000);
|
|
});
|
|
|
|
const ac = new AbortController();
|
|
await new Promise(resolve => {
|
|
Observable.from(iterable).subscribe(v => {
|
|
if (v === 3) {
|
|
ac.abort(`Aborting because v=${v}`);
|
|
resolve();
|
|
}
|
|
}, {signal: ac.signal});
|
|
});
|
|
|
|
const reason = await unhandled_rejection_promise;
|
|
assert_true(reason instanceof TypeError);
|
|
assert_true(reason.message.includes('return()'));
|
|
assert_true(reason.message.includes('Object'));
|
|
}, "from(): Async iterable: `Iterator#return()` must return an Object, or a " +
|
|
"Promise rejects asynchronously");
|
|
|
|
// This test exercises the logic of `GetIterator()` async->sync fallback
|
|
// logic. Specifically, we have an object that is an async iterable — that is,
|
|
// it has a callback [Symbol.asyncIterator] implementation. Observable.from()
|
|
// detects this, and commits to converting the object from the async iterable
|
|
// protocol. Then, after conversion but before subscription, the object is
|
|
// modified such that it no longer implements the async iterable protocol.
|
|
//
|
|
// But since it still implements the *iterable* protocol, ECMAScript's
|
|
// `GetIterator()` abstract algorithm [1] is fully exercised, which is spec'd to
|
|
// fall-back to the synchronous iterable protocol if it exists, and create a
|
|
// fully async iterable out of the synchronous iterable.
|
|
//
|
|
// [1]: https://tc39.es/ecma262/#sec-getiterator
|
|
promise_test(async () => {
|
|
const results = [];
|
|
const async_iterable = {
|
|
asyncIteratorGotten: false,
|
|
get [Symbol.asyncIterator]() {
|
|
results.push("[Symbol.asyncIterator] GETTER");
|
|
if (this.asyncIteratorGotten) {
|
|
return null; // Both null and undefined work here.
|
|
}
|
|
|
|
this.asyncIteratorGotten = true;
|
|
// The only requirement for `this` to be converted as an async
|
|
// iterable -> Observable is that the return value be callable (i.e., a function).
|
|
return function() {};
|
|
},
|
|
|
|
[Symbol.iterator]() {
|
|
results.push('[Symbol.iterator]() invoked as fallback');
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
return {
|
|
value: this.val,
|
|
done: this.val++ === 4 ? true : false,
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const source = Observable.from(async_iterable);
|
|
assert_array_equals(results, [
|
|
"[Symbol.asyncIterator] GETTER",
|
|
]);
|
|
|
|
await new Promise((resolve, reject) => {
|
|
source.subscribe({
|
|
next: v => {
|
|
results.push(`Observing ${v}`);
|
|
queueMicrotask(() => results.push(`next() microtask interleaving (v=${v})`));
|
|
},
|
|
error: e => reject(e),
|
|
complete: () => {
|
|
results.push('complete()');
|
|
resolve();
|
|
},
|
|
});
|
|
});
|
|
|
|
assert_array_equals(results, [
|
|
// Old:
|
|
"[Symbol.asyncIterator] GETTER",
|
|
// New:
|
|
"[Symbol.asyncIterator] GETTER",
|
|
"[Symbol.iterator]() invoked as fallback",
|
|
"Observing 0",
|
|
"next() microtask interleaving (v=0)",
|
|
"Observing 1",
|
|
"next() microtask interleaving (v=1)",
|
|
"Observing 2",
|
|
"next() microtask interleaving (v=2)",
|
|
"Observing 3",
|
|
"next() microtask interleaving (v=3)",
|
|
"complete()",
|
|
]);
|
|
}, "from(): Asynchronous iterable conversion, with synchronous iterable fallback");
|
|
|
|
test(() => {
|
|
const results = [];
|
|
let generatorFinalized = false;
|
|
|
|
const generator = function*() {
|
|
try {
|
|
for (let n = 0; n < 10; n++) {
|
|
yield n;
|
|
}
|
|
} finally {
|
|
generatorFinalized = true;
|
|
}
|
|
};
|
|
|
|
const observable = Observable.from(generator());
|
|
const abortController = new AbortController();
|
|
|
|
observable.subscribe(n => {
|
|
results.push(n);
|
|
if (n === 3) {
|
|
abortController.abort();
|
|
}
|
|
}, {signal: abortController.signal});
|
|
|
|
assert_array_equals(results, [0, 1, 2, 3]);
|
|
assert_true(generatorFinalized);
|
|
}, "from(): Generator finally block runs when subscription is aborted");
|
|
|
|
test(() => {
|
|
const results = [];
|
|
let generatorFinalized = false;
|
|
|
|
const generator = function*() {
|
|
try {
|
|
for (let n = 0; n < 10; n++) {
|
|
yield n;
|
|
}
|
|
} catch {
|
|
assert_unreached("generator should not be aborted");
|
|
} finally {
|
|
generatorFinalized = true;
|
|
}
|
|
};
|
|
|
|
const observable = Observable.from(generator());
|
|
|
|
observable.subscribe((n) => {
|
|
results.push(n);
|
|
});
|
|
|
|
assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
assert_true(generatorFinalized);
|
|
}, "from(): Generator finally block run when Observable completes");
|
|
|
|
test(() => {
|
|
const results = [];
|
|
let generatorFinalized = false;
|
|
|
|
const generator = function*() {
|
|
try {
|
|
for (let n = 0; n < 10; n++) {
|
|
yield n;
|
|
}
|
|
throw new Error('from the generator');
|
|
} finally {
|
|
generatorFinalized = true;
|
|
}
|
|
};
|
|
|
|
const observable = Observable.from(generator());
|
|
|
|
observable.subscribe({
|
|
next: n => results.push(n),
|
|
error: e => results.push(e.message)
|
|
});
|
|
|
|
assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "from the generator"]);
|
|
assert_true(generatorFinalized);
|
|
}, "from(): Generator finally block run when Observable errors");
|
|
|
|
promise_test(async t => {
|
|
const results = [];
|
|
let generatorFinalized = false;
|
|
|
|
async function* asyncGenerator() {
|
|
try {
|
|
for (let n = 0; n < 10; n++) {
|
|
yield n;
|
|
}
|
|
} finally {
|
|
generatorFinalized = true;
|
|
}
|
|
}
|
|
|
|
const observable = Observable.from(asyncGenerator());
|
|
const abortController = new AbortController();
|
|
|
|
await new Promise((resolve) => {
|
|
observable.subscribe((n) => {
|
|
results.push(n);
|
|
if (n === 3) {
|
|
abortController.abort();
|
|
resolve();
|
|
}
|
|
}, {signal: abortController.signal});
|
|
});
|
|
|
|
assert_array_equals(results, [0, 1, 2, 3]);
|
|
assert_true(generatorFinalized);
|
|
}, "from(): Async generator finally block run when subscription is aborted");
|
|
|
|
promise_test(async t => {
|
|
const results = [];
|
|
let generatorFinalized = false;
|
|
|
|
async function* asyncGenerator() {
|
|
try {
|
|
for (let n = 0; n < 10; n++) {
|
|
yield n;
|
|
}
|
|
} finally {
|
|
generatorFinalized = true;
|
|
}
|
|
}
|
|
|
|
const observable = Observable.from(asyncGenerator());
|
|
|
|
await new Promise(resolve => {
|
|
observable.subscribe({
|
|
next: n => results.push(n),
|
|
complete: () => resolve(),
|
|
});
|
|
});
|
|
|
|
assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
assert_true(generatorFinalized);
|
|
}, "from(): Async generator finally block runs when Observable completes");
|
|
|
|
promise_test(async t => {
|
|
const results = [];
|
|
let generatorFinalized = false;
|
|
|
|
async function* asyncGenerator() {
|
|
try {
|
|
for (let n = 0; n < 10; n++) {
|
|
if (n === 4) {
|
|
throw new Error('from the async generator');
|
|
}
|
|
yield n;
|
|
}
|
|
} finally {
|
|
generatorFinalized = true;
|
|
}
|
|
}
|
|
|
|
const observable = Observable.from(asyncGenerator());
|
|
|
|
await new Promise((resolve) => {
|
|
observable.subscribe({
|
|
next: (n) => results.push(n),
|
|
error: (e) => {
|
|
results.push(e.message);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
assert_array_equals(results, [0, 1, 2, 3, "from the async generator"]);
|
|
assert_true(generatorFinalized);
|
|
}, "from(): Async generator finally block run when Observable errors");
|
|
|
|
// Test what happens when `return()` throws an error upon abort.
|
|
test(() => {
|
|
const results = [];
|
|
const iterable = {
|
|
[Symbol.iterator]() {
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
results.push('next() called');
|
|
return {value: this.val, done: this.val++ === 10 ? true : false};
|
|
},
|
|
return() {
|
|
results.push('return() about to throw an error');
|
|
throw new Error('return() error');
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
const ac = new AbortController();
|
|
const source = Observable.from(iterable);
|
|
source.subscribe(v => {
|
|
if (v === 3) {
|
|
try {
|
|
ac.abort();
|
|
} catch (e) {
|
|
results.push(`AbortController#abort() threw an error: ${e.message}`);
|
|
}
|
|
}
|
|
}, {signal: ac.signal});
|
|
|
|
assert_array_equals(results, [
|
|
'next() called',
|
|
'next() called',
|
|
'next() called',
|
|
'next() called',
|
|
'return() about to throw an error',
|
|
'AbortController#abort() threw an error: return() error',
|
|
]);
|
|
}, "from(): Sync iterable: error thrown from IteratorRecord#return() can be " +
|
|
"synchronously caught");
|
|
promise_test(async t => {
|
|
const results = [];
|
|
const iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
results.push('next() called');
|
|
return {value: this.val, done: this.val++ === 10 ? true : false};
|
|
},
|
|
return() {
|
|
results.push('return() about to throw an error');
|
|
// For async iterables, errors thrown in `return()` end up in a
|
|
// returned rejected Promise, so no error appears on the stack
|
|
// immediately. See [1].
|
|
//
|
|
// [1]: https://whatpr.org/webidl/1397.html#async-iterator-close.
|
|
throw new Error('return() error');
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
const unhandled_rejection_promise = new Promise((resolve, reject) => {
|
|
const unhandled_rejection_handler = e => resolve(e.reason);
|
|
self.addEventListener("unhandledrejection", unhandled_rejection_handler);
|
|
t.add_cleanup(() =>
|
|
self.removeEventListener("unhandledrejection", unhandled_rejection_handler));
|
|
|
|
t.step_timeout(() => reject('Timeout'), 1500);
|
|
});
|
|
|
|
const ac = new AbortController();
|
|
const source = Observable.from(iterable);
|
|
await new Promise((resolve, reject) => {
|
|
source.subscribe(v => {
|
|
if (v === 3) {
|
|
try {
|
|
ac.abort();
|
|
results.push('No error thrown synchronously');
|
|
resolve('No error thrown synchronously');
|
|
} catch (e) {
|
|
results.push(`AbortController#abort() threw an error: ${e.message}`);
|
|
reject(e);
|
|
}
|
|
}
|
|
}, {signal: ac.signal});
|
|
});
|
|
|
|
assert_array_equals(results, [
|
|
'next() called',
|
|
'next() called',
|
|
'next() called',
|
|
'next() called',
|
|
'return() about to throw an error',
|
|
'No error thrown synchronously',
|
|
]);
|
|
|
|
const reason = await unhandled_rejection_promise;
|
|
assert_true(reason instanceof Error);
|
|
assert_equals(reason.message, "return() error",
|
|
"Custom error text passed through rejected Promise");
|
|
}, "from(): Async iterable: error thrown from IteratorRecord#return() is " +
|
|
"wrapped in rejected Promise");
|
|
|
|
test(() => {
|
|
const results = [];
|
|
const iterable = {
|
|
getter() {
|
|
results.push('GETTER called');
|
|
return () => {
|
|
results.push('Obtaining iterator');
|
|
return {
|
|
next() {
|
|
results.push('next() running');
|
|
return {done: true};
|
|
}
|
|
};
|
|
};
|
|
}
|
|
};
|
|
|
|
Object.defineProperty(iterable, Symbol.iterator, {
|
|
get: iterable.getter
|
|
});
|
|
{
|
|
const source = Observable.from(iterable);
|
|
assert_array_equals(results, ["GETTER called"]);
|
|
source.subscribe({}, {signal: AbortSignal.abort()});
|
|
assert_array_equals(results, ["GETTER called"]);
|
|
}
|
|
iterable[Symbol.iterator] = undefined;
|
|
Object.defineProperty(iterable, Symbol.asyncIterator, {
|
|
get: iterable.getter
|
|
});
|
|
{
|
|
const source = Observable.from(iterable);
|
|
assert_array_equals(results, ["GETTER called", "GETTER called"]);
|
|
source.subscribe({}, {signal: AbortSignal.abort()});
|
|
assert_array_equals(results, ["GETTER called", "GETTER called"]);
|
|
}
|
|
}, "from(): Subscribing to an iterable Observable with an aborted signal " +
|
|
"does not call next()");
|
|
|
|
test(() => {
|
|
let results = [];
|
|
|
|
const iterable = {
|
|
controller: null,
|
|
calledOnce: false,
|
|
getter() {
|
|
results.push('GETTER called');
|
|
if (!this.calledOnce) {
|
|
this.calledOnce = true;
|
|
return () => {
|
|
results.push('NOT CALLED');
|
|
// We don't need to return anything here. The only time this path is
|
|
// hit is during `Observable.from()` which doesn't actually obtain an
|
|
// iterator. It just samples the iterable protocol property to ensure
|
|
// that it's valid.
|
|
};
|
|
}
|
|
|
|
// This path is only called the second time the iterator protocol getter
|
|
// is run.
|
|
this.controller.abort();
|
|
return () => {
|
|
results.push('iterator obtained');
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
results.push('next() called');
|
|
return {done: true};
|
|
},
|
|
return() {
|
|
results.push('return() called');
|
|
}
|
|
};
|
|
};
|
|
}
|
|
};
|
|
|
|
// Test for sync iterators.
|
|
{
|
|
const ac = new AbortController();
|
|
iterable.controller = ac;
|
|
Object.defineProperty(iterable, Symbol.iterator, {
|
|
get: iterable.getter,
|
|
});
|
|
|
|
const source = Observable.from(iterable);
|
|
assert_false(ac.signal.aborted, "[Sync iterator]: signal is not yet aborted after from() conversion");
|
|
assert_array_equals(results, ["GETTER called"]);
|
|
|
|
source.subscribe({
|
|
next: n => results.push(n),
|
|
complete: () => results.push('complete'),
|
|
}, {signal: ac.signal});
|
|
assert_true(ac.signal.aborted, "[Sync iterator]: signal is aborted during subscription");
|
|
assert_array_equals(results, ["GETTER called", "GETTER called", "iterator obtained"]);
|
|
}
|
|
|
|
results = [];
|
|
|
|
// Test for async iterators.
|
|
{
|
|
// Reset `iterable` so it can be reused.
|
|
const ac = new AbortController();
|
|
iterable.controller = ac;
|
|
iterable.calledOnce = false;
|
|
iterable[Symbol.iterator] = undefined;
|
|
Object.defineProperty(iterable, Symbol.asyncIterator, {
|
|
get: iterable.getter
|
|
});
|
|
|
|
const source = Observable.from(iterable);
|
|
assert_false(ac.signal.aborted, "[Async iterator]: signal is not yet aborted after from() conversion");
|
|
assert_array_equals(results, ["GETTER called"]);
|
|
|
|
source.subscribe({
|
|
next: n => results.push(n),
|
|
complete: () => results.push('complete'),
|
|
}, {signal: ac.signal});
|
|
assert_true(ac.signal.aborted, "[Async iterator]: signal is aborted during subscription");
|
|
assert_array_equals(results, ["GETTER called", "GETTER called", "iterator obtained"]);
|
|
}
|
|
}, "from(): When iterable conversion aborts the subscription, next() is " +
|
|
"never called");
|
|
|
|
// This test asserts some very subtle behavior with regard to async iterables
|
|
// and a mid-subscription signal abort. Specifically it detects that a signal
|
|
// abort ensures that the `next()` method is not called again on the iterator
|
|
// again, BUT detects that pending Promise from the *previous* `next()` call
|
|
// still has its IteratorResult object examined. I.e., the implementation
|
|
// inspecting the `done` attribute on the resolved IteratorResult is observable
|
|
// event after abort() takes place.
|
|
promise_test(async () => {
|
|
const results = [];
|
|
let resolveNext = null;
|
|
|
|
const iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
return {
|
|
next() {
|
|
results.push('next() called');
|
|
return new Promise(resolve => {
|
|
resolveNext = resolve;
|
|
});
|
|
},
|
|
return() {
|
|
results.push('return() called');
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
const ac = new AbortController();
|
|
const source = Observable.from(iterable);
|
|
source.subscribe({
|
|
next: v => results.push(v),
|
|
complete: () => results.push('complete'),
|
|
}, {signal: ac.signal});
|
|
|
|
assert_array_equals(results, [
|
|
"next() called",
|
|
]);
|
|
|
|
// First abort, ensuring `return()` is called.
|
|
ac.abort();
|
|
|
|
assert_array_equals(results, [
|
|
"next() called",
|
|
"return() called",
|
|
]);
|
|
|
|
// Then resolve the pending `next()` Promise to an object whose `done` getter
|
|
// reports to the test whether it was accessed. We have to wait one microtask
|
|
// for the internal Observable implementation to finish "reacting" to said
|
|
// `next()` promise resolution, for it to grab the `done` attribute.
|
|
await new Promise(resolveOuter => {
|
|
resolveNext({
|
|
get done() {
|
|
results.push('IteratorResult.done GETTER');
|
|
resolveOuter();
|
|
return true;
|
|
}
|
|
});
|
|
});
|
|
|
|
assert_array_equals(results, [
|
|
"next() called",
|
|
"return() called",
|
|
"IteratorResult.done GETTER",
|
|
// Note that "next() called" does not make another appearance.
|
|
]);
|
|
}, "from(): Aborting an async iterable subscription stops subsequent next() " +
|
|
"calls, but old next() Promise reactions are web-observable");
|
|
|
|
test(() => {
|
|
const results = [];
|
|
const iterable = {
|
|
[Symbol.iterator]() {
|
|
return {
|
|
val: 0,
|
|
next() {
|
|
return {value: this.val, done: this.val++ === 4 ? true : false};
|
|
},
|
|
return() {
|
|
results.push('return() called');
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
const source = Observable.from(iterable);
|
|
const ac = new AbortController();
|
|
source.subscribe({
|
|
next: v => results.push(v),
|
|
complete: () => results.push('complete'),
|
|
}, {signal: ac.signal});
|
|
|
|
ac.abort(); // Must do nothing!
|
|
assert_array_equals(results, [0, 1, 2, 3, 'complete']);
|
|
}, "from(): Abort after complete does NOT call IteratorRecord#return()");
|
|
|
|
test(() => {
|
|
const controller = new AbortController();
|
|
// Invalid @@asyncIterator protocol that also aborts the subscription. By the
|
|
// time the invalid-ness of the protocol is detected, the controller has been
|
|
// aborted, meaning that invalid-ness cannot manifest itself in the form of an
|
|
// error that goes to the Observable's subscriber. Instead, it gets reported
|
|
// to the global.
|
|
const asyncIterable = {
|
|
calledOnce: false,
|
|
get[Symbol.asyncIterator]() {
|
|
// This `calledOnce` path is to ensure the Observable first converts
|
|
// correctly via `Observable.from()`, but *later* fails in the path where
|
|
// `@@asyncIterator` is null.
|
|
if (this.calledOnce) {
|
|
controller.abort();
|
|
return null;
|
|
} else {
|
|
this.calledOnce = true;
|
|
return this.validImplementation;
|
|
}
|
|
},
|
|
validImplementation() {
|
|
controller.abort();
|
|
return null;
|
|
}
|
|
};
|
|
|
|
let reportedError = null;
|
|
self.addEventListener("error", e => reportedError = e.error, {once: true});
|
|
|
|
let errorThrown = null;
|
|
const observable = Observable.from(asyncIterable);
|
|
observable.subscribe({
|
|
error: e => errorThrown = e,
|
|
}, {signal: controller.signal});
|
|
|
|
assert_equals(errorThrown, null, "Protocol error is not surfaced to the Subscriber");
|
|
|
|
assert_not_equals(reportedError, null, "Protocol error is reported to the global");
|
|
assert_true(reportedError instanceof TypeError);
|
|
}, "Invalid async iterator protocol error is surfaced before Subscriber#signal is consulted");
|
|
|
|
test(() => {
|
|
const controller = new AbortController();
|
|
const iterable = {
|
|
calledOnce: false,
|
|
get[Symbol.iterator]() {
|
|
if (this.calledOnce) {
|
|
controller.abort();
|
|
return null;
|
|
} else {
|
|
this.calledOnce = true;
|
|
return this.validImplementation;
|
|
}
|
|
},
|
|
validImplementation() {
|
|
controller.abort();
|
|
return null;
|
|
}
|
|
};
|
|
|
|
let reportedError = null;
|
|
self.addEventListener("error", e => reportedError = e.error, {once: true});
|
|
|
|
let errorThrown = null;
|
|
const observable = Observable.from(iterable);
|
|
observable.subscribe({
|
|
error: e => errorThrown = e,
|
|
}, {signal: controller.signal});
|
|
|
|
assert_equals(errorThrown, null, "Protocol error is not surfaced to the Subscriber");
|
|
|
|
assert_not_equals(reportedError, null, "Protocol error is reported to the global");
|
|
assert_true(reportedError instanceof TypeError);
|
|
}, "Invalid iterator protocol error is surfaced before Subscriber#signal is consulted");
|
|
|
|
// Regression test for https://github.com/WICG/observable/issues/208.
|
|
promise_test(async () => {
|
|
let errorReported = false;
|
|
self.onerror = e => errorReported = true;
|
|
|
|
// `first()` aborts the subscription after the first item is encountered.
|
|
const value = await Observable.from([1, 2, 3]).first();
|
|
assert_false(errorReported);
|
|
}, "No error is reported when aborting a subscription to a sync iterator " +
|
|
"that has no `return()` implementation");
|