unwrapped 0.1.2 → 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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # unwrapped
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - fc76494: Docs v1
8
+
3
9
  ## 0.1.2
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -1,2 +1,553 @@
1
- # unwrapped
2
- Library to deal with result types, both synchronous and asynchronous, along with utilities like caches
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.