|
1 |
| -import { Eff, Err, Ctx, Result, isGenerator } from '../src/koka' |
| 1 | +import { Eff, Err, Ctx, Result, isGenerator, Async } from '../src/koka' |
2 | 2 |
|
3 | 3 | describe('Result', () => {
|
4 | 4 | it('should create ok result', () => {
|
@@ -446,6 +446,230 @@ describe('helpers', () => {
|
446 | 446 | })
|
447 | 447 | })
|
448 | 448 |
|
| 449 | +describe('Eff.all', () => { |
| 450 | + it('should handle sync effects', () => { |
| 451 | + function* effect1() { |
| 452 | + return 1 |
| 453 | + } |
| 454 | + |
| 455 | + function* effect2() { |
| 456 | + return '2' |
| 457 | + } |
| 458 | + |
| 459 | + function* program() { |
| 460 | + const combined: Generator<never, [number, string]> = Eff.all([effect1(), effect2()]) |
| 461 | + const results = yield* combined |
| 462 | + return results[0] + Number(results[1]) |
| 463 | + } |
| 464 | + |
| 465 | + const result = Eff.run(program()) |
| 466 | + expect(result).toBe(3) |
| 467 | + }) |
| 468 | + |
| 469 | + it('should handle async effects', async () => { |
| 470 | + function* effect1() { |
| 471 | + return yield* Eff.await(Promise.resolve(1)) |
| 472 | + } |
| 473 | + |
| 474 | + function* effect2() { |
| 475 | + return yield* Eff.await(Promise.resolve('2')) |
| 476 | + } |
| 477 | + |
| 478 | + function* program() { |
| 479 | + const combined: Generator<Async, [number, string]> = Eff.all([effect1(), effect2()]) |
| 480 | + const results = yield* combined |
| 481 | + |
| 482 | + console.log('results', results) |
| 483 | + |
| 484 | + return results[0] + Number(results[1]) |
| 485 | + } |
| 486 | + |
| 487 | + const result = await Eff.run(program()) |
| 488 | + expect(result).toBe(3) |
| 489 | + }) |
| 490 | + |
| 491 | + it('should handle mixed sync/async effects', async () => { |
| 492 | + function* syncEffect() { |
| 493 | + return 1 |
| 494 | + } |
| 495 | + |
| 496 | + function* asyncEffect() { |
| 497 | + return yield* Eff.await(Promise.resolve(2)) |
| 498 | + } |
| 499 | + |
| 500 | + function* program() { |
| 501 | + const combined: Generator<Async, [number, number]> = Eff.all([syncEffect(), asyncEffect()]) |
| 502 | + const results = yield* combined |
| 503 | + return results[0] + results[1] |
| 504 | + } |
| 505 | + |
| 506 | + const result = await Eff.run(program()) |
| 507 | + expect(result).toBe(3) |
| 508 | + }) |
| 509 | + |
| 510 | + it('should handle errors in effects', () => { |
| 511 | + class TestErr extends Eff.Err('TestErr')<string> {} |
| 512 | + |
| 513 | + function* effect1() { |
| 514 | + return 1 |
| 515 | + } |
| 516 | + |
| 517 | + function* effect2() { |
| 518 | + yield* Eff.throw(new TestErr('error')) |
| 519 | + return 2 |
| 520 | + } |
| 521 | + |
| 522 | + function* program() { |
| 523 | + const combined: Generator<TestErr, [number, number]> = Eff.all([effect1(), effect2()]) |
| 524 | + const results = yield* combined |
| 525 | + return results[0] + results[1] |
| 526 | + } |
| 527 | + |
| 528 | + const result = Eff.run(Eff.result(program())) |
| 529 | + |
| 530 | + expect(result).toEqual({ |
| 531 | + type: 'err', |
| 532 | + name: 'TestErr', |
| 533 | + error: 'error', |
| 534 | + }) |
| 535 | + }) |
| 536 | + |
| 537 | + it('should handle multiple async effects and run concurrently', async () => { |
| 538 | + async function delayTime(ms: number): Promise<void> { |
| 539 | + return new Promise((resolve) => setTimeout(resolve, ms)) |
| 540 | + } |
| 541 | + |
| 542 | + function* delayedEffect<T>(value: T, delay: number) { |
| 543 | + if (delay === 0) { |
| 544 | + yield* Eff.err('DelayError').throw('Delay cannot be zero') |
| 545 | + } |
| 546 | + |
| 547 | + yield* Eff.await(delayTime(delay)) |
| 548 | + |
| 549 | + return value |
| 550 | + } |
| 551 | + |
| 552 | + function* program() { |
| 553 | + const combined: Generator<Async | Err<'DelayError', string>, [number, string, boolean]> = Eff.all([ |
| 554 | + delayedEffect(1, 30), |
| 555 | + delayedEffect('2', 20), |
| 556 | + delayedEffect(false, 10), |
| 557 | + ]) |
| 558 | + |
| 559 | + const results = yield* combined |
| 560 | + return results |
| 561 | + } |
| 562 | + |
| 563 | + const start = Date.now() |
| 564 | + const result = await Eff.runResult(program()) |
| 565 | + const duration = Date.now() - start |
| 566 | + |
| 567 | + expect(result).toEqual({ |
| 568 | + type: 'ok', |
| 569 | + value: [1, '2', false], |
| 570 | + }) |
| 571 | + |
| 572 | + // Should run program in parallel |
| 573 | + expect(duration).toBeLessThan(50) // Should complete in less than 50ms |
| 574 | + }) |
| 575 | + |
| 576 | + it('should handle empty array', () => { |
| 577 | + function* program(): Generator<never, []> { |
| 578 | + const results = yield* Eff.all([]) |
| 579 | + return results |
| 580 | + } |
| 581 | + |
| 582 | + const result = Eff.run(program()) |
| 583 | + expect(result).toEqual([]) |
| 584 | + }) |
| 585 | + |
| 586 | + it('should handle function effects', () => { |
| 587 | + function* effect1(): Generator<never, number> { |
| 588 | + return 1 |
| 589 | + } |
| 590 | + |
| 591 | + function* effect2(): Generator<never, number> { |
| 592 | + return 2 |
| 593 | + } |
| 594 | + |
| 595 | + function* program(): Generator<never, number> { |
| 596 | + const results = yield* Eff.all([() => effect1(), () => effect2()]) |
| 597 | + return results[0] + results[1] |
| 598 | + } |
| 599 | + |
| 600 | + const result = Eff.run(program()) |
| 601 | + expect(result).toBe(3) |
| 602 | + }) |
| 603 | + |
| 604 | + it('should handle async errors with native try-catch', async () => { |
| 605 | + function* effectWithError(): Generator<Async, number> { |
| 606 | + const value = yield* Eff.await(Promise.reject(new Error('Async error'))) |
| 607 | + return value |
| 608 | + } |
| 609 | + |
| 610 | + function* program() { |
| 611 | + try { |
| 612 | + const results = yield* Eff.all([effectWithError(), Eff.await(Promise.resolve(2))]) |
| 613 | + return results[0] + results[1] |
| 614 | + } catch (err: unknown) { |
| 615 | + return err as Error |
| 616 | + } |
| 617 | + } |
| 618 | + |
| 619 | + const result = await Eff.run(program()) |
| 620 | + |
| 621 | + expect(result).toBeInstanceOf(Error) |
| 622 | + expect((result as Error).message).toBe('Async error') |
| 623 | + }) |
| 624 | + |
| 625 | + it('should propagate async errors', async () => { |
| 626 | + function* failingEffect(): Generator<Async, never> { |
| 627 | + yield* Eff.await(Promise.reject(new Error('Async error'))) |
| 628 | + /* istanbul ignore next */ |
| 629 | + throw new Error('Should not reach here') |
| 630 | + } |
| 631 | + |
| 632 | + function* program(): Generator<Async, number> { |
| 633 | + const results = yield* Eff.all([failingEffect(), Eff.await(Promise.resolve(2))]) |
| 634 | + return results[0] + results[1] |
| 635 | + } |
| 636 | + |
| 637 | + await expect(Eff.run(program())).rejects.toThrow('Async error') |
| 638 | + }) |
| 639 | + |
| 640 | + it('should handle thrown errors in async effects', async () => { |
| 641 | + function* effectWithThrow(): Generator<Async, number> { |
| 642 | + const value = yield* Eff.await( |
| 643 | + new Promise<number>((_, reject) => { |
| 644 | + setTimeout(() => { |
| 645 | + try { |
| 646 | + throw new Error('Thrown error') |
| 647 | + } catch (err) { |
| 648 | + reject(err) |
| 649 | + } |
| 650 | + }, 10) |
| 651 | + }), |
| 652 | + ) |
| 653 | + return value |
| 654 | + } |
| 655 | + |
| 656 | + function* program(): Generator<Async, number> { |
| 657 | + try { |
| 658 | + const results = yield* Eff.all([effectWithThrow(), Eff.await(Promise.resolve(2))]) |
| 659 | + return results[0] + results[1] |
| 660 | + } catch (err) { |
| 661 | + if (err instanceof Error) { |
| 662 | + return -100 |
| 663 | + } |
| 664 | + throw err |
| 665 | + } |
| 666 | + } |
| 667 | + |
| 668 | + const result = await Eff.run(program()) |
| 669 | + expect(result).toBe(-100) |
| 670 | + }) |
| 671 | +}) |
| 672 | + |
449 | 673 | describe('design first approach', () => {
|
450 | 674 | // predefined error effects
|
451 | 675 | class UserNotFoundErr extends Eff.Err('UserNotFound')<string> {}
|
|
0 commit comments