@typed/async-data 0.3.0 → 0.3.2

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/Schema.ts CHANGED
@@ -4,60 +4,433 @@
4
4
 
5
5
  import * as Arbitrary from "@effect/schema/Arbitrary"
6
6
  import * as AST from "@effect/schema/AST"
7
+ import * as Eq from "@effect/schema/Equivalence"
7
8
  import * as ParseResult from "@effect/schema/ParseResult"
8
9
  import * as Pretty from "@effect/schema/Pretty"
9
10
  import * as Schema from "@effect/schema/Schema"
10
11
  import * as AsyncData from "@typed/async-data/AsyncData"
12
+ import { Equal } from "effect"
11
13
  import type * as Cause from "effect/Cause"
12
14
  import * as Effect from "effect/Effect"
13
15
  import * as Option from "effect/Option"
16
+ import * as P from "./Progress.js"
14
17
 
15
- const asyncDataPretty = <E, A>(
16
- prettyCause: Pretty.Pretty<Cause.Cause<E>>,
17
- prettyValue: Pretty.Pretty<A>
18
- ): Pretty.Pretty<AsyncData.AsyncData<E, A>> =>
19
- AsyncData.match({
20
- NoData: () => `AsyncData.NoData`,
21
- Loading: () => `AsyncData.Loading`,
22
- Failure: (cause, data) =>
23
- `AsyncData.Failure(refreshing=${Option.isSome(data.refreshing)}, cause=${prettyCause(cause)})`,
24
- Success: (value, data) =>
25
- `AsyncData.Success(refreshing=${Option.isSome(data.refreshing)}, value=${prettyValue(value)})`
18
+ const NO_DATA_PRETTY = "AsyncData.NoData"
19
+ const LOADING_PRETTY = (loading: AsyncData.Loading) =>
20
+ Option.match(loading.progress, {
21
+ onNone: () => `AsyncData.Loading(timestamp=${loading.timestamp})`,
22
+ onSome: (progress) => `AsyncData.Loading(timestamp=${loading.timestamp}, progress=${P.pretty(progress)})`
26
23
  })
24
+ const FAILURE_PRETTY = <E>(print: Pretty.Pretty<Cause.Cause<E>>) => (failure: AsyncData.Failure<E>) =>
25
+ Option.match(failure.refreshing, {
26
+ onNone: () => `AsyncData.Failure(timestamp=${failure.timestamp}, cause=${print(failure.cause)})`,
27
+ onSome: (loading) =>
28
+ `AsyncData.Failure(timestamp=${failure.timestamp}, refreshing=${LOADING_PRETTY(loading)}, cause=${
29
+ print(failure.cause)
30
+ })`
31
+ })
32
+ const SUCCESS_PRETTY = <A>(print: Pretty.Pretty<A>) => (success: AsyncData.Success<A>) =>
33
+ Option.match(success.refreshing, {
34
+ onNone: () => `AsyncData.Success(timestamp=${success.timestamp}, value=${print(success.value)})`,
35
+ onSome: (loading) =>
36
+ `AsyncData.Success(timestamp=${success.timestamp}, refreshing=${LOADING_PRETTY(loading)}, value=${
37
+ print(success.value)
38
+ })`
39
+ })
40
+
41
+ const NoDataSchemaJson = Schema.struct({
42
+ _tag: Schema.literal("NoData")
43
+ })
44
+ const NoDataSchema = Schema.instanceOf(AsyncData.NoData)
45
+
46
+ /**
47
+ * @since 1.0.0
48
+ */
49
+ export const NoData: Schema.Schema<{ readonly _tag: "NoData" }, AsyncData.NoData> = NoDataSchemaJson
50
+ .pipe(
51
+ Schema.transform(
52
+ NoDataSchema,
53
+ (): AsyncData.NoData => AsyncData.noData(),
54
+ (): Schema.Schema.To<typeof NoDataSchemaJson> => ({
55
+ _tag: "NoData"
56
+ })
57
+ ),
58
+ Schema.annotations({
59
+ [AST.IdentifierAnnotationId]: NO_DATA_PRETTY,
60
+ [Pretty.PrettyHookId]: () => NO_DATA_PRETTY,
61
+ [Arbitrary.ArbitraryHookId]: (): Arbitrary.Arbitrary<AsyncData.NoData> => (fc) => fc.constant(AsyncData.noData()),
62
+ [Eq.EquivalenceHookId]: () => Equal.equals
63
+ })
64
+ )
65
+
66
+ /**
67
+ * @since 1.0.0
68
+ */
69
+ export type NoDataFrom = Schema.Schema.From<typeof NoData>
70
+
71
+ const ProgressSchemaJson = Schema.struct({
72
+ loaded: Schema.bigint,
73
+ total: Schema.optional(Schema.bigint)
74
+ })
75
+ type ProgressFromTo = Schema.Schema.To<typeof ProgressSchemaJson>
76
+
77
+ const ProgressSchema: Schema.Schema<
78
+ {
79
+ readonly loaded: bigint
80
+ readonly total: Option.Option<bigint>
81
+ },
82
+ P.Progress
83
+ > = Schema.data(Schema.struct({
84
+ loaded: Schema.bigintFromSelf,
85
+ total: Schema.optionFromSelf(Schema.bigintFromSelf)
86
+ }))
87
+
88
+ const progressArbitrary: Arbitrary.Arbitrary<P.Progress> = (fc) =>
89
+ fc.bigInt().chain((loaded) => fc.option(fc.bigInt({ min: loaded })).map((total) => P.make(loaded, total)))
90
+
91
+ /**
92
+ * @since 1.0.0
93
+ */
94
+ export const Progress: Schema.Schema<{ readonly loaded: string; readonly total?: string | undefined }, P.Progress> =
95
+ ProgressSchemaJson.pipe(
96
+ Schema.transform(
97
+ ProgressSchema,
98
+ (json): P.Progress => P.make(json.loaded, json.total),
99
+ (progress) => ({
100
+ loaded: progress.loaded,
101
+ total: Option.getOrUndefined(progress.total)
102
+ })
103
+ ),
104
+ Schema.annotations({
105
+ [AST.IdentifierAnnotationId]: "Progress",
106
+ [Pretty.PrettyHookId]: () => "Progress",
107
+ [Arbitrary.ArbitraryHookId]: (): Arbitrary.Arbitrary<P.Progress> => progressArbitrary,
108
+ [Eq.EquivalenceHookId]: () => Equal.equals
109
+ })
110
+ )
111
+
112
+ /**
113
+ * @since 1.0.0
114
+ */
115
+ export type ProgressFrom = Schema.Schema.From<typeof Progress>
116
+
117
+ const LoadingSchemaJson = Schema.struct({
118
+ _tag: Schema.literal("Loading"),
119
+ timestamp: Schema.number,
120
+ progress: Schema.optional(ProgressSchemaJson)
121
+ })
122
+ const LoadingSchema = Schema.instanceOf(AsyncData.Loading)
123
+
124
+ const loadingFromJson = (json: Schema.Schema.To<typeof LoadingSchemaJson>): AsyncData.Loading =>
125
+ AsyncData.loading({ timestamp: json.timestamp, progress: progressFromJson(json.progress) })
126
+
127
+ const loadingToJson = (loading: AsyncData.Loading): Schema.Schema.To<typeof LoadingSchemaJson> => ({
128
+ _tag: "Loading",
129
+ timestamp: loading.timestamp,
130
+ progress: progressToJson(loading.progress)
131
+ })
132
+
133
+ const loadingArbitrary: Arbitrary.Arbitrary<AsyncData.Loading> = (fc) =>
134
+ fc.option(progressArbitrary(fc)).map((progress) =>
135
+ AsyncData.loading({
136
+ timestamp: Date.now(),
137
+ progress: progress || undefined
138
+ })
139
+ )
140
+
141
+ /**
142
+ * @since 1.0.0
143
+ */
144
+ export const Loading: Schema.Schema<
145
+ {
146
+ readonly _tag: "Loading"
147
+ readonly timestamp: number
148
+ readonly progress?: ProgressFrom | undefined
149
+ },
150
+ AsyncData.Loading
151
+ > = LoadingSchemaJson
152
+ .pipe(
153
+ Schema.transform(
154
+ LoadingSchema,
155
+ loadingFromJson,
156
+ loadingToJson
157
+ ),
158
+ Schema.annotations({
159
+ [AST.IdentifierAnnotationId]: "AsyncData.Loading",
160
+ [Pretty.PrettyHookId]: () => LOADING_PRETTY,
161
+ [Arbitrary.ArbitraryHookId]: () => loadingArbitrary,
162
+ [Eq.EquivalenceHookId]: () => Equal.equals
163
+ })
164
+ )
165
+
166
+ /**
167
+ * @since 1.0.0
168
+ */
169
+ export type LoadingFrom = Schema.Schema.From<typeof Loading>
170
+
171
+ const FailureSchemaJson = <EI, E>(cause: Schema.Schema<Schema.CauseFrom<EI>, Cause.Cause<E>>) =>
172
+ Schema.struct({
173
+ _tag: Schema.literal("Failure"),
174
+ cause,
175
+ timestamp: Schema.number,
176
+ refreshing: Schema.optional(LoadingSchemaJson)
177
+ })
178
+
179
+ const FailureSchema = <EI, E>(
180
+ error: Schema.Schema<EI, E>
181
+ ): Schema.Schema<AsyncData.Failure<EI>, AsyncData.Failure<E>> =>
182
+ Schema.declare(
183
+ [Schema.cause(error)],
184
+ Schema.struct({}),
185
+ (isDecoding, causeSchema) => {
186
+ const parseCause = isDecoding ? Schema.parse(causeSchema) : Schema.encode(causeSchema)
187
+
188
+ return (input, options) => {
189
+ return Effect.gen(function*(_) {
190
+ if (!AsyncData.isAsyncData(input)) return yield* _(ParseResult.fail(ParseResult.forbidden(input)))
27
191
 
28
- const asyncDataArbitrary = <E, A>(
29
- causeArbitrary: Arbitrary.Arbitrary<Cause.Cause<E>>,
30
- valueArbitrary: Arbitrary.Arbitrary<A>
31
- ): Arbitrary.Arbitrary<AsyncData.AsyncData<E, A>> =>
192
+ switch (input._tag) {
193
+ case "Failure": {
194
+ const cause = yield* _(parseCause(input.cause, options))
195
+
196
+ return AsyncData.failCause(cause, {
197
+ timestamp: input.timestamp,
198
+ refreshing: Option.getOrUndefined(input.refreshing)
199
+ })
200
+ }
201
+ default:
202
+ return yield* _(ParseResult.fail(ParseResult.forbidden(input)))
203
+ }
204
+ })
205
+ }
206
+ }
207
+ )
208
+
209
+ const failureArbitrary = <E>(
210
+ cause: Arbitrary.Arbitrary<Cause.Cause<E>>
211
+ ): Arbitrary.Arbitrary<AsyncData.Failure<E>> =>
32
212
  (fc) =>
33
- fc.oneof(
34
- fc.constant(AsyncData.noData()),
35
- fc.constant(AsyncData.loading()),
36
- causeArbitrary(fc).map((cause) => AsyncData.failCause(cause)),
37
- valueArbitrary(fc).map((a) => AsyncData.success(a))
213
+ fc.option(loadingArbitrary(fc)).chain((refreshing) =>
214
+ cause(fc).chain((cause) =>
215
+ fc.date().map((date) =>
216
+ AsyncData.failCause(cause, {
217
+ timestamp: date.getTime(),
218
+ refreshing: refreshing || undefined
219
+ })
220
+ )
221
+ )
38
222
  )
39
223
 
224
+ /**
225
+ * @since 1.0.0
226
+ */
227
+ export const Failure = <EI, E>(
228
+ error: Schema.Schema<EI, E>
229
+ ): Schema.Schema<
230
+ {
231
+ readonly _tag: "Failure"
232
+ readonly cause: Schema.CauseFrom<EI>
233
+ readonly timestamp: number
234
+ readonly refreshing?: {
235
+ readonly _tag: "Loading"
236
+ readonly timestamp: number
237
+ readonly progress?: { readonly loaded: string; readonly total?: string | undefined } | undefined
238
+ } | undefined
239
+ },
240
+ AsyncData.Failure<E>
241
+ > =>
242
+ FailureSchemaJson(Schema.cause(Schema.from(error)))
243
+ .pipe(
244
+ Schema.transform(
245
+ FailureSchema(error),
246
+ (json): AsyncData.Failure<EI> =>
247
+ AsyncData.failCause(json.cause, {
248
+ timestamp: json.timestamp,
249
+ refreshing: json.refreshing ? loadingFromJson(json.refreshing) : undefined
250
+ }),
251
+ (failure) => ({
252
+ _tag: "Failure",
253
+ cause: failure.cause,
254
+ timestamp: failure.timestamp,
255
+ refreshing: Option.getOrUndefined(Option.map(failure.refreshing, loadingToJson))
256
+ } as const)
257
+ ),
258
+ Schema.annotations({
259
+ [AST.IdentifierAnnotationId]: "AsyncData.Failure",
260
+ [Pretty.PrettyHookId]: FAILURE_PRETTY,
261
+ [Arbitrary.ArbitraryHookId]: failureArbitrary,
262
+ [Eq.EquivalenceHookId]: () => Equal.equals
263
+ })
264
+ )
265
+
266
+ /**
267
+ * @since 1.0.0
268
+ */
269
+ export type FailureFrom<E> = Schema.Schema.From<ReturnType<typeof FailureSchemaJson<E, E>>>
270
+
271
+ const SuccessSchemaJson = <AI, A>(
272
+ value: Schema.Schema<AI, A>
273
+ ): Schema.Schema<
274
+ {
275
+ readonly timestamp: number
276
+ readonly _tag: "Success"
277
+ readonly value: AI
278
+ readonly refreshing?: {
279
+ readonly timestamp: number
280
+ readonly _tag: "Loading"
281
+ readonly progress?: { readonly loaded: string; readonly total?: string | undefined } | undefined
282
+ } | undefined
283
+ },
284
+ {
285
+ readonly timestamp: number
286
+ readonly _tag: "Success"
287
+ readonly value: A
288
+ readonly refreshing?: {
289
+ readonly timestamp: number
290
+ readonly _tag: "Loading"
291
+ readonly progress?: { readonly loaded: bigint; readonly total?: bigint | undefined } | undefined
292
+ } | undefined
293
+ }
294
+ > =>
295
+ Schema.struct({
296
+ _tag: Schema.literal("Success"),
297
+ value,
298
+ timestamp: Schema.number,
299
+ refreshing: Schema.optional(LoadingSchemaJson)
300
+ })
301
+
302
+ const SuccessSchema = <AI, A>(
303
+ value: Schema.Schema<AI, A>
304
+ ): Schema.Schema<AsyncData.Success<AI>, AsyncData.Success<A>> =>
305
+ Schema.declare(
306
+ [value],
307
+ Schema.struct({
308
+ _tag: Schema.literal("Success"),
309
+ timestamp: Schema.number
310
+ }),
311
+ (isDecoding, valueSchema) => {
312
+ const parseValue = isDecoding ? Schema.parse(valueSchema) : Schema.encode(valueSchema)
313
+
314
+ return (input, options) => {
315
+ return Effect.gen(function*(_) {
316
+ if (!AsyncData.isAsyncData(input)) return yield* _(ParseResult.fail(ParseResult.forbidden(input)))
317
+
318
+ switch (input._tag) {
319
+ case "Success": {
320
+ const value = yield* _(parseValue(input.value, options))
321
+
322
+ return AsyncData.success(value, {
323
+ timestamp: input.timestamp,
324
+ refreshing: Option.getOrUndefined(input.refreshing)
325
+ })
326
+ }
327
+ default:
328
+ return yield* _(ParseResult.fail(ParseResult.forbidden(input)))
329
+ }
330
+ })
331
+ }
332
+ }
333
+ )
334
+
335
+ const successArbitrary = <A>(
336
+ value: Arbitrary.Arbitrary<A>
337
+ ): Arbitrary.Arbitrary<AsyncData.Success<A>> =>
338
+ (fc) =>
339
+ fc.option(loadingArbitrary(fc)).chain((refreshing) =>
340
+ value(fc).chain((a) =>
341
+ fc.date().map((date) =>
342
+ AsyncData.success(a, {
343
+ timestamp: date.getTime(),
344
+ refreshing: refreshing || undefined
345
+ })
346
+ )
347
+ )
348
+ )
349
+
350
+ /**
351
+ * @since 1.0.0
352
+ */
353
+ export const Success = <AI, A>(
354
+ value: Schema.Schema<AI, A>
355
+ ): Schema.Schema<
356
+ {
357
+ readonly timestamp: number
358
+ readonly _tag: "Success"
359
+ readonly value: AI
360
+ readonly refreshing?: LoadingFrom | undefined
361
+ },
362
+ AsyncData.Success<A>
363
+ > =>
364
+ SuccessSchemaJson(Schema.from(value))
365
+ .pipe(
366
+ Schema.transform(
367
+ SuccessSchema(value),
368
+ (json): AsyncData.Success<AI> =>
369
+ AsyncData.success(json.value, {
370
+ timestamp: json.timestamp,
371
+ refreshing: json.refreshing ? loadingFromJson(json.refreshing) : undefined
372
+ }),
373
+ (success) => ({
374
+ _tag: "Success",
375
+ value: success.value,
376
+ timestamp: success.timestamp,
377
+ refreshing: Option.getOrUndefined(Option.map(success.refreshing, loadingToJson))
378
+ } as const)
379
+ ),
380
+ Schema.annotations({
381
+ [AST.IdentifierAnnotationId]: "AsyncData.Success",
382
+ [Pretty.PrettyHookId]: SUCCESS_PRETTY,
383
+ [Arbitrary.ArbitraryHookId]: successArbitrary,
384
+ [Eq.EquivalenceHookId]: () => Equal.equals
385
+ })
386
+ )
387
+
388
+ /**
389
+ * @since 1.0.0
390
+ */
391
+ export type SuccessFrom<A> = Schema.Schema.From<ReturnType<typeof SuccessSchemaJson<A, A>>>
392
+
393
+ /**
394
+ * @since 1.0.0
395
+ */
396
+ export type AsyncDataFrom<E, A> = NoDataFrom | LoadingFrom | FailureFrom<E> | SuccessFrom<A>
397
+
40
398
  /**
41
399
  * @since 1.0.0
42
400
  */
43
401
  export const asyncData = <EI, E, AI, A>(
44
402
  error: Schema.Schema<EI, E>,
45
403
  value: Schema.Schema<AI, A>
404
+ ): Schema.Schema<AsyncDataFrom<EI, AI>, AsyncData.AsyncData<E, A>> => {
405
+ return Schema.union(
406
+ NoData,
407
+ Loading,
408
+ Failure(error),
409
+ Success(value)
410
+ )
411
+ }
412
+
413
+ /**
414
+ * @since 1.0.0
415
+ */
416
+ export const asyncDataFromSelf = <EI, E, AI, A>(
417
+ error: Schema.Schema<EI, E>,
418
+ value: Schema.Schema<AI, A>
46
419
  ): Schema.Schema<AsyncData.AsyncData<EI, AI>, AsyncData.AsyncData<E, A>> => {
47
420
  return Schema.declare(
48
421
  [Schema.cause(error), value],
49
422
  Schema.struct({}),
50
- (_, ...params) => {
423
+ (isDecoding, ...params) => {
51
424
  const [causeSchema, valueSchema] = params as readonly [
52
- Schema.Schema<Cause.Cause<EI>, Cause.Cause<E>>,
53
- Schema.Schema<AI, A>
425
+ Schema.Schema<Cause.Cause<any>, Cause.Cause<any>>,
426
+ Schema.Schema<any, any>
54
427
  ]
55
- const parseCause = Schema.parse(causeSchema)
56
- const parseValue = Schema.parse(valueSchema)
428
+ const parseCause = isDecoding ? Schema.parse(causeSchema) : Schema.encode(causeSchema)
429
+ const parseValue = isDecoding ? Schema.parse(valueSchema) : Schema.encode(valueSchema)
57
430
 
58
431
  return (input, options) => {
59
432
  return Effect.gen(function*(_) {
60
- if (!AsyncData.isAsyncData<EI, AI>(input)) return yield* _(ParseResult.fail(ParseResult.forbidden(input)))
433
+ if (!AsyncData.isAsyncData(input)) return yield* _(ParseResult.fail(ParseResult.forbidden(input)))
61
434
 
62
435
  switch (input._tag) {
63
436
  case "NoData":
@@ -67,6 +440,7 @@ export const asyncData = <EI, E, AI, A>(
67
440
  const cause = yield* _(parseCause(input.cause, options))
68
441
 
69
442
  return AsyncData.failCause(cause, {
443
+ timestamp: input.timestamp,
70
444
  refreshing: Option.getOrUndefined(input.refreshing)
71
445
  })
72
446
  }
@@ -74,6 +448,7 @@ export const asyncData = <EI, E, AI, A>(
74
448
  const a = yield* _(parseValue(input.value, options))
75
449
 
76
450
  return AsyncData.success(a, {
451
+ timestamp: input.timestamp,
77
452
  refreshing: Option.getOrUndefined(input.refreshing)
78
453
  })
79
454
  }
@@ -83,8 +458,52 @@ export const asyncData = <EI, E, AI, A>(
83
458
  },
84
459
  {
85
460
  [AST.IdentifierAnnotationId]: "AsyncData",
86
- [Arbitrary.ArbitraryHookId]: asyncDataPretty,
87
- [Pretty.PrettyHookId]: asyncDataArbitrary
461
+ [Pretty.PrettyHookId]: asyncDataPretty,
462
+ [Arbitrary.ArbitraryHookId]: asyncDataArbitrary,
463
+ [Eq.EquivalenceHookId]: () => Equal.equals
88
464
  }
89
465
  )
90
466
  }
467
+
468
+ function asyncDataPretty<E, A>(
469
+ E: Pretty.Pretty<Cause.Cause<E>>,
470
+ A: Pretty.Pretty<A>
471
+ ): Pretty.Pretty<AsyncData.AsyncData<E, A>> {
472
+ return AsyncData.match({
473
+ NoData: () => NO_DATA_PRETTY,
474
+ Loading: LOADING_PRETTY,
475
+ Failure: (_, data) => FAILURE_PRETTY(E)(data),
476
+ Success: (_, data) => SUCCESS_PRETTY(A)(data)
477
+ })
478
+ }
479
+
480
+ function asyncDataArbitrary<E, A>(
481
+ E: Arbitrary.Arbitrary<Cause.Cause<E>>,
482
+ A: Arbitrary.Arbitrary<A>
483
+ ): Arbitrary.Arbitrary<AsyncData.AsyncData<E, A>> {
484
+ const noDataArb = Arbitrary.make(NoData)
485
+ const loadingArb = Arbitrary.make(Loading)
486
+ const failureArb = failureArbitrary(E)
487
+ const successArb = successArbitrary(A)
488
+
489
+ return (fc) =>
490
+ fc.oneof(
491
+ noDataArb(fc),
492
+ loadingArb(fc),
493
+ failureArb(fc),
494
+ successArb(fc)
495
+ )
496
+ }
497
+
498
+ function progressFromJson(json: ProgressFromTo | undefined): P.Progress | undefined {
499
+ if (json === undefined) return
500
+ return P.make(json.loaded, json.total)
501
+ }
502
+
503
+ function progressToJson(progres: Option.Option<P.Progress>): ProgressFromTo | undefined {
504
+ if (Option.isNone(progres)) return
505
+ return {
506
+ loaded: progres.value.loaded,
507
+ total: Option.getOrUndefined(progres.value.total)
508
+ }
509
+ }