-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
200 lines (185 loc) · 6.83 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/**
* `EffectDef<A>` defines the set of effects.
* Users can extend this interface to define custom effects.
*
* Each property defines an effect; the property name is the ID of the effect, and the property type
* is the associated data type of the effect.
* @param A Placeholder for the type that is returned when an effect is performed.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface EffectDef<A> {}
/**
* `EffectId` is the union of all possible effect IDs.
*/
export type EffectId = keyof EffectDef<unknown>;
/**
* `EffectData<Row, A>` represents the data types associated to the effects in `Row`
*/
export type EffectData<Row extends EffectId, A> = EffectDef<A>[Row];
/**
* `Effect<Row, A>` represents an effect that returns `A` when performed.
* It distributes over `Row` i.e. `Effect<X | Y, A> = Effect<X, A> | Effect<Y, A>`
*/
export type Effect<Row extends EffectId, A> =
Row extends infer Id extends EffectId ?
Readonly<{
id: Row;
data: EffectData<Id, A>;
}>
: never;
/**
* `Effectful<Row, A>` represents an effectful computation that may perform effects in `Row` and
* returns `A`.
*/
export type Effectful<Row extends EffectId, A> = Generator<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Effect<Row, any>,
A,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>;
/**
* `Eff<Row, A>` is an alias for `Effectful<Row, A>`.
*/
export type Eff<Row extends EffectId, A> = Effectful<Row, A>;
/**
* Creates an effectful computation that performs a single effect.
* @param eff The effect to perform.
* @returns An effectful computation.
*/
export function* perform<Row extends EffectId, A>(eff: Effect<Row, A>): Effectful<Row, A> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return yield eff;
}
/**
* `Handler<Row, R>` handles effects in `Row` and returns a value of type `R`.
* It distributes over `Row` i.e. `Handler<X | Y, A> = Handler<X, A> | Handler<Y, A>`
*/
export type Handler<Row extends EffectId, R> =
Row extends infer Id extends EffectId ? <A>(eff: Effect<Id, A>, resume: (value: A) => R) => R
: never;
/**
* `Handlers<Row, R>` is a set of effect handlers.
*/
export type Handlers<Row extends EffectId, R> = Readonly<{
[Id in Row]: Handler<Id, R>;
}>;
/**
* Runs an effectful computation.
* @param comp The effectful computation to run.
* It can `yield*` to compose other effectful computations.
* @param ret A function that handles the return value of the computation.
* @param handlers Effect handlers that handle effects performed in the computation.
* An effect handler can resolve an effect and resume the computation, or abort the whole computation.
* @returns A value returned from `ret` or `handlers`.
*/
export function run<Row extends EffectId, A, R>(
comp: Effectful<Row, A>,
ret: (value: A) => R,
handlers: Handlers<Row, R>,
): R {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loop = (value?: any): R => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const res = comp.next(value);
if (res.done) {
return ret(res.value);
} else {
let resumed = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resume = (value: any): R => {
if (resumed) {
throw new Error("resume cannot be called more than once");
}
resumed = true;
return loop(value);
};
const eff = res.value;
// eslint-disable-next-line @susisu/safe-typescript/no-type-assertion
const handler = handlers[eff.id as Row];
// eslint-disable-next-line @susisu/safe-typescript/no-type-assertion
return handler(eff as never, resume);
}
};
return loop();
}
/**
* Maps the return value of the computation.
* @param comp A computation.
* @param func A function that maps the return value of the computation.
* @returns A computation that returns the value mapped by `func`.
*/
export function* map<Row extends EffectId, A, B>(
comp: Effectful<Row, A>,
func: (value: A) => B,
): Effectful<Row, B> {
const value = yield* comp;
return func(value);
}
/**
* Creates a computation that does not perform any effect and returns the given value.
* @param value The value that the compuation returns.
* @returns A new computation.
*/
// eslint-disable-next-line require-yield
export function* pure<Row extends EffectId, A>(value: A): Effectful<Row, A> {
return value;
}
/**
* Composes two computations sequentially.
* @param comp A computation.
* @param func A function that takes the return value of the first computation and returns a
* subsequent computation.
* @returns A composed computation.
*/
export function* bind<Row extends EffectId, A, B>(
comp: Effectful<Row, A>,
func: (value: A) => Effectful<Row, B>,
): Effectful<Row, B> {
const value = yield* comp;
return yield* func(value);
}
/**
* Handles a subset of effects performed by a computation.
* @param comp The effectful computation.
* @param ret A function that handles the return value of the computation.
* @param handlers Effect handlers that handle effects performed in the computation.
* @returns The same computation that only performs the rest subset of the effects.
*/
export function handle<Row extends EffectId, SubRow extends Row, A, R>(
comp: Effectful<Row, A>,
ret: (value: A) => Effectful<Exclude<Row, SubRow>, R>,
handlers: Handlers<SubRow, Effectful<Exclude<Row, SubRow>, R>>,
): Effectful<Exclude<Row, SubRow>, R> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loop = (value?: any): Effectful<Exclude<Row, SubRow>, R> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const res = comp.next(value);
if (res.done) {
return ret(res.value);
} else {
let resumed = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resume = (value: any): Effectful<Exclude<Row, SubRow>, R> => {
if (resumed) {
throw new Error("resume cannot be called more than once");
}
resumed = true;
return loop(value);
};
const eff = res.value;
// `eff.id in handlers` does not always imply `eff.id: SubRow` because of subtyping, but we assume so.
// eslint-disable-next-line @susisu/safe-typescript/no-unsafe-object-property-check
if (eff.id in handlers) {
// eslint-disable-next-line @susisu/safe-typescript/no-type-assertion
const handler = handlers[eff.id as SubRow];
// eslint-disable-next-line @susisu/safe-typescript/no-type-assertion
return handler(eff as never, resume);
} else {
// eslint-disable-next-line @susisu/safe-typescript/no-type-assertion, @typescript-eslint/no-explicit-any
return bind(perform(eff as Effect<Exclude<Row, SubRow>, any>), resume);
}
}
};
return loop();
}