Skip to content

Commit e2ead55

Browse files
authored
fix multiple from() tested behaviours (#37)
* wrap asyncIterator.return in a promise * implement async -> sync iterable fallback behaviour * move done further up so abort handler can avoid calling return if iterator is already done * match behaviour of AsyncFromIteratorPrototype better * use a boolean instead of enum for getIterator
1 parent c4b989b commit e2ead55

1 file changed

Lines changed: 121 additions & 18 deletions

File tree

observable.js

Lines changed: 121 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,113 @@ const [Observable, Subscriber] = (() => {
2020
});
2121
}
2222

23+
const pTry = "try" in Promise ? Promise.try.bind(Promise) : (fn, ...args) => new Promise((r) => r(fn(...args)));
24+
25+
const pWithResolvers = 'withResolvers' in Promise ? Promise.withResolvers.bind(Promise) : () => {
26+
let resolve, reject;
27+
const promise = new Promise((res, rej) => ((resolve = res), (reject = rej)));
28+
return { promise, resolve, reject };
29+
}
30+
31+
function getIteratorFromMethod(obj, method) {
32+
// 1. Let iterator be ? Call(method, obj).
33+
const iterator = method.call(obj);
34+
// 2. If iterator is not an Object, throw a TypeError exception.
35+
if (iterator === null || typeof iterator !== "object") throw new TypeError("Iterator is not an object");
36+
// 3. Return ? GetIteratorDirect(iterator)
37+
return iterator;
38+
}
39+
40+
const privateState = new WeakMap();
41+
42+
const AsyncFromSyncIteratorPrototype = {
43+
next(...args) {
44+
// 1. Let O be the this value.
45+
const O = this;
46+
// 2. Assert: O is an Object that has a [[SyncIteratorRecord]] internal slot.
47+
const state = privateState.get(O);
48+
if (!state?.syncIteratorRecord)
49+
throw new TypeError(
50+
"AsyncFromSyncIteratorPrototype.next called on invalid object"
51+
);
52+
// 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]].
53+
const { syncIteratorRecord } = state;
54+
return pTry(() => syncIteratorRecord.next(...args));
55+
},
56+
return(...args) {
57+
// 1. Let O be the this value.
58+
const O = this;
59+
// 2. Assert: O is an Object that has a [[SyncIteratorRecord]] internal slot.
60+
const state = privateState.get(O);
61+
if (!state?.syncIteratorRecord)
62+
throw new TypeError(
63+
"AsyncFromSyncIteratorPrototype.return called on invalid object"
64+
);
65+
// 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]].
66+
const { syncIteratorRecord } = state;
67+
return pTry(() => {
68+
if (!syncIteratorRecord.return) return { value: undefined, done: true };
69+
return syncIteratorRecord.return(...args);
70+
});
71+
},
72+
throw(...args) {
73+
// 1. Let O be the this value.
74+
const O = this;
75+
// 2. Assert: O is an Object that has a [[SyncIteratorRecord]] internal slot.
76+
const state = privateState.get(O);
77+
if (!state?.syncIteratorRecord)
78+
throw new TypeError(
79+
"AsyncFromSyncIteratorPrototype.throw called on invalid object"
80+
);
81+
// 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]].
82+
const { syncIteratorRecord } = state;
83+
return pTry(() => {
84+
if (!syncIteratorRecord.throw) {
85+
// a. NOTE: If syncIterator does not have a throw method, close it to give it a chance to clean up before we reject the capability.
86+
syncIteratorRecord.return();
87+
throw new TypeError("no throw method");
88+
}
89+
return syncIteratorRecord.throw(...args);
90+
});
91+
},
92+
};
93+
94+
function createAsyncFromSyncIterator(syncIteratorRecord) {
95+
// 1. Let asyncIterator be OrdinaryObjectCreate(%AsyncFromSyncIteratorPrototype%, « [[SyncIteratorRecord]] »).
96+
const asyncIterator = Object.create(AsyncFromSyncIteratorPrototype);
97+
// 2. Set asyncIterator.[[SyncIteratorRecord]] to syncIteratorRecord.
98+
privateState.set(asyncIterator, { syncIteratorRecord });
99+
return asyncIterator;
100+
}
101+
102+
function getIterator(obj, isAsync) {
103+
let method = undefined;
104+
// 1. if kind is ASYNC, then
105+
if (isAsync) {
106+
// 1.a. Let method be ? GetMethod(obj, %Symbol.asyncIterator%).
107+
method = obj[Symbol.asyncIterator];
108+
// 1.b. If method is undefined, then
109+
if (method == undefined) {
110+
// 1.b.i. Let method be ? GetMethod(obj, %Symbol.iterator%).
111+
method = obj[Symbol.iterator];
112+
// 1.b.ii. If method is undefined, throw a TypeError exception.
113+
if (method == undefined) throw new TypeError("Object is not async iterable");
114+
// 1.b.iii. Let syncIteratorRecord be ? GetIteratorFromMethod(obj, syncMethod).
115+
const syncIteratorRecord = getIteratorFromMethod(obj, method);
116+
// 1.b.iv. Return ! CreateAsyncFromSyncIterator(syncIteratorRecord).
117+
return createAsyncFromSyncIterator(syncIteratorRecord);
118+
}
119+
// 2. Else,
120+
} else {
121+
// 2.a. Let method be ? GetMethod(obj, %Symbol.iterator%).
122+
method = obj[Symbol.iterator];
123+
}
124+
// 3. If method is undefined, throw a TypeError exception.
125+
if (method == undefined) throw new TypeError("Object is not iterable");
126+
// 4. Return ? GetIteratorFromMethod(obj, method).
127+
return getIteratorFromMethod(obj, method);
128+
}
129+
23130
const abortSignalAny = "any" in AbortSignal ? AbortSignal.any.bind(AbortSignal) : (signals) => {
24131
// create a signal that will abort when any of the signals aborts.
25132
const ac = new AbortController();
@@ -43,14 +150,6 @@ const [Observable, Subscriber] = (() => {
43150
// wrapper for AbortSignal.any that removes null and undefined, for convenience.
44151
const anySignal = (signalArray) => abortSignalAny(signalArray.filter(Boolean));
45152

46-
const pWithResolvers = 'withResolvers' in Promise ? Promise.withResolvers.bind(Promise) : () => {
47-
let resolve, reject;
48-
const promise = new Promise((res, rej) => ((resolve = res), (reject = rej)));
49-
return { promise, resolve, reject };
50-
}
51-
52-
const privateState = new WeakMap();
53-
54153
class InternalObserver {
55154
constructor({ next, error, complete } = {}) {
56155
privateState.set(this, { next, error, complete });
@@ -363,6 +462,7 @@ const [Observable, Subscriber] = (() => {
363462
const asyncIteratorMethodRecord = Symbol.asyncIterator in value && value[Symbol.asyncIterator];
364463
// 4. If asyncIteratorMethod’s is undefined or null, then jump to the step labeled From iterable.
365464
if (typeof asyncIteratorMethodRecord === "function") {
465+
let done = false;
366466
// 5. Let nextAlgorithm be the following steps, given a Subscriber subscriber and an Iterator Record iteratorRecord:
367467
function nextAlgorithm(subscriber, iteratorRecord) {
368468
// 5.1. If subscriber’s subscription controller’s signal is aborted, then return.
@@ -389,10 +489,9 @@ const [Observable, Subscriber] = (() => {
389489
subscriber.error(new TypeError("Not an IteratorResult."));
390490
return;
391491
}
392-
let done;
393492
try {
394493
// 5.6.2 Let done be IteratorComplete(iteratorResult).
395-
done = iteratorResult.done;
494+
({ done } = iteratorResult);
396495
} catch (error) {
397496
// 5.6.3 If done is a throw completion, then run subscriber’s error() method with done’s [[Value]] and abort these steps.
398497
subscriber.error(error);
@@ -430,7 +529,7 @@ const [Observable, Subscriber] = (() => {
430529
let iteratorRecordCompletion;
431530
try {
432531
// 6.2. Let iteratorRecordCompletion be GetIterator(value, async).
433-
iteratorRecordCompletion = value[Symbol.asyncIterator]();
532+
iteratorRecordCompletion = getIterator(value, true);
434533
} catch (error) {
435534
// 6.3. If iteratorRecordCompletion is a throw completion, then run subscriber’s error() method with iteratorRecordCompletion’s [[Value]] and abort these steps.
436535
subscriber.error(error);
@@ -444,11 +543,13 @@ const [Observable, Subscriber] = (() => {
444543
// 6.7. Add the following abort algorithm to subscriber’s subscription controller’s signal:
445544
subscriber.signal.addEventListener("abort", () => {
446545
// 6.7.1. Run AsyncIteratorClose(iteratorRecord, NormalCompletion(subscriber’s subscription controller’s abort reason)).
447-
if (typeof iteratorRecord.return !== "function") return;
448-
const returnResult = iteratorRecord.return(subscriber.signal.reason);
449-
if (returnResult === null || typeof returnResult !== "object") {
546+
if (typeof iteratorRecord.return !== "function" || done) return;
547+
const returnPromise = pTry(() => iteratorRecord.return(subscriber.signal.reason));
548+
returnPromise.then((result) => {
549+
if (result === null || typeof result !== "object") {
450550
throw new TypeError("Iterator .return() must return an Object");
451-
}
551+
}
552+
});
452553
});
453554
// 6.8. Run nextAlgorithm given subscriber and iteratorRecord.
454555
nextAlgorithm(subscriber, iteratorRecord);
@@ -466,20 +567,21 @@ const [Observable, Subscriber] = (() => {
466567
let iteratorRecordCompletion;
467568
try {
468569
// 8.2. Let iteratorRecordCompletion be GetIterator(value, sync).
469-
iteratorRecordCompletion = value[Symbol.iterator]();
570+
iteratorRecordCompletion = getIterator(value, false);
470571
} catch (error) {
471572
// 8.3. If iteratorRecordCompletion is a throw completion, then run subscriber’s error() method, given iteratorRecordCompletion’s [[Value]], and abort these steps.
472573
subscriber.error(error);
473574
return;
474575
}
576+
let done = false;
475577
// 8.4. Let iteratorRecord be ! iteratorRecordCompletion.
476578
let iteratorRecord = iteratorRecordCompletion;
477579
// 8.5 If subscriber’s subscription controller’s signal is aborted, then return.
478580
if (subscriber.signal.aborted) return;
479581
// 8.6. Add the following abort algorithm to subscriber’s subscription controller’s signal:
480582
subscriber.signal.addEventListener("abort", () => {
481583
// 8.6.1. Run IteratorClose(iteratorRecord, NormalCompletion(UNUSED)).
482-
if (typeof iteratorRecord.return !== "function") return;
584+
if (typeof iteratorRecord.return !== "function" || done) return;
483585
const returnResult = iteratorRecord.return();
484586
if (returnResult === null || typeof returnResult !== "object") {
485587
throw new TypeError("Iterator .return() must return an Object");
@@ -490,9 +592,10 @@ const [Observable, Subscriber] = (() => {
490592
try {
491593
// 8.7.1. Let next be IteratorStepValue(iteratorRecord).
492594
let next = iteratorRecord.next();
595+
({ done } = next);
493596
// 8.7.3. Set next to ! to next.
494597
// 8.7.4. If next is done, then:
495-
if (next.done) {
598+
if (done) {
496599
// 8.7.4.1. Assert: iteratorRecord’s [[Done]] is true.
497600
// 8.7.4.2. Run subscriber’s complete().
498601
subscriber.complete();

0 commit comments

Comments
 (0)