@typed/async-data 0.13.1 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,89 +1,444 @@
1
- import { describe, expect, it } from '@effect/vitest'
2
- import { Cause, Effect, Exit, Schema, TestClock } from 'effect'
3
- import * as AsyncData from './index.js'
1
+ // oxlint-disable require-yield
2
+ import { assert, describe, it } from "vitest";
3
+ import { Effect } from "effect";
4
+ import * as Cause from "effect/Cause";
5
+ import * as Exit from "effect/Exit";
6
+ import * as Option from "effect/Option";
7
+ import * as Result from "effect/Result";
8
+ import * as AsyncData from "./index.js";
4
9
 
5
- describe('AsyncData', () => {
6
- describe('Effect', () => {
7
- it.effect('NoData is a Failure', () =>
10
+ describe("AsyncData", () => {
11
+ describe("constructors", () => {
12
+ it("NoData", () =>
8
13
  Effect.gen(function* () {
9
- const noData = AsyncData.noData()
10
- const exit = yield* Effect.exit(noData)
14
+ const data = AsyncData.NoData;
15
+ assert(AsyncData.isNoData(data));
16
+ assert(data._tag === "NoData");
17
+ }).pipe(Effect.scoped, Effect.runPromise));
11
18
 
12
- expect(exit).toEqual(Exit.fail(noData))
13
- }),
14
- )
19
+ it("loading without progress", () =>
20
+ Effect.gen(function* () {
21
+ const data = AsyncData.loading();
22
+ assert(AsyncData.isLoading(data));
23
+ assert(data._tag === "Loading");
24
+ assert(data.progress === undefined);
25
+ }).pipe(Effect.scoped, Effect.runPromise));
26
+
27
+ it("loading with progress", () =>
28
+ Effect.gen(function* () {
29
+ const progress = { loaded: 50, total: 100 };
30
+ const data = AsyncData.loading(progress);
31
+ assert(AsyncData.isLoading(data));
32
+ assert(data.progress?.loaded === 50);
33
+ assert(data.progress?.total === 100);
34
+ }).pipe(Effect.scoped, Effect.runPromise));
35
+
36
+ it("success without progress", () =>
37
+ Effect.gen(function* () {
38
+ const data = AsyncData.success(42);
39
+ assert(AsyncData.isSuccess(data));
40
+ assert(data._tag === "Success");
41
+ assert(data.value === 42);
42
+ assert(data.progress === undefined);
43
+ }).pipe(Effect.scoped, Effect.runPromise));
44
+
45
+ it("success with progress", () =>
46
+ Effect.gen(function* () {
47
+ const progress = { loaded: 75, total: 100 };
48
+ const data = AsyncData.success(42, progress);
49
+ assert(AsyncData.isSuccess(data));
50
+ assert(data.value === 42);
51
+ assert(data.progress?.loaded === 75);
52
+ assert(data.progress?.total === 100);
53
+ }).pipe(Effect.scoped, Effect.runPromise));
54
+
55
+ it("failure without progress", () =>
56
+ Effect.gen(function* () {
57
+ const cause = Cause.fail("error");
58
+ const data = AsyncData.failure(cause);
59
+ assert(AsyncData.isFailure(data));
60
+ assert(data._tag === "Failure");
61
+ assert(data.cause === cause);
62
+ assert(data.progress === undefined);
63
+ }).pipe(Effect.scoped, Effect.runPromise));
64
+
65
+ it("failure with progress", () =>
66
+ Effect.gen(function* () {
67
+ const cause = Cause.fail("error");
68
+ const progress = { loaded: 25 };
69
+ const data = AsyncData.failure(cause, progress);
70
+ assert(AsyncData.isFailure(data));
71
+ assert(data.progress?.loaded === 25);
72
+ assert(data.progress?.total === undefined);
73
+ }).pipe(Effect.scoped, Effect.runPromise));
74
+
75
+ it("optimistic", () =>
76
+ Effect.gen(function* () {
77
+ const previous = AsyncData.success(10);
78
+ const data = AsyncData.optimistic(previous, 20);
79
+ assert(AsyncData.isOptimistic(data));
80
+ assert(data._tag === "Optimistic");
81
+ assert(data.value === 20);
82
+ assert(data.previous === previous);
83
+ }).pipe(Effect.scoped, Effect.runPromise));
84
+ });
15
85
 
16
- it.effect('Loading is a Failure', () =>
86
+ describe("type guards", () => {
87
+ it("isNoData", () =>
17
88
  Effect.gen(function* () {
18
- const loading = AsyncData.loading()
19
- const exit = yield* Effect.exit(loading)
89
+ assert(AsyncData.isNoData(AsyncData.NoData));
90
+ assert(!AsyncData.isNoData(AsyncData.loading()));
91
+ assert(!AsyncData.isNoData(AsyncData.success(1)));
92
+ }).pipe(Effect.scoped, Effect.runPromise));
20
93
 
21
- expect(exit).toEqual(Exit.fail(loading))
22
- }),
23
- )
94
+ it("isLoading", () =>
95
+ Effect.gen(function* () {
96
+ assert(AsyncData.isLoading(AsyncData.loading()));
97
+ assert(!AsyncData.isLoading(AsyncData.NoData));
98
+ assert(!AsyncData.isLoading(AsyncData.success(1)));
99
+ }).pipe(Effect.scoped, Effect.runPromise));
24
100
 
25
- it.effect('Failure is a Failure', () =>
101
+ it("isSuccess", () =>
26
102
  Effect.gen(function* () {
27
- const cause = Cause.fail('test')
28
- const failure = AsyncData.failure(cause)
29
- const exit = yield* Effect.exit(failure)
103
+ assert(AsyncData.isSuccess(AsyncData.success(1)));
104
+ assert(!AsyncData.isSuccess(AsyncData.NoData));
105
+ assert(!AsyncData.isSuccess(AsyncData.loading()));
106
+ }).pipe(Effect.scoped, Effect.runPromise));
30
107
 
31
- expect(exit).toEqual(Exit.failCause(cause))
32
- }),
33
- )
108
+ it("isFailure", () =>
109
+ Effect.gen(function* () {
110
+ const cause = Cause.fail("error");
111
+ assert(AsyncData.isFailure(AsyncData.failure(cause)));
112
+ assert(!AsyncData.isFailure(AsyncData.NoData));
113
+ assert(!AsyncData.isFailure(AsyncData.success(1)));
114
+ }).pipe(Effect.scoped, Effect.runPromise));
34
115
 
35
- it.effect('Success is a Success', () =>
116
+ it("isOptimistic", () =>
36
117
  Effect.gen(function* () {
37
- expect(yield* AsyncData.success(1)).toEqual(1)
38
- }),
39
- )
118
+ const previous = AsyncData.success(10);
119
+ const data = AsyncData.optimistic(previous, 20);
120
+ assert(AsyncData.isOptimistic(data));
121
+ assert(!AsyncData.isOptimistic(AsyncData.NoData));
122
+ assert(!AsyncData.isOptimistic(AsyncData.success(1)));
123
+ }).pipe(Effect.scoped, Effect.runPromise));
40
124
 
41
- it.effect('Refreshing<Success> is a Success', () =>
125
+ it("isAsyncData", () =>
42
126
  Effect.gen(function* () {
43
- expect(yield* AsyncData.refreshing(AsyncData.success(1))).toEqual(1)
44
- }),
45
- )
127
+ assert(AsyncData.isAsyncData(AsyncData.NoData));
128
+ assert(AsyncData.isAsyncData(AsyncData.loading()));
129
+ assert(AsyncData.isAsyncData(AsyncData.success(1)));
130
+ assert(AsyncData.isAsyncData(AsyncData.failure(Cause.fail("error"))));
131
+ assert(!AsyncData.isAsyncData(null));
132
+ assert(!AsyncData.isAsyncData({}));
133
+ assert(!AsyncData.isAsyncData({ _tag: "Invalid" }));
134
+ }).pipe(Effect.scoped, Effect.runPromise));
46
135
 
47
- it.effect('Refreshing<Failure> is a Failure', () =>
136
+ it("isRefreshing", () =>
48
137
  Effect.gen(function* () {
49
- const cause = Cause.fail('test')
50
- const failure = AsyncData.failure(cause)
51
- const exit = yield* Effect.exit(AsyncData.refreshing(failure))
138
+ const progress = { loaded: 50 };
139
+ const successWithProgress = AsyncData.success(1, progress);
140
+ const failureWithProgress = AsyncData.failure(Cause.fail("error"), progress);
141
+ assert(AsyncData.isRefreshing(successWithProgress));
142
+ assert(AsyncData.isRefreshing(failureWithProgress));
143
+ assert(!AsyncData.isRefreshing(AsyncData.success(1)));
144
+ assert(!AsyncData.isRefreshing(AsyncData.loading()));
145
+ }).pipe(Effect.scoped, Effect.runPromise));
52
146
 
53
- expect(exit).toEqual(Exit.failCause(cause))
54
- }),
55
- )
147
+ it("isPending", () =>
148
+ Effect.gen(function* () {
149
+ assert(AsyncData.isPending(AsyncData.loading()));
150
+ const progress = { loaded: 50 };
151
+ assert(AsyncData.isPending(AsyncData.success(1, progress)));
152
+ assert(AsyncData.isPending(AsyncData.failure(Cause.fail("error"), progress)));
153
+ assert(!AsyncData.isPending(AsyncData.NoData));
154
+ assert(!AsyncData.isPending(AsyncData.success(1)));
155
+ }).pipe(Effect.scoped, Effect.runPromise));
156
+ });
56
157
 
57
- it.effect('Optimistic is a Success', () =>
158
+ describe("extractors", () => {
159
+ it("getSuccess", () =>
58
160
  Effect.gen(function* () {
59
- expect(yield* AsyncData.optimistic(AsyncData.fail('not used'), 1)).toEqual(1)
60
- }),
61
- )
62
- })
161
+ const successData = AsyncData.success(42);
162
+ const successOption = AsyncData.getSuccess(successData);
163
+ assert(Option.isSome(successOption));
164
+ assert(successOption.value === 42);
165
+ assert(Option.isNone(AsyncData.getSuccess(AsyncData.NoData)));
166
+ assert(Option.isNone(AsyncData.getSuccess(AsyncData.loading())));
167
+ const previous = AsyncData.success(10);
168
+ const optimistic = AsyncData.optimistic(previous, 20);
169
+ const optimisticOption = AsyncData.getSuccess(optimistic);
170
+ assert(Option.isSome(optimisticOption));
171
+ assert(optimisticOption.value === 20);
172
+ }).pipe(Effect.scoped, Effect.runPromise));
63
173
 
64
- describe('Schema + LazyRef', () => {
65
- class Example extends AsyncData.AsyncData({
66
- success: Schema.Number,
67
- failure: Schema.String,
68
- }) {}
174
+ it("getCause", () =>
175
+ Effect.gen(function* () {
176
+ const cause = Cause.fail("error");
177
+ const failureData = AsyncData.failure(cause);
178
+ const causeOption = AsyncData.getCause(failureData);
179
+ assert(Option.isSome(causeOption));
180
+ assert(causeOption.value === cause);
181
+ assert(Option.isNone(AsyncData.getCause(AsyncData.NoData)));
182
+ assert(Option.isNone(AsyncData.getCause(AsyncData.loading())));
183
+ assert(Option.isNone(AsyncData.getCause(AsyncData.success(1))));
184
+ }).pipe(Effect.scoped, Effect.runPromise));
69
185
 
70
- it.scoped('keeps state', () =>
186
+ it("getError", () =>
71
187
  Effect.gen(function* () {
72
- const example = yield* AsyncData.lazyRef(Example)
188
+ const error = "error";
189
+ const cause = Cause.fail(error);
190
+ const failureData = AsyncData.failure(cause);
191
+ const errorOption = AsyncData.getError(failureData);
192
+ assert(Option.isSome(errorOption));
193
+ assert(errorOption.value === error);
194
+ assert(Option.isNone(AsyncData.getError(AsyncData.NoData)));
195
+ assert(Option.isNone(AsyncData.getError(AsyncData.loading())));
196
+ assert(Option.isNone(AsyncData.getError(AsyncData.success(1))));
197
+ }).pipe(Effect.scoped, Effect.runPromise));
198
+ });
73
199
 
74
- // Simulate an async operation
75
- yield* Effect.forkScoped(AsyncData.runEffect(example, Effect.delay(Effect.succeed(1), 500)))
200
+ describe("transformations", () => {
201
+ it("map", () =>
202
+ Effect.gen(function* () {
203
+ const data = AsyncData.success(5);
204
+ const mapped = AsyncData.map(data, (n) => n * 2);
205
+ assert(AsyncData.isSuccess(mapped));
206
+ assert(mapped.value === 10);
207
+ const noData = AsyncData.NoData;
208
+ const mappedNoData = AsyncData.map(noData, (n: number) => n * 2);
209
+ assert(AsyncData.isNoData(mappedNoData));
210
+ const optimistic = AsyncData.optimistic(AsyncData.success(5), 10);
211
+ const mappedOptimistic = AsyncData.map(optimistic, (n) => n * 2);
212
+ assert(AsyncData.isOptimistic(mappedOptimistic));
213
+ assert(mappedOptimistic.value === 20);
214
+ assert(AsyncData.isSuccess(mappedOptimistic.previous));
215
+ assert(mappedOptimistic.previous.value === 10);
216
+ }).pipe(Effect.scoped, Effect.runPromise));
76
217
 
77
- expect(yield* example).toEqual(Example.noData())
218
+ it("map preserves progress", () =>
219
+ Effect.gen(function* () {
220
+ const progress = { loaded: 50, total: 100 };
221
+ const data = AsyncData.success(5, progress);
222
+ const mapped = AsyncData.map(data, (n) => n * 2);
223
+ assert(AsyncData.isSuccess(mapped));
224
+ assert(mapped.progress?.loaded === 50);
225
+ assert(mapped.progress?.total === 100);
226
+ }).pipe(Effect.scoped, Effect.runPromise));
78
227
 
79
- yield* TestClock.adjust(1)
228
+ it("flatMap", () =>
229
+ Effect.gen(function* () {
230
+ const data = AsyncData.success(5);
231
+ const flatMapped = AsyncData.flatMap(data, (n) => AsyncData.success(n * 2));
232
+ assert(AsyncData.isSuccess(flatMapped));
233
+ assert(flatMapped.value === 10);
234
+ const noData = AsyncData.NoData;
235
+ const flatMappedNoData = AsyncData.flatMap(noData, (n: number) => AsyncData.success(n * 2));
236
+ assert(AsyncData.isNoData(flatMappedNoData));
237
+ const optimistic = AsyncData.optimistic(AsyncData.success(5), 10);
238
+ const flatMappedOptimistic = AsyncData.flatMap(optimistic, (n) => AsyncData.success(n * 2));
239
+ assert(AsyncData.isSuccess(flatMappedOptimistic));
240
+ assert(flatMappedOptimistic.value === 20);
241
+ const optimisticToOptimistic = AsyncData.optimistic(AsyncData.success(5), 10);
242
+ const flatMappedOptimistic2 = AsyncData.flatMap(optimisticToOptimistic, (n, prev) =>
243
+ AsyncData.optimistic(prev, n * 2),
244
+ );
245
+ assert(AsyncData.isOptimistic(flatMappedOptimistic2));
246
+ assert(flatMappedOptimistic2.value === 20);
247
+ }).pipe(Effect.scoped, Effect.runPromise));
80
248
 
81
- expect(yield* example).toEqual(Example.loading())
249
+ it("mapError", () =>
250
+ Effect.gen(function* () {
251
+ const cause = Cause.fail("error");
252
+ const data = AsyncData.failure(cause);
253
+ const mapped = AsyncData.mapError(data, (e) => `mapped: ${e}`);
254
+ assert(AsyncData.isFailure(mapped));
255
+ const error = Cause.findErrorOption(mapped.cause);
256
+ assert(Option.isSome(error));
257
+ assert(error.value === "mapped: error");
258
+ const noData = AsyncData.NoData;
259
+ const mappedNoData = AsyncData.mapError(noData, (e: string) => `mapped: ${e}`);
260
+ assert(AsyncData.isNoData(mappedNoData));
261
+ const optimistic = AsyncData.optimistic(AsyncData.failure(cause), 10);
262
+ const mappedOptimistic = AsyncData.mapError(optimistic, (e) => `mapped: ${e}`);
263
+ assert(AsyncData.isOptimistic(mappedOptimistic));
264
+ assert(AsyncData.isFailure(mappedOptimistic.previous));
265
+ }).pipe(Effect.scoped, Effect.runPromise));
266
+
267
+ it("mapError preserves progress", () =>
268
+ Effect.gen(function* () {
269
+ const progress = { loaded: 50 };
270
+ const cause = Cause.fail("error");
271
+ const data = AsyncData.failure(cause, progress);
272
+ const mapped = AsyncData.mapError(data, (e) => `mapped: ${e}`);
273
+ assert(AsyncData.isFailure(mapped));
274
+ assert(mapped.progress?.loaded === 50);
275
+ }).pipe(Effect.scoped, Effect.runPromise));
276
+ });
82
277
 
83
- yield* TestClock.adjust(500)
278
+ describe("utilities", () => {
279
+ it("startLoading from NoData", () =>
280
+ Effect.gen(function* () {
281
+ const data = AsyncData.NoData;
282
+ const started = AsyncData.startLoading(data);
283
+ assert(AsyncData.isLoading(started));
284
+ }).pipe(Effect.scoped, Effect.runPromise));
285
+
286
+ it("startLoading from Success", () =>
287
+ Effect.gen(function* () {
288
+ const data = AsyncData.success(42);
289
+ const progress = { loaded: 50 };
290
+ const started = AsyncData.startLoading(data, progress);
291
+ assert(AsyncData.isSuccess(started));
292
+ assert(started.value === 42);
293
+ assert(started.progress?.loaded === 50);
294
+ }).pipe(Effect.scoped, Effect.runPromise));
84
295
 
85
- expect(yield* example).toEqual(Example.success(1))
86
- }),
87
- )
88
- })
89
- })
296
+ it("startLoading from Failure", () =>
297
+ Effect.gen(function* () {
298
+ const cause = Cause.fail("error");
299
+ const data = AsyncData.failure(cause);
300
+ const progress = { loaded: 50 };
301
+ const started = AsyncData.startLoading(data, progress);
302
+ assert(AsyncData.isFailure(started));
303
+ assert(started.cause === cause);
304
+ assert(started.progress?.loaded === 50);
305
+ }).pipe(Effect.scoped, Effect.runPromise));
306
+
307
+ it("startLoading from Optimistic", () =>
308
+ Effect.gen(function* () {
309
+ const previous = AsyncData.success(10);
310
+ const data = AsyncData.optimistic(previous, 20);
311
+ const progress = { loaded: 50 };
312
+ const started = AsyncData.startLoading(data, progress);
313
+ assert(AsyncData.isOptimistic(started));
314
+ assert(started.value === 20);
315
+ assert(AsyncData.isSuccess(started.previous));
316
+ assert(started.previous.progress?.loaded === 50);
317
+ }).pipe(Effect.scoped, Effect.runPromise));
318
+
319
+ it("stopLoading from Success with progress", () =>
320
+ Effect.gen(function* () {
321
+ const progress = { loaded: 50 };
322
+ const data = AsyncData.success(42, progress);
323
+ const stopped = AsyncData.stopLoading(data);
324
+ assert(AsyncData.isSuccess(stopped));
325
+ assert(stopped.value === 42);
326
+ assert(stopped.progress === undefined);
327
+ }).pipe(Effect.scoped, Effect.runPromise));
328
+
329
+ it("stopLoading from Failure with progress", () =>
330
+ Effect.gen(function* () {
331
+ const cause = Cause.fail("error");
332
+ const progress = { loaded: 50 };
333
+ const data = AsyncData.failure(cause, progress);
334
+ const stopped = AsyncData.stopLoading(data);
335
+ assert(AsyncData.isFailure(stopped));
336
+ assert(stopped.cause === cause);
337
+ assert(stopped.progress === undefined);
338
+ }).pipe(Effect.scoped, Effect.runPromise));
339
+
340
+ it("stopLoading from Optimistic", () =>
341
+ Effect.gen(function* () {
342
+ const previous = AsyncData.success(10, { loaded: 50 });
343
+ const data = AsyncData.optimistic(previous, 20);
344
+ const stopped = AsyncData.stopLoading(data);
345
+ assert(AsyncData.isOptimistic(stopped));
346
+ assert(stopped.value === 20);
347
+ assert(AsyncData.isSuccess(stopped.previous));
348
+ assert(stopped.previous.progress === undefined);
349
+ }).pipe(Effect.scoped, Effect.runPromise));
350
+
351
+ it("stopLoading from NoData", () =>
352
+ Effect.gen(function* () {
353
+ const data = AsyncData.NoData;
354
+ const stopped = AsyncData.stopLoading(data);
355
+ assert(AsyncData.isNoData(stopped));
356
+ }).pipe(Effect.scoped, Effect.runPromise));
357
+
358
+ it("match", () =>
359
+ Effect.gen(function* () {
360
+ const noDataResult = AsyncData.match(AsyncData.NoData, {
361
+ NoData: () => "no-data",
362
+ Loading: () => "loading",
363
+ Failure: () => "failure",
364
+ Success: () => "success",
365
+ Optimistic: () => "optimistic",
366
+ });
367
+ assert(noDataResult === "no-data");
368
+
369
+ const loadingResult = AsyncData.match(AsyncData.loading(), {
370
+ NoData: () => "no-data",
371
+ Loading: () => "loading",
372
+ Failure: () => "failure",
373
+ Success: () => "success",
374
+ Optimistic: () => "optimistic",
375
+ });
376
+ assert(loadingResult === "loading");
377
+
378
+ const successResult = AsyncData.match(AsyncData.success(42), {
379
+ NoData: () => "no-data",
380
+ Loading: () => "loading",
381
+ Failure: () => "failure",
382
+ Success: (value) => `success-${value}`,
383
+ Optimistic: () => "optimistic",
384
+ });
385
+ assert(successResult === "success-42");
386
+
387
+ const cause = Cause.fail("error");
388
+ const failureResult = AsyncData.match(AsyncData.failure(cause), {
389
+ NoData: () => "no-data",
390
+ Loading: () => "loading",
391
+ Failure: (cause) =>
392
+ `failure-${Cause.findErrorOption(cause).pipe(Option.getOrElse(() => "unknown"))}`,
393
+ Success: () => "success",
394
+ Optimistic: () => "optimistic",
395
+ });
396
+ assert(failureResult === "failure-error");
397
+
398
+ const previous = AsyncData.success(10);
399
+ const optimisticResult = AsyncData.match(AsyncData.optimistic(previous, 20), {
400
+ NoData: () => "no-data",
401
+ Loading: () => "loading",
402
+ Failure: () => "failure",
403
+ Success: () => "success",
404
+ Optimistic: (value) => `optimistic-${value}`,
405
+ });
406
+ assert(optimisticResult === "optimistic-20");
407
+ }).pipe(Effect.scoped, Effect.runPromise));
408
+
409
+ it("fromExit Success", () =>
410
+ Effect.gen(function* () {
411
+ const exit = Exit.succeed(42);
412
+ const data = AsyncData.fromExit(exit);
413
+ assert(AsyncData.isSuccess(data));
414
+ assert(data.value === 42);
415
+ }).pipe(Effect.scoped, Effect.runPromise));
416
+
417
+ it("fromExit Failure", () =>
418
+ Effect.gen(function* () {
419
+ const cause = Cause.fail("error");
420
+ const exit = Exit.failCause(cause);
421
+ const data = AsyncData.fromExit(exit);
422
+ assert(AsyncData.isFailure(data));
423
+ assert(data.cause === cause);
424
+ }).pipe(Effect.scoped, Effect.runPromise));
425
+
426
+ it("fromResult Success", () =>
427
+ Effect.gen(function* () {
428
+ const result = Result.succeed(42);
429
+ const data = AsyncData.fromResult(result);
430
+ assert(AsyncData.isSuccess(data));
431
+ assert(data.value === 42);
432
+ }).pipe(Effect.scoped, Effect.runPromise));
433
+
434
+ it("fromResult Failure", () =>
435
+ Effect.gen(function* () {
436
+ const result = Result.fail("error");
437
+ const data = AsyncData.fromResult(result);
438
+ assert(AsyncData.isFailure(data));
439
+ const error = Cause.findErrorOption(data.cause);
440
+ assert(Option.isSome(error));
441
+ assert(error.value === "error");
442
+ }).pipe(Effect.scoped, Effect.runPromise));
443
+ });
444
+ });