unwrapped 0.1.1 → 0.1.3
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/CHANGELOG.md +12 -0
- package/README.md +553 -2
- package/dist/core/index.d.mts +483 -44
- package/dist/core/index.d.ts +483 -44
- package/dist/core/index.js +529 -117
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +529 -117
- package/dist/core/index.mjs.map +1 -1
- package/dist/vue/index.d.mts +103 -22
- package/dist/vue/index.d.ts +103 -22
- package/dist/vue/index.js +16 -44
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/index.mjs +18 -46
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/core/asyncResult.ts +0 -312
- package/src/core/cache.ts +0 -81
- package/src/core/error.ts +0 -23
- package/src/core/index.ts +0 -4
- package/src/core/result.ts +0 -101
- package/src/vue/components/asyncResultLoader.ts +0 -99
- package/src/vue/composables.ts +0 -106
- package/src/vue/index.ts +0 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,2 +1,553 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# Unwrapped
|
|
2
|
+
|
|
3
|
+
A TypeScript library for elegant error handling and asynchronous state management, inspired by Rust's `Result` type and designed with Vue 3 integration in mind.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
**Unwrapped** provides a robust alternative to `try/catch` blocks and promise chains, making error handling explicit, type-safe, and composable. It consists of two main parts:
|
|
8
|
+
|
|
9
|
+
- **Core**: Framework-agnostic utilities for managing results and async operations
|
|
10
|
+
- **Vue**: Vue 3 composables and components for reactive async state management
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install unwrapped
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Core Concepts
|
|
19
|
+
|
|
20
|
+
### `Result<T, E>`
|
|
21
|
+
|
|
22
|
+
A `Result` represents a synchronous operation that can either succeed with a value of type `T` or fail with an error of type `E`.
|
|
23
|
+
|
|
24
|
+
#### **Basic Usage:**
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { Result, ErrorBase } from 'unwrapped/core';
|
|
28
|
+
|
|
29
|
+
function divide(a: number, b: number): Result<number> {
|
|
30
|
+
if (b === 0) {
|
|
31
|
+
return Result.errTag("division_by_zero", "Can't divide by 0 !");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return Result.ok(a / b);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const shouldSucceed = divide(10, 2);
|
|
38
|
+
const shouldError = divide(10, 0);
|
|
39
|
+
|
|
40
|
+
// Checking status
|
|
41
|
+
if (shouldSucceed.isSuccess()) {
|
|
42
|
+
console.log("Success !");
|
|
43
|
+
}
|
|
44
|
+
if (shouldError.isError()) {
|
|
45
|
+
console.log("Error !");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Unwrapping values
|
|
49
|
+
const value = shouldSucceed.unwrapOrNull(); // 5
|
|
50
|
+
const valueOrDefault = shouldError.unwrapOr(0); // Returns 0 since it's an error
|
|
51
|
+
const valueOrThrow = shouldSucceed.unwrapOrThrow();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### **Working with Promises:**
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// Wrap a promise and catch errors
|
|
58
|
+
const result = await Result.tryPromise(
|
|
59
|
+
fetch("/api/data").then(r => r.json(),
|
|
60
|
+
(error) => new ErrorBase("fetch_error", "Failed to fetch data", error)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Execute an async function
|
|
64
|
+
const result = await Result.tryFunction(
|
|
65
|
+
async () => {
|
|
66
|
+
const response = await fetch("/api/data");
|
|
67
|
+
return response.json();
|
|
68
|
+
},
|
|
69
|
+
(error) => new ErrorBase("fetch_error", "Failed to fetch data", error)
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### **Chaining Operations:**
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
function validateAge(age: number): Result<number, ErrorBase> {
|
|
77
|
+
if (age < 0) {
|
|
78
|
+
return Result.err(new ErrorBase("invalid_age", "Age must be positive"));
|
|
79
|
+
}
|
|
80
|
+
return Result.ok(age);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function categorizeAge(age: number): Result<string, ErrorBase> {
|
|
84
|
+
if (age < 18) return Result.ok('minor');
|
|
85
|
+
if (age < 65) return Result.ok('adult');
|
|
86
|
+
return Result.ok('senior');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Chain operations - stops at first error
|
|
90
|
+
const valid = Result.ok(25)
|
|
91
|
+
.flatChain(validateAge) // 25 is a valid age so execution continues
|
|
92
|
+
.flatChain(categorizeAge); // 25 is passed to categorizeAge
|
|
93
|
+
|
|
94
|
+
const invalid = Result.ok(-1)
|
|
95
|
+
.flatChain(validateAge) // -1 is not a valid age, so the chain short-circuits and returns a Result containing the error given by validateAge
|
|
96
|
+
.flatChain(categorizeAge) // categorizeAge does not get called
|
|
97
|
+
|
|
98
|
+
console.log(valid.state); // { status: "success", value: "adult" }
|
|
99
|
+
console.log(invalid.state); // { status: "error", value: <ErrorBase> }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### **Generator Syntax for Complex Flows:**
|
|
103
|
+
|
|
104
|
+
The same way async/await allows to write asynchronous code in a synchronous-looking way, generators can be used to write result chaining in a more imperative-looking manner via `Result.run()`.
|
|
105
|
+
|
|
106
|
+
Think of `function*` as `async function` and `yield*` as `await`. Inside a generator function executed by `Result.run()`, yielding a `Result` with `yield*` unwraps the `Result` and allows you to get its value if it is successful. If the `Result` contains an error, the whole generator will terminate and `Result.run()` will return a `Result` containing the error.
|
|
107
|
+
|
|
108
|
+
Note that in the case of `Result.run()`, everything is synchronous. For performing the same kind of operations on asynchronous tasks, use `AsyncResult.run()`.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
|
|
112
|
+
const valid = Result.run(function* () {
|
|
113
|
+
const validatedAge = yield* validateAge(10); // 10 is a valid age so validatedAge is set to 10 and execution continues
|
|
114
|
+
const category = yield* categorizeAge(validateAge); // yield* unwraps the value so category is set to "minor"
|
|
115
|
+
|
|
116
|
+
return category;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const invalid = Result.run(function* () {
|
|
120
|
+
const validatedAge = yield* validateAge(-1); // -1 is not a valid age so the run terminates early and returns a Result containing the error given by validateAge
|
|
121
|
+
const category = yield* categorizeAge(validateAge); // this never gets reached
|
|
122
|
+
|
|
123
|
+
return category;
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// If any step fails, the error is automatically propagated
|
|
127
|
+
if (invalid.isError()) {
|
|
128
|
+
console.error(result.state.error);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### `AsyncResult<T, E>`
|
|
133
|
+
|
|
134
|
+
An `AsyncResult` represents an asynchronous operation with four possible states: `idle`, `loading`, `success`, or `error`.
|
|
135
|
+
|
|
136
|
+
#### **Basic Usage:**
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { AsyncResult, Result } from 'unwrapped/core';
|
|
140
|
+
|
|
141
|
+
// Create from a promise that returns a Result
|
|
142
|
+
const asyncResult = AsyncResult.fromResultPromise(
|
|
143
|
+
fetch('/api/user')
|
|
144
|
+
.then(r => r.json())
|
|
145
|
+
.then(data => Result.ok(data))
|
|
146
|
+
.catch(err => Result.err(new ErrorBase('API_ERROR', 'Failed to fetch', err)))
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Create from a plain promise
|
|
150
|
+
const asyncResult = AsyncResult.fromValuePromise(
|
|
151
|
+
fetch('/api/user').then(r => r.json())
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Check current state
|
|
155
|
+
console.log(asyncResult.isLoading()); // true
|
|
156
|
+
console.log(asyncResult.isSuccess()); // false
|
|
157
|
+
|
|
158
|
+
// Listen to state changes
|
|
159
|
+
asyncResult.listen((result) => {
|
|
160
|
+
if (result.isSuccess()) {
|
|
161
|
+
console.log('Data loaded:', result.unwrapOrNull());
|
|
162
|
+
} else if (result.isError()) {
|
|
163
|
+
console.error('Error:', result.state.error);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Wait for completion
|
|
168
|
+
const settledResult = await asyncResult.waitForSettled();
|
|
169
|
+
const value = settledResult.unwrapOrNull();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### **Lazy Actions:**
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Create an action that doesn't execute until triggered
|
|
176
|
+
const { trigger, result } = AsyncResult.makeLazyAction(async () => {
|
|
177
|
+
const response = await fetch('/api/data');
|
|
178
|
+
const data = await response.json();
|
|
179
|
+
return Result.ok(data);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Listen for changes
|
|
183
|
+
result.listen((r) => {
|
|
184
|
+
console.log('State:', r.state.status);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Trigger execution
|
|
188
|
+
trigger();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### **Chaining Async Operations:**
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const userResult = AsyncResult.fromValuePromise(fetch('/api/user/1').then(r => r.json()));
|
|
195
|
+
|
|
196
|
+
// Chain with another async operation
|
|
197
|
+
const postsResult = userResult.chain(async (user) => {
|
|
198
|
+
const response = await fetch(`/api/posts?userId=${user.id}`);
|
|
199
|
+
const posts = await response.json();
|
|
200
|
+
return Result.ok(posts);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// FlatChain with AsyncResult
|
|
204
|
+
const enrichedPosts = postsResult.flatChain((posts) => {
|
|
205
|
+
return AsyncResult.fromValuePromise(
|
|
206
|
+
Promise.all(posts.map(p => enrichPost(p)))
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### **Generator Syntax for Async Operations:**
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
function fetchUser(id: string): AsyncResult<User> {
|
|
215
|
+
// ...
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function fetchProfile(id: string): AsyncResult<Profile> {
|
|
219
|
+
// ...
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const result = AsyncResult.run(function* () {
|
|
223
|
+
const user = yield* fetchUser(userId);
|
|
224
|
+
const profile = yield* fetchProfile(user.profileId);
|
|
225
|
+
|
|
226
|
+
return { user, profile };
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// result is an AsyncResult that automatically tracks loading/success/error states
|
|
230
|
+
result.debug("Profile fetcher");
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### **Ensuring Multiple AsyncResults:**
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Wait for multiple AsyncResults to complete
|
|
237
|
+
const user: AsyncResult<User> = fetchUser(userId);
|
|
238
|
+
const settings: AsyncResult<Settings> = AsyncResult.fromValuePromise(fetchSettings());
|
|
239
|
+
|
|
240
|
+
const all = AsyncResult.ensureAvailable([user, settings]);
|
|
241
|
+
|
|
242
|
+
all.listen((result) => {
|
|
243
|
+
if (result.isSuccess()) {
|
|
244
|
+
const [userData, settingsData] = result.unwrapOrThrow();
|
|
245
|
+
console.log('All data loaded', { userData, settingsData });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### `KeyedAsyncCache<P, V, E>`
|
|
251
|
+
|
|
252
|
+
A cache for asynchronous operations that maps parameters to their results, with support for automatic refetching.
|
|
253
|
+
|
|
254
|
+
**Usage:**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { KeyedAsyncCache, Result } from 'unwrapped/core';
|
|
258
|
+
|
|
259
|
+
// Create a cache with a fetcher function
|
|
260
|
+
const userCache = new KeyedAsyncCache(
|
|
261
|
+
async (userId: number) => Result.tryFunction(
|
|
262
|
+
async () => {
|
|
263
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
return data;
|
|
266
|
+
},
|
|
267
|
+
(e) => new ErrorBase("fetch_error", "Error on fetch", e)
|
|
268
|
+
),
|
|
269
|
+
(userId) => `user-${userId}`, // Key generator
|
|
270
|
+
60000 // TTL: 60 seconds
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Get cached or fetch
|
|
274
|
+
const userResult = userCache.get(123); // Returns AsyncResult
|
|
275
|
+
|
|
276
|
+
// Get with refetch policy
|
|
277
|
+
const freshUser = userCache.get(123, { policy: 'refetch' });
|
|
278
|
+
const errorRetry = userCache.get(456, { policy: 'if-error' });
|
|
279
|
+
|
|
280
|
+
// Check if any request is loading
|
|
281
|
+
if (userCache.anyLoading()) {
|
|
282
|
+
console.log('Loading data...');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Invalidate cache
|
|
286
|
+
userCache.invalidateParams(123);
|
|
287
|
+
userCache.invalidateAll();
|
|
288
|
+
userCache.clear();
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### `ErrorBase`
|
|
292
|
+
|
|
293
|
+
A structured error class that provides consistent error handling with codes, messages, and automatic logging.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { ErrorBase } from 'unwrapped/core';
|
|
297
|
+
|
|
298
|
+
// Create an error
|
|
299
|
+
const error = new ErrorBase(
|
|
300
|
+
'VALIDATION_ERROR',
|
|
301
|
+
'Email address is invalid',
|
|
302
|
+
originalError, // Optional: the caught error
|
|
303
|
+
true // Optional: whether to log immediately (default: true)
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// Access error properties
|
|
307
|
+
console.log(error.code); // 'VALIDATION_ERROR'
|
|
308
|
+
console.log(error.message); // 'Email address is invalid'
|
|
309
|
+
console.log(error.toString()); // 'Error VALIDATION_ERROR: Email address is invalid'
|
|
310
|
+
|
|
311
|
+
// Log the error
|
|
312
|
+
error.logError();
|
|
313
|
+
|
|
314
|
+
// Use with Result
|
|
315
|
+
const result = Result.err(error);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Vue Integration
|
|
319
|
+
|
|
320
|
+
The Vue package provides composables and components for seamless integration with Vue 3's reactivity system.
|
|
321
|
+
|
|
322
|
+
### Composables
|
|
323
|
+
|
|
324
|
+
#### `useAsyncResultRef(asyncResult)`
|
|
325
|
+
|
|
326
|
+
Makes an `AsyncResult` reactive by wrapping it in a Vue ref:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { AsyncResult } from 'unwrapped/core';
|
|
330
|
+
import { useAsyncResultRef } from 'unwrapped/vue';
|
|
331
|
+
|
|
332
|
+
const asyncResult = AsyncResult.fromValuePromise(fetch('/api/data').then(r => r.json()));
|
|
333
|
+
const resultRef = useAsyncResultRef(asyncResult);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
```vue
|
|
337
|
+
<template>
|
|
338
|
+
<div v-if="resultRef.isLoading()">Loading...</div>
|
|
339
|
+
<div v-else-if="resultRef.isSuccess()">
|
|
340
|
+
Data: {{ resultRef.unwrapOrNull() }}
|
|
341
|
+
</div>
|
|
342
|
+
<div v-else-if="resultRef.isError()">
|
|
343
|
+
Error: {{ resultRef.state.error.message }}
|
|
344
|
+
</div>
|
|
345
|
+
</template>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### `useAction(action)`
|
|
349
|
+
|
|
350
|
+
Executes an action immediately and returns a reactive AsyncResult:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { useAction } from 'unwrapped/vue';
|
|
354
|
+
import { Result } from 'unwrapped/core';
|
|
355
|
+
|
|
356
|
+
const resultRef = useAction(async () => Result.tryFunction(
|
|
357
|
+
async () => {
|
|
358
|
+
const response = await fetch('/api/data');
|
|
359
|
+
const data = await response.json();
|
|
360
|
+
return data;
|
|
361
|
+
},
|
|
362
|
+
(e) => new ErrorBase("fetch_error", "Error on fetch", e)
|
|
363
|
+
));
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### `useLazyAction(action)`
|
|
368
|
+
|
|
369
|
+
Creates a lazy action that can be triggered manually:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import { useLazyAction } from 'unwrapped/vue';
|
|
373
|
+
import { Result } from 'unwrapped/core';
|
|
374
|
+
|
|
375
|
+
const { resultRef, trigger } = useLazyAction(async () => Result.tryFunction(
|
|
376
|
+
async () => {
|
|
377
|
+
const response = await fetch('/api/data');
|
|
378
|
+
const data = await response.json();
|
|
379
|
+
return data;
|
|
380
|
+
},
|
|
381
|
+
(e) => new ErrorBase("fetch_error", "Error on fetch", e)
|
|
382
|
+
));
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```vue
|
|
386
|
+
<template>
|
|
387
|
+
<button @click="trigger">Load Data</button>
|
|
388
|
+
<div v-if="resultRef.isLoading()">Loading...</div>
|
|
389
|
+
<div v-else-if="resultRef.isSuccess()">{{ resultRef.unwrapOrNull() }}</div>
|
|
390
|
+
</template>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
#### `useReactiveChain(source, pipe, options)`
|
|
394
|
+
|
|
395
|
+
Creates a reactive pipeline that automatically updates when the source changes:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import { ref } from 'vue';
|
|
399
|
+
import { useReactiveChain } from 'unwrapped/vue';
|
|
400
|
+
import { AsyncResult, Result } from 'unwrapped/core';
|
|
401
|
+
|
|
402
|
+
const userId = ref(1);
|
|
403
|
+
|
|
404
|
+
const userResultRef = useReactiveChain(
|
|
405
|
+
() => userId.value,
|
|
406
|
+
(id) => AsyncResult.fromValuePromise(
|
|
407
|
+
fetch(`/api/users/${id}`).then(r => r.json())
|
|
408
|
+
),
|
|
409
|
+
{ immediate: true }
|
|
410
|
+
);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### `useGenerator(generatorFunc)` / `useLazyGenerator(generatorFunc)`
|
|
414
|
+
|
|
415
|
+
Run generator functions with reactive AsyncResults:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { useGenerator } from 'unwrapped/vue';
|
|
419
|
+
import { AsyncResult } from 'unwrapped/core';
|
|
420
|
+
|
|
421
|
+
const resultRef = useGenerator(function* () {
|
|
422
|
+
const user = yield* AsyncResult.fromValuePromise(fetchUser());
|
|
423
|
+
const posts = yield* AsyncResult.fromValuePromise(fetchPosts(user.id));
|
|
424
|
+
return { user, posts };
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### `useReactiveGenerator(source, generatorFunc, options)`
|
|
429
|
+
|
|
430
|
+
Reactive generator that reruns when source changes:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { ref } from 'vue';
|
|
434
|
+
import { useReactiveGenerator } from 'unwrapped/vue';
|
|
435
|
+
import { AsyncResult } from 'unwrapped/core';
|
|
436
|
+
|
|
437
|
+
const searchQuery = ref('');
|
|
438
|
+
|
|
439
|
+
const resultsRef = useReactiveGenerator(
|
|
440
|
+
() => searchQuery.value,
|
|
441
|
+
function* (query) {
|
|
442
|
+
if (!query) return [];
|
|
443
|
+
|
|
444
|
+
const results = yield* AsyncResult.fromValuePromise(
|
|
445
|
+
fetch(`/api/search?q=${query}`).then(r => r.json())
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
return results;
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Components
|
|
454
|
+
|
|
455
|
+
#### `<AsyncResultLoader>`
|
|
456
|
+
|
|
457
|
+
A component that renders different content based on AsyncResult state:
|
|
458
|
+
|
|
459
|
+
```vue
|
|
460
|
+
<template>
|
|
461
|
+
<AsyncResultLoader :result="dataResult">
|
|
462
|
+
<template #loading>
|
|
463
|
+
<div class="spinner">Loading...</div>
|
|
464
|
+
</template>
|
|
465
|
+
|
|
466
|
+
<template #error="{ error }">
|
|
467
|
+
<div class="error-message">
|
|
468
|
+
Error {{ error.code }}: {{ error.message }}
|
|
469
|
+
</div>
|
|
470
|
+
</template>
|
|
471
|
+
|
|
472
|
+
<template #default="{ value }">
|
|
473
|
+
<div class="data">{{ value }}</div>
|
|
474
|
+
</template>
|
|
475
|
+
|
|
476
|
+
<template #idle>
|
|
477
|
+
<div>Click the button to load data</div>
|
|
478
|
+
</template>
|
|
479
|
+
</AsyncResultLoader>
|
|
480
|
+
</template>
|
|
481
|
+
|
|
482
|
+
<script setup>
|
|
483
|
+
import { AsyncResultLoader } from 'unwrapped/vue';
|
|
484
|
+
import { useLazyAction } from 'unwrapped/vue';
|
|
485
|
+
import { Result } from 'unwrapped/core';
|
|
486
|
+
|
|
487
|
+
const { resultRef: dataResult, trigger: loadData } = useLazyAction(async () => {
|
|
488
|
+
const response = await fetch('/api/data');
|
|
489
|
+
const data = await response.json();
|
|
490
|
+
return Result.ok(data);
|
|
491
|
+
});
|
|
492
|
+
</script>
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
#### `buildCustomAsyncResultLoader(slots)`
|
|
496
|
+
|
|
497
|
+
Create reusable loaders with consistent loading and error UI:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import { buildCustomAsyncResultLoader } from 'unwrapped/vue';
|
|
501
|
+
import { h } from 'vue';
|
|
502
|
+
import Spinner from './Spinner.vue';
|
|
503
|
+
import ErrorAlert from './ErrorAlert.vue';
|
|
504
|
+
|
|
505
|
+
export const CustomLoader = buildCustomAsyncResultLoader({
|
|
506
|
+
loading: () => h(Spinner),
|
|
507
|
+
error: ({ error }) => h(ErrorAlert, { error })
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
```vue
|
|
512
|
+
<template>
|
|
513
|
+
<CustomLoader :result="myAsyncResult">
|
|
514
|
+
<template #default="{ value }">
|
|
515
|
+
<!-- Your success content -->
|
|
516
|
+
<div>{{ value }}</div>
|
|
517
|
+
</template>
|
|
518
|
+
</CustomLoader>
|
|
519
|
+
</template>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## Real-World Examples
|
|
523
|
+
|
|
524
|
+
TODO
|
|
525
|
+
|
|
526
|
+
## Why Unwrapped ?
|
|
527
|
+
|
|
528
|
+
Todo
|
|
529
|
+
|
|
530
|
+
### Why not Effect ?
|
|
531
|
+
todo
|
|
532
|
+
|
|
533
|
+
## API Reference
|
|
534
|
+
|
|
535
|
+
### Core Module (`unwrapped/core`)
|
|
536
|
+
|
|
537
|
+
- **`Result<T, E>`**: Synchronous result type
|
|
538
|
+
- **`AsyncResult<T, E>`**: Asynchronous result with state tracking
|
|
539
|
+
- **`ErrorBase`**: Base error class with structured logging
|
|
540
|
+
- **`KeyedAsyncCache<P, V, E>`**: Cache for async operations
|
|
541
|
+
|
|
542
|
+
### Vue Module (`unwrapped/vue`)
|
|
543
|
+
|
|
544
|
+
- **Composables**: `useAsyncResultRef`, `useAction`, `useLazyAction`, `useReactiveChain`, `useGenerator`, `useLazyGenerator`, `useReactiveGenerator`
|
|
545
|
+
- **Components**: `AsyncResultLoader`, `buildCustomAsyncResultLoader`
|
|
546
|
+
|
|
547
|
+
## License
|
|
548
|
+
|
|
549
|
+
LGPL-3.0-or-later
|
|
550
|
+
|
|
551
|
+
## Contributing
|
|
552
|
+
|
|
553
|
+
Contributions are welcome! Please feel free to submit issues or pull requests.
|