Skip to content

Commit a5823c5

Browse files
committed
feat: Eff.all support has been added for handling multiple effects concurrently
1 parent 512628b commit a5823c5

File tree

9 files changed

+682
-251
lines changed

9 files changed

+682
-251
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# koka
22

3+
## 1.0.4
4+
5+
### Patch Changes
6+
7+
- feat: add design-first approach support
8+
39
## 1.0.3
410

511
### Patch Changes

__tests__/koka.test.ts

Lines changed: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Eff, Err, Ctx, Result, isGenerator } from '../src/koka'
1+
import { Eff, Err, Ctx, Result, isGenerator, Async } from '../src/koka'
22

33
describe('Result', () => {
44
it('should create ok result', () => {
@@ -446,6 +446,230 @@ describe('helpers', () => {
446446
})
447447
})
448448

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+
449673
describe('design first approach', () => {
450674
// predefined error effects
451675
class UserNotFoundErr extends Eff.Err('UserNotFound')<string> {}

cjs/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from './koka'
1+
export * from './koka';

cjs/index.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)