@typed/async-data 0.2.0 → 0.3.1

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.
package/src/AsyncData.ts CHANGED
@@ -6,12 +6,22 @@
6
6
  */
7
7
 
8
8
  import type { Effect } from "effect"
9
- import { Cause, Data, Equal, Equivalence, Exit, Option, Unify } from "effect"
9
+ import * as Cause from "effect/Cause"
10
+ import * as Data from "effect/Data"
11
+ import * as Duration from "effect/Duration"
12
+ import * as Either from "effect/Either"
13
+ import * as Equal from "effect/Equal"
14
+ import * as Equivalence from "effect/Equivalence"
15
+ import * as Exit from "effect/Exit"
10
16
  import { dual } from "effect/Function"
17
+ import * as Option from "effect/Option"
18
+ import * as Unify from "effect/Unify"
11
19
  import * as internal from "./internal/async-data.js"
12
20
  import { FAILURE_TAG, LOADING_TAG, NO_DATA_TAG, SUCCESS_TAG } from "./internal/tag.js"
13
21
  import * as Progress from "./Progress.js"
14
22
 
23
+ const getCurrentTimestamp = () => Date.now()
24
+
15
25
  /**
16
26
  * AsyncData represents a piece of data which is acquired asynchronously with loading, failure, and progress states
17
27
  * in addition to Option-like states of NoData and Success.
@@ -107,15 +117,18 @@ export class Loading extends Data.TaggedError(LOADING_TAG)<LoadingOptions> {
107
117
  * @since 1.0.0
108
118
  */
109
119
  export type LoadingOptions = {
120
+ readonly timestamp: number // Date.now()
110
121
  readonly progress: Option.Option<Progress.Progress>
111
122
  }
112
123
 
113
124
  /**
114
125
  * @since 1.0.0
115
126
  */
116
- export type OptionalPartial<A> = {
117
- [K in keyof A]+?: [A[K]] extends [Option.Option<infer R>] ? R | undefined : A[K]
118
- }
127
+ export type OptionalPartial<A> = [
128
+ {
129
+ [K in keyof A]+?: [A[K]] extends [Option.Option<infer R>] ? R | undefined : A[K]
130
+ }
131
+ ] extends [infer R] ? { readonly [K in keyof R]: R[K] } : never
119
132
 
120
133
  /**
121
134
  * @since 1.0.0
@@ -125,13 +138,14 @@ export const loading: {
125
138
  <E, A>(options?: OptionalPartial<LoadingOptions>): AsyncData<E, A>
126
139
  } = (options?: OptionalPartial<LoadingOptions>): Loading =>
127
140
  new Loading({
141
+ timestamp: options?.timestamp ?? getCurrentTimestamp(),
128
142
  progress: Option.fromNullable(options?.progress)
129
143
  })
130
144
 
131
145
  /**
132
146
  * @since 1.0.0
133
147
  */
134
- export interface Failure<E> extends Effect.Effect<never, E, never> {
148
+ export interface Failure<out E> extends Effect.Effect<never, E, never> {
135
149
  /**
136
150
  * @since 1.18.0
137
151
  */
@@ -142,6 +156,11 @@ export interface Failure<E> extends Effect.Effect<never, E, never> {
142
156
  */
143
157
  readonly cause: Cause.Cause<E>
144
158
 
159
+ /**
160
+ * @since 1.20.0
161
+ */
162
+ readonly timestamp: number // Date.now()
163
+
145
164
  /**
146
165
  * @since 1.18.0
147
166
  */
@@ -167,6 +186,7 @@ export interface Failure<E> extends Effect.Effect<never, E, never> {
167
186
  * @since 1.0.0
168
187
  */
169
188
  export type FailureOptions = {
189
+ readonly timestamp: number // Date.now()
170
190
  readonly refreshing: Option.Option<Loading>
171
191
  }
172
192
 
@@ -179,6 +199,7 @@ export const failCause: {
179
199
  } = <E>(cause: Cause.Cause<E>, options?: OptionalPartial<FailureOptions>): Failure<E> =>
180
200
  new internal.FailureImpl(
181
201
  cause,
202
+ options?.timestamp ?? getCurrentTimestamp(),
182
203
  Option.fromNullable(options?.refreshing)
183
204
  )
184
205
 
@@ -193,9 +214,13 @@ export const fail: {
193
214
  /**
194
215
  * @since 1.0.0
195
216
  */
196
- export interface Success<A> extends Effect.Effect<never, never, A> {
217
+ export interface Success<out A> extends Effect.Effect<never, never, A> {
197
218
  readonly _tag: typeof SUCCESS_TAG
198
219
  readonly value: A
220
+ /**
221
+ * @since 1.20.0
222
+ */
223
+ readonly timestamp: number // Date.now()
199
224
  readonly refreshing: Option.Option<Loading>
200
225
 
201
226
  readonly [Unify.typeSymbol]: unknown
@@ -207,6 +232,7 @@ export interface Success<A> extends Effect.Effect<never, never, A> {
207
232
  * @since 1.0.0
208
233
  */
209
234
  export type SuccessOptions = {
235
+ readonly timestamp: number // Date.now()
210
236
  readonly refreshing: Option.Option<Loading>
211
237
  }
212
238
 
@@ -219,6 +245,7 @@ export const success: {
219
245
  } = <A>(value: A, options?: OptionalPartial<SuccessOptions>): Success<A> =>
220
246
  new internal.SuccessImpl(
221
247
  value,
248
+ options?.timestamp ?? getCurrentTimestamp(),
222
249
  Option.fromNullable(options?.refreshing)
223
250
  )
224
251
 
@@ -400,6 +427,7 @@ const optionProgressEq = Option.getEquivalence(Progress.equals)
400
427
 
401
428
  const loadingEquivalence: Equivalence.Equivalence<Loading> = Equivalence.struct({
402
429
  _tag: Equivalence.string,
430
+ timestamp: Equivalence.number,
403
431
  progress: optionProgressEq
404
432
  })
405
433
 
@@ -408,6 +436,7 @@ const optionLoadingEq = Option.getEquivalence(loadingEquivalence)
408
436
  const failureEquivalence: Equivalence.Equivalence<Failure<any>> = Equivalence.struct({
409
437
  _tag: Equivalence.string,
410
438
  cause: Equal.equals,
439
+ timestamp: Equivalence.number,
411
440
  refreshing: optionLoadingEq
412
441
  })
413
442
 
@@ -415,6 +444,7 @@ const successEquivalence = <A>(valueEq: Equivalence.Equivalence<A>): Equivalence
415
444
  Equivalence.struct({
416
445
  _tag: Equivalence.string,
417
446
  value: valueEq,
447
+ timestamp: Equivalence.number,
418
448
  refreshing: optionLoadingEq
419
449
  })
420
450
 
@@ -432,3 +462,73 @@ export const getEquivalence =
432
462
  Success: (_, s1) => isSuccess(b) ? successEquivalence(valueEq)(s1, b) : false
433
463
  })
434
464
  }
465
+
466
+ /**
467
+ * @since 1.0.0
468
+ */
469
+ export function fromExit<E, A>(exit: Exit.Exit<E, A>): AsyncData<E, A> {
470
+ return Exit.match(exit, {
471
+ onFailure: (cause) => failCause(cause),
472
+ onSuccess: (value) => success(value)
473
+ })
474
+ }
475
+
476
+ /**
477
+ * @since 1.0.0
478
+ */
479
+ export function fromEither<E, A>(either: Either.Either<E, A>): AsyncData<E, A> {
480
+ return Either.match(either, {
481
+ onLeft: (e) => fail(e),
482
+ onRight: (a) => success(a)
483
+ })
484
+ }
485
+
486
+ const isAsyncDataFirst = (args: IArguments) => args.length === 3 || isAsyncData(args[0])
487
+
488
+ /**
489
+ * @since 1.0.0
490
+ */
491
+ export const isExpired: {
492
+ (ttl: Duration.DurationInput, now?: number): <E, A>(data: AsyncData<E, A>) => boolean
493
+ <E, A>(data: AsyncData<E, A>, ttl: Duration.DurationInput, now?: number): boolean
494
+ } = dual(isAsyncDataFirst, function isExpired<E, A>(
495
+ data: AsyncData<E, A>,
496
+ ttl: Duration.DurationInput,
497
+ now: number = getCurrentTimestamp()
498
+ ): boolean {
499
+ return match(data, {
500
+ NoData: () => true,
501
+ Loading: () => false,
502
+ Failure: (_, f) =>
503
+ Option.isNone(f.refreshing)
504
+ ? isPastTTL(f.timestamp, ttl, now)
505
+ : isPastTTL(f.refreshing.value.timestamp, ttl, now),
506
+ Success: (_, s) =>
507
+ Option.isNone(s.refreshing)
508
+ ? isPastTTL(s.timestamp, ttl, now) :
509
+ isPastTTL(s.refreshing.value.timestamp, ttl, now)
510
+ })
511
+ })
512
+
513
+ function isPastTTL(timestamp: number, ttl: Duration.DurationInput, now: number): boolean {
514
+ const millis = Duration.toMillis(ttl)
515
+
516
+ return now - timestamp > millis
517
+ }
518
+
519
+ /**
520
+ * Checks if two AsyncData are equal, disregarding the timestamps associated with them. Useful for testing
521
+ * without needing to manage timestamps.
522
+ *
523
+ * @since 1.0.0
524
+ */
525
+ export function dataEqual<E, A>(first: AsyncData<E, A>, second: AsyncData<E, A>): boolean {
526
+ return match(first, {
527
+ NoData: () => isNoData(second),
528
+ Loading: (l) => isLoading(second) && Equal.equals(l.progress, second.progress),
529
+ Failure: (_, f1) =>
530
+ isFailure(second) && Equal.equals(f1.cause, second.cause) && Equal.equals(f1.refreshing, second.refreshing),
531
+ Success: (_, s1) =>
532
+ isSuccess(second) && Equal.equals(s1.value, second.value) && Equal.equals(s1.refreshing, second.refreshing)
533
+ })
534
+ }
package/src/Progress.ts CHANGED
@@ -2,25 +2,30 @@
2
2
  * @since 1.0.0
3
3
  */
4
4
 
5
- import { Equivalence, Option } from "effect"
5
+ import { Data } from "effect"
6
+ import * as Equivalence from "effect/Equivalence"
6
7
  import { dual } from "effect/Function"
8
+ import * as Option from "effect/Option"
7
9
 
8
10
  /**
9
11
  * @since 1.0.0
10
12
  */
11
- export interface Progress {
12
- readonly loaded: bigint
13
- readonly total: Option.Option<bigint>
13
+ export interface Progress extends
14
+ Data.Data<{
15
+ readonly loaded: bigint
16
+ readonly total: Option.Option<bigint>
17
+ }>
18
+ {
14
19
  }
15
20
 
16
21
  /**
17
22
  * @since 1.0.0
18
23
  */
19
24
  export function Progress(loaded: bigint, total: Option.Option<bigint> = Option.none()): Progress {
20
- return {
25
+ return Data.struct({
21
26
  loaded,
22
27
  total
23
- }
28
+ })
24
29
  }
25
30
 
26
31
  /**
@@ -58,8 +63,23 @@ export const setTotal: {
58
63
  * @since 1.0.0
59
64
  */
60
65
  export const equals: Equivalence.Equivalence<Progress> = Equivalence.struct<
61
- { readonly [K in keyof Progress]: Equivalence.Equivalence<Progress[K]> }
66
+ { readonly [K in Exclude<keyof Progress, keyof Data.Data<any>>]: Equivalence.Equivalence<Progress[K]> }
62
67
  >({
63
68
  loaded: Equivalence.bigint,
64
69
  total: Option.getEquivalence(Equivalence.bigint)
65
70
  })
71
+
72
+ /**
73
+ * @since 1.0.0
74
+ */
75
+ export function pretty(progress: Progress): string {
76
+ return `${progress.loaded}${
77
+ Option.match(
78
+ progress.total,
79
+ {
80
+ onNone: () => "",
81
+ onSome: (total) => `/${total}`
82
+ }
83
+ )
84
+ }`
85
+ }