Mirror: 🎩 A tiny but capable push & pull stream library for TypeScript and Flow

feat: Implement toAsyncIterable (#133)

Co-authored-by: Gustav Tiger <gustav.tiger@axis.com>

authored by kitten.sh Gustav Tiger and committed by GitHub e82aa6aa 5517a763

Changed files
+211
.changeset
src
+5
.changeset/beige-worms-help.md
··· 1 + --- 2 + 'wonka': minor 3 + --- 4 + 5 + Implement `toAsyncIterable`, converting a Wonka source to a JS Async Iterable.
+160
src/__tests__/sinks.test.ts
··· 229 229 }); 230 230 }); 231 231 232 + describe('toAsyncIterable', () => { 233 + it('creates an async iterable mirroring the Wonka source', async () => { 234 + let pulls = 0; 235 + let sink: Sink<any> | null = null; 236 + 237 + const source: Source<any> = _sink => { 238 + sink = _sink; 239 + sink( 240 + start(signal => { 241 + if (signal === TalkbackKind.Pull) pulls++; 242 + }) 243 + ); 244 + }; 245 + 246 + const asyncIterator = sinks.toAsyncIterable(source)[Symbol.asyncIterator](); 247 + 248 + expect(pulls).toBe(1); 249 + sink!(push(0)); 250 + expect(await asyncIterator.next()).toEqual({ value: 0, done: false }); 251 + expect(pulls).toBe(2); 252 + 253 + sink!(push(1)); 254 + expect(await asyncIterator.next()).toEqual({ value: 1, done: false }); 255 + expect(pulls).toBe(3); 256 + 257 + sink!(SignalKind.End); 258 + expect(await asyncIterator.next()).toEqual({ done: true }); 259 + expect(pulls).toBe(3); 260 + }); 261 + 262 + it('buffers actively pushed values', async () => { 263 + let pulls = 0; 264 + let sink: Sink<any> | null = null; 265 + 266 + const source: Source<any> = _sink => { 267 + sink = _sink; 268 + sink( 269 + start(signal => { 270 + if (signal === TalkbackKind.Pull) pulls++; 271 + }) 272 + ); 273 + }; 274 + 275 + const asyncIterator = sinks.toAsyncIterable(source)[Symbol.asyncIterator](); 276 + 277 + sink!(push(0)); 278 + sink!(push(1)); 279 + sink!(SignalKind.End); 280 + 281 + expect(pulls).toBe(1); 282 + expect(await asyncIterator.next()).toEqual({ value: 0, done: false }); 283 + expect(await asyncIterator.next()).toEqual({ value: 1, done: false }); 284 + expect(await asyncIterator.next()).toEqual({ done: true }); 285 + }); 286 + 287 + it('asynchronously waits for pulled values', async () => { 288 + let pulls = 0; 289 + let sink: Sink<any> | null = null; 290 + 291 + const source: Source<any> = _sink => { 292 + sink = _sink; 293 + sink( 294 + start(signal => { 295 + if (signal === TalkbackKind.Pull) pulls++; 296 + }) 297 + ); 298 + }; 299 + 300 + const asyncIterator = sinks.toAsyncIterable(source)[Symbol.asyncIterator](); 301 + expect(pulls).toBe(1); 302 + 303 + let resolved = false; 304 + 305 + const promise = asyncIterator.next().then(value => { 306 + resolved = true; 307 + return value; 308 + }); 309 + 310 + await Promise.resolve(); 311 + expect(resolved).toBe(false); 312 + 313 + sink!(push(0)); 314 + sink!(SignalKind.End); 315 + expect(await promise).toEqual({ value: 0, done: false }); 316 + expect(await asyncIterator.next()).toEqual({ done: true }); 317 + }); 318 + 319 + it('supports cancellation via return', async () => { 320 + let ended = false; 321 + let sink: Sink<any> | null = null; 322 + 323 + const source: Source<any> = _sink => { 324 + sink = _sink; 325 + sink( 326 + start(signal => { 327 + if (signal === TalkbackKind.Close) ended = true; 328 + }) 329 + ); 330 + }; 331 + 332 + const asyncIterator = sinks.toAsyncIterable(source)[Symbol.asyncIterator](); 333 + 334 + sink!(push(0)); 335 + expect(await asyncIterator.next()).toEqual({ value: 0, done: false }); 336 + expect(await asyncIterator.return!()).toEqual({ done: true }); 337 + 338 + sink!(push(1)); 339 + expect(await asyncIterator.next()).toEqual({ done: true }); 340 + 341 + expect(ended).toBeTruthy(); 342 + }); 343 + 344 + it('supports for-await-of', async () => { 345 + let pulls = 0; 346 + 347 + const source: Source<any> = sink => { 348 + sink( 349 + start(signal => { 350 + if (signal === TalkbackKind.Pull) { 351 + sink(pulls < 3 ? push(pulls++) : SignalKind.End); 352 + } 353 + }) 354 + ); 355 + }; 356 + 357 + const iterable = sinks.toAsyncIterable(source); 358 + const values: any[] = []; 359 + for await (const value of iterable) { 360 + values.push(value); 361 + } 362 + 363 + expect(values).toEqual([0, 1, 2]); 364 + }); 365 + 366 + it('supports for-await-of with early break', async () => { 367 + let pulls = 0; 368 + let closed = false; 369 + 370 + const source: Source<any> = sink => { 371 + sink( 372 + start(signal => { 373 + if (signal === TalkbackKind.Pull) { 374 + sink(pulls < 3 ? push(pulls++) : SignalKind.End); 375 + } else { 376 + closed = true; 377 + } 378 + }) 379 + ); 380 + }; 381 + 382 + const iterable = sinks.toAsyncIterable(source); 383 + for await (const value of iterable) { 384 + expect(value).toBe(0); 385 + break; 386 + } 387 + 388 + expect(closed).toBe(true); 389 + }); 390 + }); 391 + 232 392 describe('toObservable', () => { 233 393 it('creates an Observable mirroring the Wonka source', () => { 234 394 const next = vi.fn();
+46
src/sinks.ts
··· 38 38 })(source); 39 39 } 40 40 41 + const doneResult = { done: true } as IteratorReturnResult<void>; 42 + 43 + export const toAsyncIterable = <T>(source: Source<T>): AsyncIterable<T> => ({ 44 + [Symbol.asyncIterator](): AsyncIterator<T> { 45 + const buffer: T[] = []; 46 + 47 + let ended = false; 48 + let talkback = talkbackPlaceholder; 49 + let next: ((value: IteratorResult<T>) => void) | void; 50 + 51 + source(signal => { 52 + if (ended) { 53 + /*noop*/ 54 + } else if (signal === SignalKind.End) { 55 + if (next) next = next(doneResult); 56 + ended = true; 57 + } else if (signal.tag === SignalKind.Start) { 58 + (talkback = signal[0])(TalkbackKind.Pull); 59 + } else if (next) { 60 + next = next({ value: signal[0], done: false }); 61 + } else { 62 + buffer.push(signal[0]); 63 + } 64 + }); 65 + 66 + return { 67 + async next(): Promise<IteratorResult<T>> { 68 + if (ended && !buffer.length) { 69 + return doneResult; 70 + } else if (!ended && buffer.length <= 1) { 71 + talkback(TalkbackKind.Pull); 72 + } 73 + 74 + return buffer.length 75 + ? { value: buffer.shift()!, done: false } 76 + : new Promise(resolve => (next = resolve)); 77 + }, 78 + async return(): Promise<IteratorReturnResult<void>> { 79 + if (!ended) next = talkback(TalkbackKind.Close); 80 + ended = true; 81 + return doneResult; 82 + }, 83 + }; 84 + }, 85 + }); 86 + 41 87 export function toArray<T>(source: Source<T>): T[] { 42 88 const values: T[] = []; 43 89 let talkback = talkbackPlaceholder;