@typed/async-data 0.1.2 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typed/async-data",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,9 +10,9 @@
10
10
  "sideEffects": [],
11
11
  "author": "Typed contributors",
12
12
  "dependencies": {
13
- "@effect/schema": "0.49.4",
14
- "effect": "2.0.0-next.56",
15
- "fast-check": "^3.14.0"
13
+ "@effect/schema": "0.59.1",
14
+ "effect": "^2.0.3",
15
+ "fast-check": "^3.15.0"
16
16
  },
17
17
  "exports": {
18
18
  "./package.json": "./package.json",
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,6 +117,7 @@ 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
 
@@ -125,6 +136,7 @@ export const loading: {
125
136
  <E, A>(options?: OptionalPartial<LoadingOptions>): AsyncData<E, A>
126
137
  } = (options?: OptionalPartial<LoadingOptions>): Loading =>
127
138
  new Loading({
139
+ timestamp: options?.timestamp ?? getCurrentTimestamp(),
128
140
  progress: Option.fromNullable(options?.progress)
129
141
  })
130
142
 
@@ -142,6 +154,11 @@ export interface Failure<E> extends Effect.Effect<never, E, never> {
142
154
  */
143
155
  readonly cause: Cause.Cause<E>
144
156
 
157
+ /**
158
+ * @since 1.20.0
159
+ */
160
+ readonly timestamp: number // Date.now()
161
+
145
162
  /**
146
163
  * @since 1.18.0
147
164
  */
@@ -167,6 +184,7 @@ export interface Failure<E> extends Effect.Effect<never, E, never> {
167
184
  * @since 1.0.0
168
185
  */
169
186
  export type FailureOptions = {
187
+ readonly timestamp: number // Date.now()
170
188
  readonly refreshing: Option.Option<Loading>
171
189
  }
172
190
 
@@ -179,6 +197,7 @@ export const failCause: {
179
197
  } = <E>(cause: Cause.Cause<E>, options?: OptionalPartial<FailureOptions>): Failure<E> =>
180
198
  new internal.FailureImpl(
181
199
  cause,
200
+ options?.timestamp ?? getCurrentTimestamp(),
182
201
  Option.fromNullable(options?.refreshing)
183
202
  )
184
203
 
@@ -196,6 +215,10 @@ export const fail: {
196
215
  export interface Success<A> extends Effect.Effect<never, never, A> {
197
216
  readonly _tag: typeof SUCCESS_TAG
198
217
  readonly value: A
218
+ /**
219
+ * @since 1.20.0
220
+ */
221
+ readonly timestamp: number // Date.now()
199
222
  readonly refreshing: Option.Option<Loading>
200
223
 
201
224
  readonly [Unify.typeSymbol]: unknown
@@ -207,6 +230,7 @@ export interface Success<A> extends Effect.Effect<never, never, A> {
207
230
  * @since 1.0.0
208
231
  */
209
232
  export type SuccessOptions = {
233
+ readonly timestamp: number // Date.now()
210
234
  readonly refreshing: Option.Option<Loading>
211
235
  }
212
236
 
@@ -219,6 +243,7 @@ export const success: {
219
243
  } = <A>(value: A, options?: OptionalPartial<SuccessOptions>): Success<A> =>
220
244
  new internal.SuccessImpl(
221
245
  value,
246
+ options?.timestamp ?? getCurrentTimestamp(),
222
247
  Option.fromNullable(options?.refreshing)
223
248
  )
224
249
 
@@ -400,6 +425,7 @@ const optionProgressEq = Option.getEquivalence(Progress.equals)
400
425
 
401
426
  const loadingEquivalence: Equivalence.Equivalence<Loading> = Equivalence.struct({
402
427
  _tag: Equivalence.string,
428
+ timestamp: Equivalence.number,
403
429
  progress: optionProgressEq
404
430
  })
405
431
 
@@ -408,6 +434,7 @@ const optionLoadingEq = Option.getEquivalence(loadingEquivalence)
408
434
  const failureEquivalence: Equivalence.Equivalence<Failure<any>> = Equivalence.struct({
409
435
  _tag: Equivalence.string,
410
436
  cause: Equal.equals,
437
+ timestamp: Equivalence.number,
411
438
  refreshing: optionLoadingEq
412
439
  })
413
440
 
@@ -415,6 +442,7 @@ const successEquivalence = <A>(valueEq: Equivalence.Equivalence<A>): Equivalence
415
442
  Equivalence.struct({
416
443
  _tag: Equivalence.string,
417
444
  value: valueEq,
445
+ timestamp: Equivalence.number,
418
446
  refreshing: optionLoadingEq
419
447
  })
420
448
 
@@ -432,3 +460,73 @@ export const getEquivalence =
432
460
  Success: (_, s1) => isSuccess(b) ? successEquivalence(valueEq)(s1, b) : false
433
461
  })
434
462
  }
463
+
464
+ /**
465
+ * @since 1.0.0
466
+ */
467
+ export function fromExit<E, A>(exit: Exit.Exit<E, A>): AsyncData<E, A> {
468
+ return Exit.match(exit, {
469
+ onFailure: (cause) => failCause(cause),
470
+ onSuccess: (value) => success(value)
471
+ })
472
+ }
473
+
474
+ /**
475
+ * @since 1.0.0
476
+ */
477
+ export function fromEither<E, A>(either: Either.Either<E, A>): AsyncData<E, A> {
478
+ return Either.match(either, {
479
+ onLeft: (e) => fail(e),
480
+ onRight: (a) => success(a)
481
+ })
482
+ }
483
+
484
+ const isAsyncDataFirst = (args: IArguments) => args.length === 3 || isAsyncData(args[0])
485
+
486
+ /**
487
+ * @since 1.0.0
488
+ */
489
+ export const isExpired: {
490
+ (ttl: Duration.DurationInput, now?: number): <E, A>(data: AsyncData<E, A>) => boolean
491
+ <E, A>(data: AsyncData<E, A>, ttl: Duration.DurationInput, now?: number): boolean
492
+ } = dual(isAsyncDataFirst, function isExpired<E, A>(
493
+ data: AsyncData<E, A>,
494
+ ttl: Duration.DurationInput,
495
+ now: number = getCurrentTimestamp()
496
+ ): boolean {
497
+ return match(data, {
498
+ NoData: () => true,
499
+ Loading: () => false,
500
+ Failure: (_, f) =>
501
+ Option.isNone(f.refreshing)
502
+ ? isPastTTL(f.timestamp, ttl, now)
503
+ : isPastTTL(f.refreshing.value.timestamp, ttl, now),
504
+ Success: (_, s) =>
505
+ Option.isNone(s.refreshing)
506
+ ? isPastTTL(s.timestamp, ttl, now) :
507
+ isPastTTL(s.refreshing.value.timestamp, ttl, now)
508
+ })
509
+ })
510
+
511
+ function isPastTTL(timestamp: number, ttl: Duration.DurationInput, now: number): boolean {
512
+ const millis = Duration.toMillis(ttl)
513
+
514
+ return now - timestamp > millis
515
+ }
516
+
517
+ /**
518
+ * Checks if two AsyncData are equal, disregarding the timestamps associated with them. Useful for testing
519
+ * without needing to manage timestamps.
520
+ *
521
+ * @since 1.0.0
522
+ */
523
+ export function dataEqual<E, A>(first: AsyncData<E, A>, second: AsyncData<E, A>): boolean {
524
+ return match(first, {
525
+ NoData: () => isNoData(second),
526
+ Loading: (l) => isLoading(second) && Equal.equals(l.progress, second.progress),
527
+ Failure: (_, f1) =>
528
+ isFailure(second) && Equal.equals(f1.cause, second.cause) && Equal.equals(f1.refreshing, second.refreshing),
529
+ Success: (_, s1) =>
530
+ isSuccess(second) && Equal.equals(s1.value, second.value) && Equal.equals(s1.refreshing, second.refreshing)
531
+ })
532
+ }
package/src/Progress.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  * @since 1.0.0
3
3
  */
4
4
 
5
- import { Equivalence, Option } from "effect"
5
+ import * as Equivalence from "effect/Equivalence"
6
6
  import { dual } from "effect/Function"
7
+ import * as Option from "effect/Option"
7
8
 
8
9
  /**
9
10
  * @since 1.0.0
package/src/Schema.ts CHANGED
@@ -8,86 +8,10 @@ import * as ParseResult from "@effect/schema/ParseResult"
8
8
  import * as Pretty from "@effect/schema/Pretty"
9
9
  import * as Schema from "@effect/schema/Schema"
10
10
  import * as AsyncData from "@typed/async-data/AsyncData"
11
- import { Cause, Chunk, Effect, FiberId, HashSet } from "effect"
11
+ import type * as Cause from "effect/Cause"
12
+ import * as Effect from "effect/Effect"
12
13
  import * as Option from "effect/Option"
13
14
 
14
- const fiberIdArbitrary: Arbitrary.Arbitrary<FiberId.FiberId> = (fc) =>
15
- fc.oneof(
16
- fc.constant(FiberId.none),
17
- fc.integer().chain((i) => fc.date().map((date) => FiberId.make(i, date.getTime() / 1000)))
18
- )
19
-
20
- const causeFromItems = <A>(
21
- items: Array<A>,
22
- join: (first: Cause.Cause<A>, second: Cause.Cause<A>) => Cause.Cause<A>
23
- ) => {
24
- if (items.length === 0) return Cause.empty
25
- if (items.length === 1) return Cause.fail(items[0])
26
- return items.map(Cause.fail).reduce(join)
27
- }
28
-
29
- const causeArbitrary = <A>(item: Arbitrary.Arbitrary<A>): Arbitrary.Arbitrary<Cause.Cause<A>> => (fc) =>
30
- fc.oneof(
31
- fc.constant(Cause.empty),
32
- fc.anything().map(Cause.die),
33
- fiberIdArbitrary(fc).map((id) => Cause.interrupt(id)),
34
- fc.array(item(fc)).chain((items) =>
35
- fc.integer({ min: 0, max: 1 }).map((i) => causeFromItems(items, i > 0.5 ? Cause.sequential : Cause.parallel))
36
- )
37
- )
38
-
39
- const causePretty = <A>(): Pretty.Pretty<Cause.Cause<A>> => Cause.pretty
40
-
41
- /**
42
- * @since 1.0.0
43
- */
44
- export const cause = <EI, E>(error: Schema.Schema<EI, E>): Schema.Schema<Cause.Cause<EI>, Cause.Cause<E>> => {
45
- const parseE = Schema.parse(Schema.chunkFromSelf(error))
46
-
47
- const self: Schema.Schema<Cause.Cause<EI>, Cause.Cause<E>> = Schema.lazy(() =>
48
- Schema.declare(
49
- [error],
50
- Schema.struct({}),
51
- () => (input, options) =>
52
- Effect.gen(function*(_) {
53
- if (!Cause.isCause(input)) return yield* _(ParseResult.failure(ParseResult.unexpected(input)))
54
-
55
- let output: Cause.Cause<E> = Cause.empty
56
- for (const cause of Cause.linearize<E>(input)) {
57
- const parrallelCauses = Cause.linearize(cause)
58
-
59
- if (HashSet.size(parrallelCauses) === 1) {
60
- const failures = Cause.failures(cause)
61
-
62
- output = Cause.parallel(
63
- output,
64
- Chunk.isEmpty(failures) ? cause : Chunk.reduce(
65
- yield* _(parseE(failures, options)),
66
- Cause.empty as Cause.Cause<E>,
67
- (cause, e) => Cause.sequential(cause, Cause.fail(e))
68
- )
69
- )
70
- } else {
71
- output = Cause.parallel(
72
- output,
73
- yield* _(Schema.parse(self)(cause, options))
74
- )
75
- }
76
- }
77
-
78
- return output
79
- }),
80
- {
81
- [AST.IdentifierAnnotationId]: "Cause",
82
- [Arbitrary.ArbitraryHookId]: causePretty,
83
- [Pretty.PrettyHookId]: causeArbitrary
84
- }
85
- )
86
- )
87
-
88
- return self
89
- }
90
-
91
15
  const asyncDataPretty = <E, A>(
92
16
  prettyCause: Pretty.Pretty<Cause.Cause<E>>,
93
17
  prettyValue: Pretty.Pretty<A>
@@ -121,7 +45,7 @@ export const asyncData = <EI, E, AI, A>(
121
45
  value: Schema.Schema<AI, A>
122
46
  ): Schema.Schema<AsyncData.AsyncData<EI, AI>, AsyncData.AsyncData<E, A>> => {
123
47
  return Schema.declare(
124
- [cause(error), value],
48
+ [Schema.cause(error), value],
125
49
  Schema.struct({}),
126
50
  (_, ...params) => {
127
51
  const [causeSchema, valueSchema] = params as readonly [
@@ -133,7 +57,7 @@ export const asyncData = <EI, E, AI, A>(
133
57
 
134
58
  return (input, options) => {
135
59
  return Effect.gen(function*(_) {
136
- if (!AsyncData.isAsyncData<EI, AI>(input)) return yield* _(ParseResult.failure(ParseResult.unexpected(input)))
60
+ if (!AsyncData.isAsyncData<EI, AI>(input)) return yield* _(ParseResult.fail(ParseResult.forbidden(input)))
137
61
 
138
62
  switch (input._tag) {
139
63
  case "NoData":
@@ -1,7 +1,13 @@
1
1
  // Internal
2
2
 
3
- import { Cause, Effect, Effectable, Equal, Hash, Option, pipe, Unify } from "effect"
4
- import { constant } from "effect/Function"
3
+ import * as Cause from "effect/Cause"
4
+ import * as Effect from "effect/Effect"
5
+ import * as Effectable from "effect/Effectable"
6
+ import * as Equal from "effect/Equal"
7
+ import { constant, pipe } from "effect/Function"
8
+ import * as Hash from "effect/Hash"
9
+ import * as Option from "effect/Option"
10
+ import * as Unify from "effect/Unify"
5
11
  import { type AsyncData, type Failure, type Loading, type Success } from "../AsyncData.js"
6
12
  import { FAILURE_TAG, LOADING_TAG, NO_DATA_TAG, SUCCESS_TAG } from "./tag.js"
7
13
 
@@ -14,7 +20,7 @@ export class FailureImpl<E> extends Effectable.Class<never, E, never> implements
14
20
  [Unify.unifySymbol]!: AsyncData.Unify<this>;
15
21
  [Unify.ignoreSymbol]!: AsyncData.IgnoreList
16
22
 
17
- constructor(readonly cause: Cause.Cause<E>, readonly refreshing: Option.Option<Loading>) {
23
+ constructor(readonly cause: Cause.Cause<E>, readonly timestamp: number, readonly refreshing: Option.Option<Loading>) {
18
24
  super()
19
25
 
20
26
  this.commit = constant(Effect.failCause(cause))
@@ -23,6 +29,7 @@ export class FailureImpl<E> extends Effectable.Class<never, E, never> implements
23
29
  [Equal.symbol] = (that: unknown) => {
24
30
  return isAsyncData(that) && that._tag === "Failure"
25
31
  && Equal.equals(this.cause, that.cause)
32
+ && Equal.equals(this.timestamp, that.timestamp)
26
33
  && Equal.equals(this.refreshing, that.refreshing)
27
34
  };
28
35
 
@@ -44,7 +51,7 @@ export class SuccessImpl<A> extends Effectable.Class<never, never, A> implements
44
51
  [Unify.unifySymbol]!: AsyncData.Unify<this>;
45
52
  [Unify.ignoreSymbol]!: AsyncData.IgnoreList
46
53
 
47
- constructor(readonly value: A, readonly refreshing: Option.Option<Loading>) {
54
+ constructor(readonly value: A, readonly timestamp: number, readonly refreshing: Option.Option<Loading>) {
48
55
  super()
49
56
 
50
57
  this.commit = constant(Effect.succeed(value))
@@ -53,6 +60,7 @@ export class SuccessImpl<A> extends Effectable.Class<never, never, A> implements
53
60
  [Equal.symbol] = (that: unknown) => {
54
61
  return isAsyncData(that) && that._tag === "Success"
55
62
  && Equal.equals(this.value, that.value)
63
+ && Equal.equals(this.timestamp, that.timestamp)
56
64
  && Equal.equals(this.refreshing, that.refreshing)
57
65
  };
58
66