@typed/async-data 1.0.0-beta.0 → 1.0.0-beta.10
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/LICENSE +21 -0
- package/README.md +32 -23
- package/dist/index.d.ts +664 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +501 -35
- package/package.json +25 -12
- package/src/__tests__/assert.type-test.ts +4 -0
- package/src/__tests__/flat-map.type-test.ts +29 -0
- package/src/index.ts +807 -41
- package/src/AsyncData.test.ts +0 -444
- package/src/index.test.ts +0 -497
- package/tsconfig.json +0 -6
package/src/index.ts
CHANGED
|
@@ -7,52 +7,358 @@ import * as Result from "effect/Result";
|
|
|
7
7
|
import * as Schema from "effect/Schema";
|
|
8
8
|
import type { Unify } from "effect/Unify";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Reports completed work and, when known, the total amount of work.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* ## Why
|
|
15
|
+
* Progress belongs to the value so loading and refreshing states can carry the same transportable measurement.
|
|
16
|
+
*
|
|
17
|
+
* ## Ownership and lifetime
|
|
18
|
+
* This plain object acquires no resources. Its `readonly` fields are a TypeScript constraint; the runtime value is not frozen.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import type { Progress } from "@typed/async-data"
|
|
23
|
+
* const progress: Progress = { loaded: 4, total: 10 }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @category State models
|
|
27
|
+
* @since 1.0.0
|
|
28
|
+
*/
|
|
10
29
|
export interface Progress {
|
|
30
|
+
/**
|
|
31
|
+
* The finite amount of work completed so far.
|
|
32
|
+
* @remarks
|
|
33
|
+
* ## Why
|
|
34
|
+
* A required counter lets consumers render useful progress even when the total is unknown.
|
|
35
|
+
* ## Ownership and lifetime
|
|
36
|
+
* Inherits the resource-free lifetime of its enclosing `Progress` value.
|
|
37
|
+
* @category State models
|
|
38
|
+
* @since 1.0.0
|
|
39
|
+
*/
|
|
11
40
|
readonly loaded: number;
|
|
41
|
+
/**
|
|
42
|
+
* The finite total amount of work, when the producer knows it.
|
|
43
|
+
* @remarks
|
|
44
|
+
* ## Why
|
|
45
|
+
* Optionality distinguishes indeterminate work from a known total without inventing a sentinel value.
|
|
46
|
+
* ## Ownership and lifetime
|
|
47
|
+
* Inherits the resource-free lifetime of its enclosing `Progress` value.
|
|
48
|
+
* @category State models
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
*/
|
|
12
51
|
readonly total?: number | undefined;
|
|
13
52
|
}
|
|
14
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Represents an asynchronous value before work or a result exists.
|
|
56
|
+
* @remarks
|
|
57
|
+
* ## Why
|
|
58
|
+
* A distinct initial state prevents absence from being confused with loading, failure, or a successful `undefined` value.
|
|
59
|
+
* ## Ownership and lifetime
|
|
60
|
+
* This shape acquires no resources. `readonly` is compile-time only; values matching the interface need not be frozen.
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { NoData } from "@typed/async-data"
|
|
64
|
+
* const initial = NoData
|
|
65
|
+
* ```
|
|
66
|
+
* @category State models
|
|
67
|
+
* @since 1.0.0
|
|
68
|
+
*/
|
|
15
69
|
export interface NoData {
|
|
70
|
+
/**
|
|
71
|
+
* The discriminant for the initial state.
|
|
72
|
+
* @remarks
|
|
73
|
+
* ## Why
|
|
74
|
+
* A literal tag enables exhaustive matching without runtime class identity.
|
|
75
|
+
* ## Ownership and lifetime
|
|
76
|
+
* Inherits the resource-free lifetime of its enclosing state.
|
|
77
|
+
* @category State models
|
|
78
|
+
* @since 1.0.0
|
|
79
|
+
*/
|
|
16
80
|
readonly _tag: "NoData";
|
|
17
81
|
}
|
|
18
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Represents active work that has not produced a prior value or failure.
|
|
85
|
+
* @remarks
|
|
86
|
+
* ## Why
|
|
87
|
+
* Loading is separate from refreshing so consumers can decide whether stale content remains available.
|
|
88
|
+
* ## Ownership and lifetime
|
|
89
|
+
* This plain object acquires no resources; the operation it describes is owned elsewhere and the object is not frozen.
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { loading } from "@typed/async-data"
|
|
93
|
+
* const state = loading({ loaded: 0 })
|
|
94
|
+
* ```
|
|
95
|
+
* @category State models
|
|
96
|
+
* @since 1.0.0
|
|
97
|
+
*/
|
|
19
98
|
export interface Loading {
|
|
99
|
+
/**
|
|
100
|
+
* The discriminant for loading without a prior result.
|
|
101
|
+
* @remarks
|
|
102
|
+
* ## Why
|
|
103
|
+
* The literal tag makes loading branches explicit and exhaustively checkable.
|
|
104
|
+
* ## Ownership and lifetime
|
|
105
|
+
* Inherits the resource-free lifetime of its enclosing state.
|
|
106
|
+
* @category State models
|
|
107
|
+
* @since 1.0.0
|
|
108
|
+
*/
|
|
20
109
|
readonly _tag: "Loading";
|
|
110
|
+
/**
|
|
111
|
+
* Optional progress reported by the active producer.
|
|
112
|
+
* @remarks
|
|
113
|
+
* ## Why
|
|
114
|
+
* Keeping progress optional supports both determinate and uninstrumented operations.
|
|
115
|
+
* ## Ownership and lifetime
|
|
116
|
+
* Inherits the resource-free lifetime of its enclosing state.
|
|
117
|
+
* @category State models
|
|
118
|
+
* @since 1.0.0
|
|
119
|
+
*/
|
|
21
120
|
readonly progress?: Progress | undefined;
|
|
22
121
|
}
|
|
23
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Represents a successfully produced value, optionally while it refreshes.
|
|
125
|
+
* @remarks
|
|
126
|
+
* ## Why
|
|
127
|
+
* Success keeps the last usable value available while progress can describe newer work.
|
|
128
|
+
* ## Ownership and lifetime
|
|
129
|
+
* This plain wrapper acquires no resources; ownership of `value` remains with the caller and `readonly` does not freeze either object.
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* import { success } from "@typed/async-data"
|
|
133
|
+
* const state = success({ id: 1 })
|
|
134
|
+
* ```
|
|
135
|
+
* @category State models
|
|
136
|
+
* @since 1.0.0
|
|
137
|
+
*/
|
|
24
138
|
export interface Success<A> {
|
|
139
|
+
/**
|
|
140
|
+
* The discriminant for a successful value.
|
|
141
|
+
* @remarks
|
|
142
|
+
* ## Why
|
|
143
|
+
* The literal tag enables exhaustive success handling without inspecting the payload.
|
|
144
|
+
* ## Ownership and lifetime
|
|
145
|
+
* Inherits the resource-free lifetime of its enclosing state.
|
|
146
|
+
* @category State models
|
|
147
|
+
* @since 1.0.0
|
|
148
|
+
*/
|
|
25
149
|
readonly _tag: "Success";
|
|
150
|
+
/**
|
|
151
|
+
* The most recently successful value.
|
|
152
|
+
* @remarks
|
|
153
|
+
* ## Why
|
|
154
|
+
* The payload remains directly accessible even when `progress` marks a refresh.
|
|
155
|
+
* ## Ownership and lifetime
|
|
156
|
+
* Inherits the enclosing state's lifetime; the caller retains ownership of the referenced value.
|
|
157
|
+
* @category State models
|
|
158
|
+
* @since 1.0.0
|
|
159
|
+
*/
|
|
26
160
|
readonly value: A;
|
|
161
|
+
/**
|
|
162
|
+
* Optional progress for a refresh of the successful value.
|
|
163
|
+
* @remarks
|
|
164
|
+
* ## Why
|
|
165
|
+
* Its presence distinguishes refreshing success from a settled success.
|
|
166
|
+
* ## Ownership and lifetime
|
|
167
|
+
* Inherits the resource-free lifetime of its enclosing state.
|
|
168
|
+
* @category State models
|
|
169
|
+
* @since 1.0.0
|
|
170
|
+
*/
|
|
27
171
|
readonly progress?: Progress | undefined;
|
|
28
172
|
}
|
|
29
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Represents an Effect failure, optionally while a retry refreshes it.
|
|
176
|
+
* @remarks
|
|
177
|
+
* ## Why
|
|
178
|
+
* Retaining the complete Effect `Cause` preserves typed errors, defects, and interruption instead of flattening failure detail.
|
|
179
|
+
* ## Ownership and lifetime
|
|
180
|
+
* This plain wrapper acquires no resources; its `Cause` is persistent, but the wrapper itself is not frozen.
|
|
181
|
+
* @example
|
|
182
|
+
* ```ts
|
|
183
|
+
* import { failure } from "@typed/async-data"
|
|
184
|
+
* import { Cause } from "effect"
|
|
185
|
+
* const state = failure(Cause.fail("offline"))
|
|
186
|
+
* ```
|
|
187
|
+
* See [Effect Cause](https://effect.website/docs/data-types/cause/).
|
|
188
|
+
* @category State models
|
|
189
|
+
* @since 1.0.0
|
|
190
|
+
*/
|
|
30
191
|
export interface Failure<E> {
|
|
192
|
+
/**
|
|
193
|
+
* The discriminant for a failed computation.
|
|
194
|
+
* @remarks
|
|
195
|
+
* ## Why
|
|
196
|
+
* The literal tag separates failure from absence and loading.
|
|
197
|
+
* ## Ownership and lifetime
|
|
198
|
+
* Inherits the resource-free lifetime of its enclosing state.
|
|
199
|
+
* @category State models
|
|
200
|
+
* @since 1.0.0
|
|
201
|
+
*/
|
|
31
202
|
readonly _tag: "Failure";
|
|
203
|
+
/**
|
|
204
|
+
* The complete Effect cause of failure.
|
|
205
|
+
* @remarks
|
|
206
|
+
* ## Why
|
|
207
|
+
* Cause preservation keeps defects and interruption observable alongside typed failures.
|
|
208
|
+
* ## Ownership and lifetime
|
|
209
|
+
* Inherits the enclosing state's lifetime; Effect Cause is persistent.
|
|
210
|
+
* @category State models
|
|
211
|
+
* @since 1.0.0
|
|
212
|
+
*/
|
|
32
213
|
readonly cause: Cause.Cause<E>;
|
|
214
|
+
/**
|
|
215
|
+
* Optional progress for a retry of the failed operation.
|
|
216
|
+
* @remarks
|
|
217
|
+
* ## Why
|
|
218
|
+
* Its presence distinguishes a refreshing failure from a settled failure.
|
|
219
|
+
* ## Ownership and lifetime
|
|
220
|
+
* Inherits the resource-free lifetime of its enclosing state.
|
|
221
|
+
* @category State models
|
|
222
|
+
* @since 1.0.0
|
|
223
|
+
*/
|
|
33
224
|
readonly progress?: Progress | undefined;
|
|
34
225
|
}
|
|
35
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Represents an optimistic value together with the exact state it replaced.
|
|
229
|
+
* @remarks
|
|
230
|
+
* ## Why
|
|
231
|
+
* Preserving history makes rollback and nested optimistic transformations explicit rather than hiding them in renderer state.
|
|
232
|
+
* ## Ownership and lifetime
|
|
233
|
+
* This plain wrapper acquires no resources and retains its previous state; `readonly` is compile-time only.
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* import { optimistic, success } from "@typed/async-data"
|
|
237
|
+
* const state = optimistic(success(1), 2)
|
|
238
|
+
* ```
|
|
239
|
+
* @category State models
|
|
240
|
+
* @since 1.0.0
|
|
241
|
+
*/
|
|
36
242
|
export interface Optimistic<A, E> {
|
|
243
|
+
/**
|
|
244
|
+
* The discriminant for optimistic state.
|
|
245
|
+
* @remarks
|
|
246
|
+
* ## Why
|
|
247
|
+
* The literal tag enables explicit optimistic handling and cycle-safe traversal.
|
|
248
|
+
* ## Ownership and lifetime
|
|
249
|
+
* Inherits the resource-free lifetime of its enclosing state.
|
|
250
|
+
* @category State models
|
|
251
|
+
* @since 1.0.0
|
|
252
|
+
*/
|
|
37
253
|
readonly _tag: "Optimistic";
|
|
254
|
+
/**
|
|
255
|
+
* The value currently presented optimistically.
|
|
256
|
+
* @remarks
|
|
257
|
+
* ## Why
|
|
258
|
+
* Separating the provisional value from history makes pending intent directly observable.
|
|
259
|
+
* ## Ownership and lifetime
|
|
260
|
+
* Inherits the enclosing state's lifetime; the caller retains ownership of the referenced value.
|
|
261
|
+
* @category State models
|
|
262
|
+
* @since 1.0.0
|
|
263
|
+
*/
|
|
38
264
|
readonly value: A;
|
|
265
|
+
/**
|
|
266
|
+
* The state to restore or inspect beneath this optimistic layer.
|
|
267
|
+
* @remarks
|
|
268
|
+
* ## Why
|
|
269
|
+
* Explicit history supports deterministic rollback and transformation of nested optimistic states.
|
|
270
|
+
* ## Ownership and lifetime
|
|
271
|
+
* Inherits the enclosing optimistic wrapper's resource-free lifetime.
|
|
272
|
+
* @category State models
|
|
273
|
+
* @since 1.0.0
|
|
274
|
+
*/
|
|
39
275
|
readonly previous: AsyncData<A, E>;
|
|
40
276
|
}
|
|
41
277
|
|
|
278
|
+
/**
|
|
279
|
+
* The complete renderer-independent state machine for asynchronous data.
|
|
280
|
+
* @remarks
|
|
281
|
+
* ## Why
|
|
282
|
+
* A closed union makes absence, work, value, failure, and optimistic history compositional and exhaustively matchable.
|
|
283
|
+
* ## Ownership and lifetime
|
|
284
|
+
* AsyncData values acquire no resources; they describe operation state whose execution is owned elsewhere.
|
|
285
|
+
* @example
|
|
286
|
+
* ```ts
|
|
287
|
+
* import type { AsyncData } from "@typed/async-data"
|
|
288
|
+
* import { success } from "@typed/async-data"
|
|
289
|
+
* const state: AsyncData<number, string> = success(1)
|
|
290
|
+
* ```
|
|
291
|
+
* @category State models
|
|
292
|
+
* @since 1.0.0
|
|
293
|
+
*/
|
|
42
294
|
export type AsyncData<A, E> = NoData | Loading | Success<A> | Failure<E> | Optimistic<A, E>;
|
|
43
295
|
|
|
44
|
-
|
|
296
|
+
/**
|
|
297
|
+
* A success or failure that retains progress for an active refresh.
|
|
298
|
+
* @remarks
|
|
299
|
+
* ## Why
|
|
300
|
+
* This refinement lets consumers distinguish refreshes without losing the previous result.
|
|
301
|
+
* ## Ownership and lifetime
|
|
302
|
+
* This structural view acquires no resources and has the lifetime of the underlying state value.
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* import type { Refreshing } from "@typed/async-data"
|
|
306
|
+
* const state: Refreshing<number, never> = { _tag: "Success", value: 1, progress: { loaded: 0 } }
|
|
307
|
+
* ```
|
|
308
|
+
* @category State models
|
|
309
|
+
* @since 1.0.0
|
|
310
|
+
*/
|
|
311
|
+
export type Refreshing<A, E> = (Success<A> | Failure<E>) & {
|
|
312
|
+
/** Progress whose presence marks the retained result as actively refreshing. @since 1.0.0 */
|
|
313
|
+
readonly progress: Progress;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
type EncodedFailure = {
|
|
317
|
+
readonly _tag: "Failure";
|
|
318
|
+
readonly cause: Schema.Json;
|
|
319
|
+
readonly progress?: Progress | undefined;
|
|
320
|
+
};
|
|
45
321
|
|
|
322
|
+
type EncodedOptimistic<A, E> = {
|
|
323
|
+
readonly _tag: "Optimistic";
|
|
324
|
+
readonly value: A;
|
|
325
|
+
readonly previous: EncodedAsyncData<A, E>;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
type EncodedAsyncData<A, E> =
|
|
329
|
+
| NoData
|
|
330
|
+
| Loading
|
|
331
|
+
| Success<A>
|
|
332
|
+
| EncodedFailure
|
|
333
|
+
| EncodedOptimistic<A, E>;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Builds an Effect Schema codec for recursive AsyncData values.
|
|
337
|
+
* @remarks
|
|
338
|
+
* ## Why
|
|
339
|
+
* A codec gives transport boundaries the same state model as application code while encoding `Cause` as JSON and preserving schema service requirements.
|
|
340
|
+
* ## Ownership and lifetime
|
|
341
|
+
* Codec construction is pure and acquires no resources; decoding and encoding use the services declared by the supplied schemas.
|
|
342
|
+
* @example
|
|
343
|
+
* ```ts
|
|
344
|
+
* import { AsyncData } from "@typed/async-data"
|
|
345
|
+
* import { Schema } from "effect"
|
|
346
|
+
* const codec = AsyncData(Schema.String, Schema.String)
|
|
347
|
+
* ```
|
|
348
|
+
* See [Effect Schema](https://effect.website/docs/schema/introduction/).
|
|
349
|
+
* @category Serialization
|
|
350
|
+
* @since 1.0.0
|
|
351
|
+
*/
|
|
46
352
|
export const AsyncData = <const A extends Schema.Top, E extends Schema.Top>(
|
|
47
353
|
A: A,
|
|
48
354
|
E: E,
|
|
49
355
|
): Schema.Codec<
|
|
50
356
|
AsyncData<A["Type"], E["Type"]>,
|
|
51
|
-
|
|
357
|
+
EncodedAsyncData<A["Encoded"], E["Encoded"]>,
|
|
52
358
|
A["DecodingServices"] | E["DecodingServices"],
|
|
53
359
|
A["EncodingServices"] | E["EncodingServices"]
|
|
54
360
|
> => {
|
|
55
|
-
const Progress = Schema.Struct({ loaded: Schema.
|
|
361
|
+
const Progress = Schema.Struct({ loaded: Schema.Finite, total: Schema.optional(Schema.Finite) });
|
|
56
362
|
const NoData = Schema.Struct({ _tag: Schema.tag("NoData") });
|
|
57
363
|
const Loading = Schema.Struct({
|
|
58
364
|
_tag: Schema.tag("Loading"),
|
|
@@ -63,96 +369,431 @@ export const AsyncData = <const A extends Schema.Top, E extends Schema.Top>(
|
|
|
63
369
|
value: A,
|
|
64
370
|
progress: Schema.optional(Progress),
|
|
65
371
|
});
|
|
372
|
+
const CauseSchema = Schema.Cause(E, Schema.Defect());
|
|
66
373
|
const Failure = Schema.Struct({
|
|
67
374
|
_tag: Schema.tag("Failure"),
|
|
68
|
-
cause:
|
|
375
|
+
cause: Schema.toCodecJson(CauseSchema),
|
|
69
376
|
progress: Schema.optional(Progress),
|
|
70
377
|
});
|
|
71
378
|
const Optimistic = Schema.Struct({
|
|
72
379
|
_tag: Schema.tag("Optimistic"),
|
|
73
380
|
value: A,
|
|
74
|
-
previous: Schema.suspend(() =>
|
|
381
|
+
previous: Schema.suspend(() => AsyncDataSchema),
|
|
75
382
|
});
|
|
76
|
-
const
|
|
383
|
+
const AsyncDataSchema = Schema.Union([
|
|
384
|
+
NoData,
|
|
385
|
+
Loading,
|
|
386
|
+
Success,
|
|
387
|
+
Failure,
|
|
388
|
+
Optimistic,
|
|
389
|
+
]) as Schema.Codec<
|
|
77
390
|
AsyncData<A["Type"], E["Type"]>,
|
|
78
|
-
|
|
391
|
+
EncodedAsyncData<A["Encoded"], E["Encoded"]>,
|
|
79
392
|
A["DecodingServices"] | E["DecodingServices"],
|
|
80
393
|
A["EncodingServices"] | E["EncodingServices"]
|
|
81
394
|
>;
|
|
82
|
-
return
|
|
395
|
+
return AsyncDataSchema;
|
|
83
396
|
};
|
|
84
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Tests whether an AsyncData value is `NoData`.
|
|
400
|
+
* @remarks
|
|
401
|
+
* ## Why
|
|
402
|
+
* A named refinement narrows the union without duplicating tag checks.
|
|
403
|
+
* ## Ownership and lifetime
|
|
404
|
+
* This pure predicate acquires no resources and does not retain its argument.
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* import { isNoData, NoData } from "@typed/async-data"
|
|
408
|
+
* isNoData(NoData)
|
|
409
|
+
* ```
|
|
410
|
+
* @category State inspection
|
|
411
|
+
* @since 1.0.0
|
|
412
|
+
*/
|
|
85
413
|
export const isNoData = <A, E>(asyncData: AsyncData<A, E>): asyncData is NoData =>
|
|
86
414
|
asyncData._tag === "NoData";
|
|
415
|
+
/**
|
|
416
|
+
* Tests whether an AsyncData value is loading without a prior result.
|
|
417
|
+
* @remarks
|
|
418
|
+
* ## Why
|
|
419
|
+
* This refinement deliberately excludes refreshing success and failure states.
|
|
420
|
+
* ## Ownership and lifetime
|
|
421
|
+
* This pure predicate acquires no resources and does not retain its argument.
|
|
422
|
+
* @example
|
|
423
|
+
* ```ts
|
|
424
|
+
* import { isLoading, loading } from "@typed/async-data"
|
|
425
|
+
* isLoading(loading())
|
|
426
|
+
* ```
|
|
427
|
+
* @category State inspection
|
|
428
|
+
* @since 1.0.0
|
|
429
|
+
*/
|
|
87
430
|
export const isLoading = <A, E>(asyncData: AsyncData<A, E>): asyncData is Loading =>
|
|
88
431
|
asyncData._tag === "Loading";
|
|
432
|
+
/**
|
|
433
|
+
* Tests whether an AsyncData value contains a successful value.
|
|
434
|
+
* @remarks
|
|
435
|
+
* ## Why
|
|
436
|
+
* This refinement separates settled or refreshing success from optimistic state.
|
|
437
|
+
* ## Ownership and lifetime
|
|
438
|
+
* This pure predicate acquires no resources and does not retain its argument.
|
|
439
|
+
* @example
|
|
440
|
+
* ```ts
|
|
441
|
+
* import { isSuccess, success } from "@typed/async-data"
|
|
442
|
+
* isSuccess(success(1))
|
|
443
|
+
* ```
|
|
444
|
+
* @category State inspection
|
|
445
|
+
* @since 1.0.0
|
|
446
|
+
*/
|
|
89
447
|
export const isSuccess = <A, E>(asyncData: AsyncData<A, E>): asyncData is Success<A> =>
|
|
90
448
|
asyncData._tag === "Success";
|
|
449
|
+
/**
|
|
450
|
+
* Tests whether an AsyncData value contains an Effect Cause.
|
|
451
|
+
* @remarks
|
|
452
|
+
* ## Why
|
|
453
|
+
* The refinement exposes complete failure information without treating optimistic history as a current failure.
|
|
454
|
+
* ## Ownership and lifetime
|
|
455
|
+
* This pure predicate acquires no resources and does not retain its argument.
|
|
456
|
+
* @example
|
|
457
|
+
* ```ts
|
|
458
|
+
* import { failure, isFailure } from "@typed/async-data"
|
|
459
|
+
* import { Cause } from "effect"
|
|
460
|
+
* isFailure(failure(Cause.fail("offline")))
|
|
461
|
+
* ```
|
|
462
|
+
* @category State inspection
|
|
463
|
+
* @since 1.0.0
|
|
464
|
+
*/
|
|
91
465
|
export const isFailure = <A, E>(asyncData: AsyncData<A, E>): asyncData is Failure<E> =>
|
|
92
466
|
asyncData._tag === "Failure";
|
|
467
|
+
/**
|
|
468
|
+
* Tests whether an AsyncData value is an optimistic history node.
|
|
469
|
+
* @remarks
|
|
470
|
+
* ## Why
|
|
471
|
+
* A dedicated refinement makes history traversal explicit and type safe.
|
|
472
|
+
* ## Ownership and lifetime
|
|
473
|
+
* This pure predicate acquires no resources and does not retain its argument.
|
|
474
|
+
* @example
|
|
475
|
+
* ```ts
|
|
476
|
+
* import { isOptimistic, optimistic, success } from "@typed/async-data"
|
|
477
|
+
* isOptimistic(optimistic(success(1), 2))
|
|
478
|
+
* ```
|
|
479
|
+
* @category State inspection
|
|
480
|
+
* @since 1.0.0
|
|
481
|
+
*/
|
|
93
482
|
export const isOptimistic = <A, E>(asyncData: AsyncData<A, E>): asyncData is Optimistic<A, E> =>
|
|
94
483
|
asyncData._tag === "Optimistic";
|
|
95
484
|
|
|
96
|
-
const
|
|
485
|
+
const hasValidProgress = (u: object): boolean => {
|
|
486
|
+
if (!hasProperty(u, "progress") || u.progress === undefined) {
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
const progress = u.progress;
|
|
490
|
+
return (
|
|
491
|
+
isObject(progress) &&
|
|
492
|
+
hasProperty(progress, "loaded") &&
|
|
493
|
+
typeof progress.loaded === "number" &&
|
|
494
|
+
Number.isFinite(progress.loaded) &&
|
|
495
|
+
(!hasProperty(progress, "total") ||
|
|
496
|
+
progress.total === undefined ||
|
|
497
|
+
(typeof progress.total === "number" && Number.isFinite(progress.total)))
|
|
498
|
+
);
|
|
499
|
+
};
|
|
97
500
|
|
|
98
|
-
|
|
99
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Validates the runtime structure of an unknown AsyncData value.
|
|
503
|
+
* @remarks
|
|
504
|
+
* ## Why
|
|
505
|
+
* Boundary validation rejects malformed progress, failure causes, and cyclic optimistic histories before application logic depends on them.
|
|
506
|
+
* ## Ownership and lifetime
|
|
507
|
+
* Validation is iterative, acquires no resources, and retains no visited objects after returning.
|
|
508
|
+
* @example
|
|
509
|
+
* ```ts
|
|
510
|
+
* import { isAsyncData } from "@typed/async-data"
|
|
511
|
+
* isAsyncData({ _tag: "Loading", progress: { loaded: 1 } })
|
|
512
|
+
* ```
|
|
513
|
+
* @category Runtime validation
|
|
514
|
+
* @since 1.0.0
|
|
515
|
+
*/
|
|
516
|
+
export const isAsyncData = <A, E>(u: unknown): u is AsyncData<A, E> => {
|
|
517
|
+
const visited = new WeakSet<object>();
|
|
518
|
+
let current = u;
|
|
519
|
+
while (isObject(current) && hasProperty(current, "_tag") && isString(current._tag)) {
|
|
520
|
+
switch (current._tag) {
|
|
521
|
+
case "NoData":
|
|
522
|
+
return true;
|
|
523
|
+
case "Loading":
|
|
524
|
+
return hasValidProgress(current);
|
|
525
|
+
case "Success":
|
|
526
|
+
return hasProperty(current, "value") && hasValidProgress(current);
|
|
527
|
+
case "Failure":
|
|
528
|
+
return (
|
|
529
|
+
hasProperty(current, "cause") && Cause.isCause(current.cause) && hasValidProgress(current)
|
|
530
|
+
);
|
|
531
|
+
case "Optimistic":
|
|
532
|
+
if (
|
|
533
|
+
visited.has(current) ||
|
|
534
|
+
!hasProperty(current, "value") ||
|
|
535
|
+
!hasProperty(current, "previous")
|
|
536
|
+
) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
visited.add(current);
|
|
540
|
+
current = current.previous;
|
|
541
|
+
break;
|
|
542
|
+
default:
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
547
|
+
};
|
|
100
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Tests whether a success or failure carries active refresh progress.
|
|
551
|
+
* @remarks
|
|
552
|
+
* ## Why
|
|
553
|
+
* Refreshing keeps an existing result visible while making new work observable.
|
|
554
|
+
* ## Ownership and lifetime
|
|
555
|
+
* This pure predicate acquires no resources and does not retain its argument.
|
|
556
|
+
* @example
|
|
557
|
+
* ```ts
|
|
558
|
+
* import { isRefreshing, success } from "@typed/async-data"
|
|
559
|
+
* isRefreshing(success("cached", { loaded: 0 }))
|
|
560
|
+
* ```
|
|
561
|
+
* @category State inspection
|
|
562
|
+
* @since 1.0.0
|
|
563
|
+
*/
|
|
101
564
|
export const isRefreshing = <A, E>(asyncData: AsyncData<A, E>): asyncData is Refreshing<A, E> =>
|
|
102
565
|
(asyncData._tag === "Success" || asyncData._tag === "Failure") &&
|
|
103
566
|
asyncData.progress !== undefined;
|
|
104
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Tests whether the base state is loading or refreshing through optimistic history.
|
|
570
|
+
* @remarks
|
|
571
|
+
* ## Why
|
|
572
|
+
* Pending status follows the underlying operation rather than disappearing when optimistic values are layered above it; cycles return `false`.
|
|
573
|
+
*
|
|
574
|
+
* **Known type/runtime discrepancy:** an `Optimistic` wrapper over a pending base returns `true` at runtime, but the existing type predicate narrows to `Loading | Refreshing<A, E>` and excludes `Optimistic`. Do not use this function to narrow before reading `_tag`; use it only as a pending-status boolean and refine the original value separately. The predicate is retained for compatibility and is not an accurate description of every `true` result.
|
|
575
|
+
* ## Ownership and lifetime
|
|
576
|
+
* This iterative predicate acquires no resources and retains no visited objects after returning.
|
|
577
|
+
* @example
|
|
578
|
+
* ```ts
|
|
579
|
+
* import { isPending, loading, optimistic } from "@typed/async-data"
|
|
580
|
+
* isPending(optimistic(loading(), "draft"))
|
|
581
|
+
* ```
|
|
582
|
+
* @category State inspection
|
|
583
|
+
* @since 1.0.0
|
|
584
|
+
*/
|
|
105
585
|
export const isPending = <A, E>(
|
|
106
586
|
asyncData: AsyncData<A, E>,
|
|
107
|
-
): asyncData is Loading | Refreshing<A, E> =>
|
|
108
|
-
|
|
587
|
+
): asyncData is Loading | Refreshing<A, E> => {
|
|
588
|
+
const visited = new WeakSet<object>();
|
|
589
|
+
let current: AsyncData<A, E> = asyncData;
|
|
590
|
+
while (isOptimistic(current)) {
|
|
591
|
+
if (visited.has(current)) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
visited.add(current);
|
|
595
|
+
current = current.previous;
|
|
596
|
+
}
|
|
597
|
+
return current._tag === "Loading" || isRefreshing(current);
|
|
598
|
+
};
|
|
109
599
|
|
|
600
|
+
/**
|
|
601
|
+
* The canonical shared initial AsyncData object.
|
|
602
|
+
* @remarks
|
|
603
|
+
* ## Why
|
|
604
|
+
* A shared singleton avoids allocating an equivalent empty state and gives callers an exact constructor value.
|
|
605
|
+
* ## Ownership and lifetime
|
|
606
|
+
* The module owns this resource-free singleton for the process lifetime. It is not frozen: mutation through an unsafe cast changes the shared value for every caller.
|
|
607
|
+
* @example
|
|
608
|
+
* ```ts
|
|
609
|
+
* import { NoData } from "@typed/async-data"
|
|
610
|
+
* const initial = NoData
|
|
611
|
+
* ```
|
|
612
|
+
* @category State construction
|
|
613
|
+
* @since 1.0.0
|
|
614
|
+
*/
|
|
110
615
|
export const NoData: NoData = { _tag: "NoData" };
|
|
111
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Creates a loading state with optional progress.
|
|
619
|
+
* @remarks
|
|
620
|
+
* ## Why
|
|
621
|
+
* The constructor keeps state creation consistent with the discriminated union.
|
|
622
|
+
* ## Ownership and lifetime
|
|
623
|
+
* This pure function acquires no resources and returns a new wrapper.
|
|
624
|
+
* @example
|
|
625
|
+
* ```ts
|
|
626
|
+
* import { loading } from "@typed/async-data"
|
|
627
|
+
* const state = loading({ loaded: 2, total: 5 })
|
|
628
|
+
* ```
|
|
629
|
+
* @category State construction
|
|
630
|
+
* @since 1.0.0
|
|
631
|
+
*/
|
|
112
632
|
export const loading = (progress?: Progress): Loading => ({ _tag: "Loading", progress });
|
|
113
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Creates a successful state with optional refresh progress.
|
|
636
|
+
* @remarks
|
|
637
|
+
* ## Why
|
|
638
|
+
* The constructor preserves the payload type while making refresh state explicit.
|
|
639
|
+
* ## Ownership and lifetime
|
|
640
|
+
* This pure function acquires no resources; the returned wrapper retains the supplied value.
|
|
641
|
+
* @example
|
|
642
|
+
* ```ts
|
|
643
|
+
* import { success } from "@typed/async-data"
|
|
644
|
+
* const state = success("ready")
|
|
645
|
+
* ```
|
|
646
|
+
* @category State construction
|
|
647
|
+
* @since 1.0.0
|
|
648
|
+
*/
|
|
114
649
|
export const success = <A>(value: A, progress?: Progress): Success<A> => ({
|
|
115
650
|
_tag: "Success",
|
|
116
651
|
value,
|
|
117
652
|
progress,
|
|
118
653
|
});
|
|
119
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Creates a failed state while preserving the complete Effect Cause.
|
|
657
|
+
* @remarks
|
|
658
|
+
* ## Why
|
|
659
|
+
* Accepting `Cause` prevents typed failures, defects, and interruption from being collapsed into one error value.
|
|
660
|
+
* ## Ownership and lifetime
|
|
661
|
+
* This pure function acquires no resources; the returned wrapper retains the supplied persistent Cause.
|
|
662
|
+
* @example
|
|
663
|
+
* ```ts
|
|
664
|
+
* import { failure } from "@typed/async-data"
|
|
665
|
+
* import { Cause } from "effect"
|
|
666
|
+
* const state = failure(Cause.fail("offline"))
|
|
667
|
+
* ```
|
|
668
|
+
* @category State construction
|
|
669
|
+
* @since 1.0.0
|
|
670
|
+
*/
|
|
120
671
|
export const failure = <E>(cause: Cause.Cause<E>, progress?: Progress): Failure<E> => ({
|
|
121
672
|
_tag: "Failure",
|
|
122
673
|
cause,
|
|
123
674
|
progress,
|
|
124
675
|
});
|
|
125
676
|
|
|
677
|
+
/**
|
|
678
|
+
* Adds an optimistic value above an existing AsyncData history.
|
|
679
|
+
* @remarks
|
|
680
|
+
* ## Why
|
|
681
|
+
* Keeping `previous` makes rollback and reconciliation explicit rather than mutating or discarding earlier state.
|
|
682
|
+
* ## Ownership and lifetime
|
|
683
|
+
* This pure function acquires no resources; the wrapper retains both supplied values.
|
|
684
|
+
* @example
|
|
685
|
+
* ```ts
|
|
686
|
+
* import { optimistic, success } from "@typed/async-data"
|
|
687
|
+
* const state = optimistic(success("saved"), "saving")
|
|
688
|
+
* ```
|
|
689
|
+
* @category Optimistic transitions
|
|
690
|
+
* @since 1.0.0
|
|
691
|
+
*/
|
|
126
692
|
export const optimistic = <A, E>(previous: AsyncData<A, E>, value: A): Optimistic<A, E> => ({
|
|
127
693
|
_tag: "Optimistic",
|
|
128
694
|
value,
|
|
129
695
|
previous,
|
|
130
696
|
});
|
|
131
697
|
|
|
698
|
+
const optimisticHistory = <A, E>(data: AsyncData<A, E>) => {
|
|
699
|
+
const values: Array<A> = [];
|
|
700
|
+
const visited = new WeakSet<object>();
|
|
701
|
+
let current = data;
|
|
702
|
+
while (isOptimistic(current)) {
|
|
703
|
+
if (visited.has(current)) {
|
|
704
|
+
throw new TypeError("Cyclic Optimistic history");
|
|
705
|
+
}
|
|
706
|
+
visited.add(current);
|
|
707
|
+
values.push(current.value);
|
|
708
|
+
current = current.previous;
|
|
709
|
+
}
|
|
710
|
+
return { base: current, values };
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const rebuildOptimistic = <A, E>(base: AsyncData<A, E>, values: ReadonlyArray<A>) => {
|
|
714
|
+
let current = base;
|
|
715
|
+
for (let index = values.length - 1; index >= 0; index--) {
|
|
716
|
+
current = optimistic(current, values[index]!);
|
|
717
|
+
}
|
|
718
|
+
return current;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const refreshingProgress = (progress?: Progress, existing?: Progress): Progress =>
|
|
722
|
+
progress ?? existing ?? { loaded: 0 };
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Starts loading while preserving successful, failed, and optimistic history.
|
|
726
|
+
* @remarks
|
|
727
|
+
* ## Why
|
|
728
|
+
* Refreshes should keep usable values or causes visible, and optimistic layers must remain in their original order. Cyclic history throws `TypeError`.
|
|
729
|
+
* ## Ownership and lifetime
|
|
730
|
+
* This pure transformation acquires no resources and returns new plain wrappers where rebuilding is needed.
|
|
731
|
+
* @example
|
|
732
|
+
* ```ts
|
|
733
|
+
* import { startLoading, success } from "@typed/async-data"
|
|
734
|
+
* const refreshing = startLoading(success("cached"))
|
|
735
|
+
* ```
|
|
736
|
+
* @category Refresh transitions
|
|
737
|
+
* @since 1.0.0
|
|
738
|
+
*/
|
|
132
739
|
export const startLoading = <A, E>(data: AsyncData<A, E>, progress?: Progress): AsyncData<A, E> => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
} else if (
|
|
138
|
-
|
|
740
|
+
const { base, values } = optimisticHistory(data);
|
|
741
|
+
let result: AsyncData<A, E>;
|
|
742
|
+
if (isSuccess(base)) {
|
|
743
|
+
result = success(base.value, refreshingProgress(progress, base.progress));
|
|
744
|
+
} else if (isFailure(base)) {
|
|
745
|
+
result = failure(base.cause, refreshingProgress(progress, base.progress));
|
|
746
|
+
} else if (isLoading(base)) {
|
|
747
|
+
result = loading(progress ?? base.progress);
|
|
139
748
|
} else {
|
|
140
|
-
|
|
749
|
+
result = loading(progress);
|
|
141
750
|
}
|
|
751
|
+
return rebuildOptimistic(result, values);
|
|
142
752
|
};
|
|
143
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Stops loading or refreshing without discarding optimistic history.
|
|
756
|
+
* @remarks
|
|
757
|
+
* ## Why
|
|
758
|
+
* Removing progress settles success and failure while leaving initial or loading state semantics predictable. Cyclic history throws `TypeError`.
|
|
759
|
+
* ## Ownership and lifetime
|
|
760
|
+
* This pure transformation acquires no resources and returns new plain wrappers where rebuilding is needed.
|
|
761
|
+
* @example
|
|
762
|
+
* ```ts
|
|
763
|
+
* import { startLoading, stopLoading, success } from "@typed/async-data"
|
|
764
|
+
* const settled = stopLoading(startLoading(success("cached")))
|
|
765
|
+
* ```
|
|
766
|
+
* @category Refresh transitions
|
|
767
|
+
* @since 1.0.0
|
|
768
|
+
*/
|
|
144
769
|
export const stopLoading = <A, E>(data: AsyncData<A, E>): AsyncData<A, E> => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} else if (
|
|
150
|
-
|
|
770
|
+
const { base, values } = optimisticHistory(data);
|
|
771
|
+
let result: AsyncData<A, E>;
|
|
772
|
+
if (isSuccess(base)) {
|
|
773
|
+
result = success(base.value);
|
|
774
|
+
} else if (isFailure(base)) {
|
|
775
|
+
result = failure(base.cause);
|
|
151
776
|
} else {
|
|
152
|
-
|
|
777
|
+
result = base;
|
|
153
778
|
}
|
|
779
|
+
return rebuildOptimistic(result, values);
|
|
154
780
|
};
|
|
155
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Exhaustively folds every AsyncData variant, in data-first or data-last form.
|
|
784
|
+
* @remarks
|
|
785
|
+
* ## Why
|
|
786
|
+
* Centralized exhaustive dispatch exposes values and Causes with their full state while TypeScript unifies branch result types.
|
|
787
|
+
* ## Ownership and lifetime
|
|
788
|
+
* Matching is synchronous, acquires no resources, and retains nothing beyond callback behavior.
|
|
789
|
+
* @example
|
|
790
|
+
* ```ts
|
|
791
|
+
* import { match } from "@typed/async-data"
|
|
792
|
+
* const label = match({ NoData: () => "empty", Loading: () => "loading", Failure: () => "failed", Success: String, Optimistic: String })
|
|
793
|
+
* ```
|
|
794
|
+
* @category Pattern matching
|
|
795
|
+
* @since 1.0.0
|
|
796
|
+
*/
|
|
156
797
|
export const match: {
|
|
157
798
|
<A, E, R1, R2, R3, R4, R5>(matchers: {
|
|
158
799
|
NoData: (data: NoData) => R1;
|
|
@@ -198,6 +839,22 @@ export const match: {
|
|
|
198
839
|
},
|
|
199
840
|
);
|
|
200
841
|
|
|
842
|
+
/**
|
|
843
|
+
* Returns the current successful or optimistic value as an Effect `Option`.
|
|
844
|
+
* @remarks
|
|
845
|
+
* ## Why
|
|
846
|
+
* `Option` distinguishes an absent value from a present `undefined` payload and composes with Effect's data APIs.
|
|
847
|
+
* ## Ownership and lifetime
|
|
848
|
+
* This pure lookup acquires no resources and does not retain the state.
|
|
849
|
+
* @example
|
|
850
|
+
* ```ts
|
|
851
|
+
* import { getSuccess, success } from "@typed/async-data"
|
|
852
|
+
* const value = getSuccess(success(1))
|
|
853
|
+
* ```
|
|
854
|
+
* See [Effect Option](https://effect.website/docs/data-types/option/).
|
|
855
|
+
* @category State extraction
|
|
856
|
+
* @since 1.0.0
|
|
857
|
+
*/
|
|
201
858
|
export function getSuccess<A, E>(data: AsyncData<A, E>): Option.Option<A> {
|
|
202
859
|
return match(data, {
|
|
203
860
|
NoData: Option.none,
|
|
@@ -209,6 +866,19 @@ export function getSuccess<A, E>(data: AsyncData<A, E>): Option.Option<A> {
|
|
|
209
866
|
}
|
|
210
867
|
|
|
211
868
|
/**
|
|
869
|
+
* Returns the complete Cause only when the current state is a Failure.
|
|
870
|
+
* @remarks
|
|
871
|
+
* ## Why
|
|
872
|
+
* The accessor keeps typed errors, defects, and interruption intact for callers that need full failure diagnostics.
|
|
873
|
+
* ## Ownership and lifetime
|
|
874
|
+
* This pure lookup acquires no resources and returns a reference to the persistent Cause.
|
|
875
|
+
* @example
|
|
876
|
+
* ```ts
|
|
877
|
+
* import { failure, getCause } from "@typed/async-data"
|
|
878
|
+
* import { Cause } from "effect"
|
|
879
|
+
* const cause = getCause(failure(Cause.fail("offline")))
|
|
880
|
+
* ```
|
|
881
|
+
* @category State extraction
|
|
212
882
|
* @since 1.0.0
|
|
213
883
|
*/
|
|
214
884
|
export function getCause<A, E>(data: AsyncData<A, E>): Option.Option<Cause.Cause<E>> {
|
|
@@ -222,6 +892,19 @@ export function getCause<A, E>(data: AsyncData<A, E>): Option.Option<Cause.Cause
|
|
|
222
892
|
}
|
|
223
893
|
|
|
224
894
|
/**
|
|
895
|
+
* Returns the first typed failure found in a Failure Cause.
|
|
896
|
+
* @remarks
|
|
897
|
+
* ## Why
|
|
898
|
+
* This convenience accessor intentionally excludes defects and interruption; use `getCause` when those distinctions matter.
|
|
899
|
+
* ## Ownership and lifetime
|
|
900
|
+
* This pure lookup acquires no resources and does not retain the state.
|
|
901
|
+
* @example
|
|
902
|
+
* ```ts
|
|
903
|
+
* import { failure, getError } from "@typed/async-data"
|
|
904
|
+
* import { Cause } from "effect"
|
|
905
|
+
* const error = getError(failure(Cause.fail("offline")))
|
|
906
|
+
* ```
|
|
907
|
+
* @category State extraction
|
|
225
908
|
* @since 1.0.0
|
|
226
909
|
*/
|
|
227
910
|
export function getError<A, E>(data: AsyncData<A, E>): Option.Option<E> {
|
|
@@ -234,26 +917,60 @@ export function getError<A, E>(data: AsyncData<A, E>): Option.Option<E> {
|
|
|
234
917
|
});
|
|
235
918
|
}
|
|
236
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Maps successful and every optimistic value while preserving state structure.
|
|
922
|
+
* @remarks
|
|
923
|
+
* ## Why
|
|
924
|
+
* Value transformations should not erase progress, Causes, or optimistic rollback history. Cyclic history throws `TypeError`.
|
|
925
|
+
* ## Ownership and lifetime
|
|
926
|
+
* This pure transformation acquires no resources and returns new plain wrappers.
|
|
927
|
+
* @example
|
|
928
|
+
* ```ts
|
|
929
|
+
* import { map, success } from "@typed/async-data"
|
|
930
|
+
* const state = map(success(2), (n) => n * 2)
|
|
931
|
+
* ```
|
|
932
|
+
* @category Value transformations
|
|
933
|
+
* @since 1.0.0
|
|
934
|
+
*/
|
|
237
935
|
export const map: {
|
|
238
936
|
<A, B>(f: (a: A) => B): <E>(data: AsyncData<A, E>) => AsyncData<B, E>;
|
|
239
937
|
<A, E, B>(data: AsyncData<A, E>, f: (a: A) => B): AsyncData<B, E>;
|
|
240
938
|
} = dual(2, function map<A, E, B>(data: AsyncData<A, E>, f: (a: A) => B): AsyncData<B, E> {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
939
|
+
const { base, values } = optimisticHistory(data);
|
|
940
|
+
let result: AsyncData<B, E>;
|
|
941
|
+
if (isSuccess(base)) {
|
|
942
|
+
result = success(f(base.value), base.progress);
|
|
245
943
|
} else {
|
|
246
|
-
|
|
944
|
+
result = base;
|
|
247
945
|
}
|
|
946
|
+
for (let index = values.length - 1; index >= 0; index--) {
|
|
947
|
+
result = optimistic(result, f(values[index]!));
|
|
948
|
+
}
|
|
949
|
+
return result;
|
|
248
950
|
});
|
|
249
951
|
|
|
952
|
+
/**
|
|
953
|
+
* Replaces a successful or outer optimistic value with another AsyncData value.
|
|
954
|
+
* @remarks
|
|
955
|
+
* ## Why
|
|
956
|
+
* State-producing transformations can change both value and error types while non-value states pass through unchanged.
|
|
957
|
+
* ## Ownership and lifetime
|
|
958
|
+
* This pure transformation acquires no resources; ownership follows the AsyncData returned by the callback.
|
|
959
|
+
* @example
|
|
960
|
+
* ```ts
|
|
961
|
+
* import { flatMap, success } from "@typed/async-data"
|
|
962
|
+
* const parsed = flatMap(success("2"), (text) => success(Number(text)))
|
|
963
|
+
* ```
|
|
964
|
+
* @category Value transformations
|
|
965
|
+
* @since 1.0.0
|
|
966
|
+
*/
|
|
250
967
|
export const flatMap: {
|
|
251
|
-
<A,
|
|
252
|
-
f: (a: A, data: Success<A> | Optimistic<A,
|
|
253
|
-
): (data: AsyncData<A, E>) => AsyncData<B, E | E2>;
|
|
968
|
+
<A, B, E2>(
|
|
969
|
+
f: (a: A, data: Success<A> | Optimistic<A, unknown>) => AsyncData<B, E2>,
|
|
970
|
+
): <E>(data: AsyncData<A, E>) => AsyncData<B, E | E2>;
|
|
254
971
|
<A, E, B, E2>(
|
|
255
972
|
data: AsyncData<A, E>,
|
|
256
|
-
f: (a: A, data: Success<A> | Optimistic<A, E>) => AsyncData<B,
|
|
973
|
+
f: (a: A, data: Success<A> | Optimistic<A, E>) => AsyncData<B, E2>,
|
|
257
974
|
): AsyncData<B, E | E2>;
|
|
258
975
|
} = dual(2, function <
|
|
259
976
|
A,
|
|
@@ -271,21 +988,70 @@ export const flatMap: {
|
|
|
271
988
|
}
|
|
272
989
|
});
|
|
273
990
|
|
|
991
|
+
/**
|
|
992
|
+
* Maps typed failures inside the base Cause while preserving defects, interruption, progress, and optimistic history.
|
|
993
|
+
* @remarks
|
|
994
|
+
* ## Why
|
|
995
|
+
* Error adaptation should use Effect Cause semantics instead of flattening the failure channel. Cyclic history throws `TypeError`.
|
|
996
|
+
* ## Ownership and lifetime
|
|
997
|
+
* This pure transformation acquires no resources and returns new plain wrappers.
|
|
998
|
+
* @example
|
|
999
|
+
* ```ts
|
|
1000
|
+
* import { failure, mapError } from "@typed/async-data"
|
|
1001
|
+
* import { Cause } from "effect"
|
|
1002
|
+
* const state = mapError(failure(Cause.fail(404)), String)
|
|
1003
|
+
* ```
|
|
1004
|
+
* @category Failure transformations
|
|
1005
|
+
* @since 1.0.0
|
|
1006
|
+
*/
|
|
274
1007
|
export const mapError: {
|
|
275
1008
|
<A, E, E2>(f: (e: E) => E2): (data: AsyncData<A, E>) => AsyncData<A, E2>;
|
|
276
1009
|
<A, E, E2>(data: AsyncData<A, E>, f: (e: E) => E2): AsyncData<A, E2>;
|
|
277
1010
|
} = dual(2, function mapError<A, E, E2>(data: AsyncData<A, E>, f: (e: E) => E2): AsyncData<A, E2> {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
1011
|
+
const { base, values } = optimisticHistory(data);
|
|
1012
|
+
let result: AsyncData<A, E2>;
|
|
1013
|
+
if (isFailure(base)) {
|
|
1014
|
+
result = failure(Cause.map(base.cause, f), base.progress);
|
|
282
1015
|
} else {
|
|
283
|
-
|
|
1016
|
+
result = base;
|
|
284
1017
|
}
|
|
1018
|
+
return rebuildOptimistic(result, values);
|
|
285
1019
|
});
|
|
286
1020
|
|
|
1021
|
+
/**
|
|
1022
|
+
* Converts an Effect Exit to Success or Failure without losing its Cause.
|
|
1023
|
+
* @remarks
|
|
1024
|
+
* ## Why
|
|
1025
|
+
* Exit is Effect's complete computation result, so preserving its Cause keeps typed failures, defects, and interruption available.
|
|
1026
|
+
* ## Ownership and lifetime
|
|
1027
|
+
* This pure conversion acquires no resources and retains the Exit payload or Cause.
|
|
1028
|
+
* @example
|
|
1029
|
+
* ```ts
|
|
1030
|
+
* import { fromExit } from "@typed/async-data"
|
|
1031
|
+
* import { Exit } from "effect"
|
|
1032
|
+
* const state = fromExit(Exit.succeed(1))
|
|
1033
|
+
* ```
|
|
1034
|
+
* @category Effect outcome conversion
|
|
1035
|
+
* @since 1.0.0
|
|
1036
|
+
*/
|
|
287
1037
|
export const fromExit = <A, E>(exit: Exit.Exit<A, E>): AsyncData<A, E> =>
|
|
288
1038
|
Exit.isSuccess(exit) ? success(exit.value) : failure(exit.cause);
|
|
289
1039
|
|
|
1040
|
+
/**
|
|
1041
|
+
* Converts an Effect Result to Success or a typed Failure Cause.
|
|
1042
|
+
* @remarks
|
|
1043
|
+
* ## Why
|
|
1044
|
+
* Result has only typed success/failure, so a failed result becomes `Cause.fail` without inventing defects or interruption.
|
|
1045
|
+
* ## Ownership and lifetime
|
|
1046
|
+
* This pure conversion acquires no resources and retains the Result payload.
|
|
1047
|
+
* @example
|
|
1048
|
+
* ```ts
|
|
1049
|
+
* import { fromResult } from "@typed/async-data"
|
|
1050
|
+
* import { Result } from "effect"
|
|
1051
|
+
* const state = fromResult(Result.succeed(1))
|
|
1052
|
+
* ```
|
|
1053
|
+
* @category Effect outcome conversion
|
|
1054
|
+
* @since 1.0.0
|
|
1055
|
+
*/
|
|
290
1056
|
export const fromResult = <A, E>(result: Result.Result<A, E>): AsyncData<A, E> =>
|
|
291
1057
|
Result.isSuccess(result) ? success(result.success) : failure(Cause.fail(result.failure));
|