unwrapped 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ on:
8
8
  concurrency: ${{ github.workflow }}-${{ github.ref }}
9
9
 
10
10
  permissions:
11
+ id-token: write # Required for OIDC
11
12
  contents: write
12
13
  pull-requests: write
13
14
 
@@ -31,5 +32,5 @@ jobs:
31
32
  with:
32
33
  publish: npm run release
33
34
  env:
34
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35
+ GITHUB_TOKEN: ${{ secrets.PUBLISH_GITHUB_SECRET }}
35
36
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # unwrapped
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 78b1202: Configured build for core and vue package, and added the existing first implementation
8
+
9
+ ### Patch Changes
10
+
11
+ - c2a6144: Fixed linting script
package/package.json CHANGED
@@ -1,21 +1,45 @@
1
1
  {
2
2
  "name": "unwrapped",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "license": "MIT",
8
- "main": "dist/index.js",
9
- "module": "dist/index.mjs",
10
- "types": "dist/index.d.ts",
8
+ "main": "./dist/core/index.js",
9
+ "module": "./dist/core/index.mjs",
10
+ "types": "./dist/core/index.d.ts",
11
+ "exports": {
12
+ "./core": {
13
+ "types": "./dist/core/index.d.ts",
14
+ "import": "./dist/core/index.mjs",
15
+ "require": "./dist/core/index.js"
16
+ },
17
+ "./vue": {
18
+ "types": "./dist/vue/index.d.ts",
19
+ "import": "./dist/vue/index.mjs",
20
+ "require": "./dist/vue/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
11
24
  "scripts": {
12
- "build": "tsup index.ts --format cjs,esm --dts",
25
+ "build": "npm run build:core && npm run build:vue",
26
+ "build:core": "tsup --config tsup.config.core.ts",
27
+ "build:vue": "tsup --config tsup.config.vue.ts",
13
28
  "release": "npm run build && changeset publish",
14
29
  "lint": "tsc"
15
30
  },
16
31
  "devDependencies": {
17
32
  "@changesets/cli": "^2.29.7",
18
33
  "tsup": "^8.5.0",
19
- "typescript": "^5.9.3"
34
+ "typescript": "^5.9.3",
35
+ "vue": "^3.5.24"
36
+ },
37
+ "peerDependencies": {
38
+ "vue": "^3.0.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "vue": {
42
+ "optional": true
43
+ }
20
44
  }
21
- }
45
+ }
@@ -0,0 +1,312 @@
1
+ import type { ErrorBase } from "./error";
2
+ import { Result, type ResultState } from "./result";
3
+
4
+ export type AsyncResultState<T, E> =
5
+ | { status: 'idle' }
6
+ | { status: 'loading'; promise: Promise<Result<T, E>> }
7
+ | ResultState<T, E>;
8
+
9
+ export type ChainFunction<I, O, E> = (input: I) => Result<O, E> | Promise<Result<O, E>>;
10
+ export type FlatChainFunction<I, O, E> = (input: I) => AsyncResult<O, E>;
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ export type AsyncResultListener<T, E> = (result: AsyncResult<T, E>) => any;
14
+
15
+ export class AsyncResult<T, E = ErrorBase> {
16
+ private _state: AsyncResultState<T, E>;
17
+ private _listeners: Set<AsyncResultListener<T, E>> = new Set();
18
+
19
+ constructor(state?: AsyncResultState<T, E>) {
20
+ this._state = state || { status: 'idle' };
21
+ }
22
+
23
+ get state() {
24
+ return this._state;
25
+ }
26
+
27
+ private set state(newState: AsyncResultState<T, E>) {
28
+ this._state = newState;
29
+ this._listeners.forEach((listener) => listener(this));
30
+ }
31
+
32
+ static ok<T>(value: T): AsyncResult<T, never> {
33
+ return new AsyncResult<T, never>({ status: 'success', value });
34
+ }
35
+
36
+ static err<E>(error: E): AsyncResult<never, E> {
37
+ return new AsyncResult<never, E>({ status: 'error', error });
38
+ }
39
+
40
+ isSuccess() {
41
+ return this._state.status === 'success';
42
+ }
43
+
44
+ isError() {
45
+ return this._state.status === 'error';
46
+ }
47
+
48
+ isLoading() {
49
+ return this._state.status === 'loading';
50
+ }
51
+
52
+ listen(listener: AsyncResultListener<T, E>, immediate = true) {
53
+ this._listeners.add(listener);
54
+ if (immediate) {
55
+ listener(this);
56
+ }
57
+
58
+ return () => {
59
+ this._listeners.delete(listener);
60
+ };
61
+ }
62
+
63
+ listenUntilSettled(listener: AsyncResultListener<T, E>, immediate = true) {
64
+ const unsub = this.listen((result) => {
65
+ listener(result);
66
+ if (result.state.status === 'success' || result.state.status === 'error') {
67
+ unsub();
68
+ }
69
+ }, immediate);
70
+
71
+ return unsub;
72
+ }
73
+
74
+ setState(newState: AsyncResultState<T, E>) {
75
+ this.state = newState;
76
+ }
77
+
78
+ copyOnceSettled(other: AsyncResult<T, E>) {
79
+ this.updateFromResultPromise(other.toResultPromise());
80
+ }
81
+
82
+ update(newState: AsyncResultState<T, E>) {
83
+ this.state = newState;
84
+ }
85
+
86
+ updateToValue(value: T) {
87
+ this.state = { status: 'success', value };
88
+ }
89
+
90
+ updateToError(error: E) {
91
+ this.state = { status: 'error', error };
92
+ }
93
+
94
+ updateFromResultPromise(promise: Promise<Result<T, E>>) {
95
+ this.state = { status: 'loading', promise };
96
+ promise
97
+ .then((res) => {
98
+ this.state = res.state;
99
+ })
100
+ .catch((error) => {
101
+ this.state = { status: 'error', error };
102
+ });
103
+ }
104
+
105
+ static fromResultPromise<T, E>(promise: Promise<Result<T, E>>): AsyncResult<T, E> {
106
+ const result = new AsyncResult<T, E>();
107
+ result.updateFromResultPromise(promise);
108
+ return result;
109
+ }
110
+
111
+ updateFromValuePromise(promise: Promise<T>) {
112
+ const resultStatePromise = async (): Promise<Result<T, E>> => {
113
+ try {
114
+ const value = await promise;
115
+ return Result.ok(value);
116
+ } catch (error) {
117
+ return Result.err(error as E);
118
+ }
119
+ };
120
+ this.updateFromResultPromise(resultStatePromise());
121
+ }
122
+
123
+ static fromValuePromise<T, E>(promise: Promise<T>): AsyncResult<T, E> {
124
+ const result = new AsyncResult<T, E>();
125
+ result.updateFromValuePromise(promise);
126
+ return result;
127
+ }
128
+
129
+ async waitForSettled(): Promise<AsyncResult<T, E>> {
130
+ if (this._state.status === 'loading') {
131
+ try {
132
+ const value = await this._state.promise;
133
+ this._state = value.state;
134
+ } catch (error) {
135
+ this._state = { status: 'error', error: error as E };
136
+ }
137
+ }
138
+ return this;
139
+ }
140
+
141
+ async waitForSettledResult(): Promise<Result<T, E>> {
142
+ const settled = await this.waitForSettled();
143
+ if (settled.state.status === 'idle' || settled.state.status === 'loading') {
144
+ throw new Error('Cannot convert idle or loading AsyncResult to ResultState');
145
+ }
146
+ if (settled.state.status === 'error') {
147
+ return Result.err(settled.state.error);
148
+ }
149
+ return Result.ok(settled.state.value);
150
+ }
151
+
152
+ async toResultPromise(): Promise<Result<T, E>> {
153
+ if (this._state.status === 'idle') {
154
+ throw new Error('Cannot convert idle AsyncResult to ResultState');
155
+ }
156
+ if (this._state.status === 'loading') {
157
+ try {
158
+ const value = await this._state.promise;
159
+ this._state = value.state;
160
+ } catch (error) {
161
+ this._state = { status: 'error', error: error as E };
162
+ }
163
+ }
164
+ return new Result(this._state);
165
+ }
166
+
167
+ async toValuePromiseThrow(): Promise<T> {
168
+ const settled = await this.waitForSettled();
169
+ return settled.unwrapOrThrow();
170
+ }
171
+
172
+ async toValueOrNullPromise(): Promise<T | null> {
173
+ const settled = await this.waitForSettled();
174
+ return settled.unwrapOrNull();
175
+ }
176
+
177
+ unwrapOrNull(): T | null {
178
+ if (this._state.status === 'success') {
179
+ return this._state.value;
180
+ }
181
+ return null;
182
+ }
183
+
184
+ async unwrapOrNullOnceSettled(): Promise<T | null> {
185
+ return (await this.waitForSettled()).unwrapOrNull();
186
+ }
187
+
188
+ unwrapOrThrow(): T {
189
+ if (this._state.status === 'success') {
190
+ return this._state.value;
191
+ }
192
+ throw new Error('Tried to unwrap an AsyncResult that is not successful');
193
+ }
194
+
195
+ async unwrapOrThrowOnceSettled(): Promise<T> {
196
+ return (await this.waitForSettled()).unwrapOrThrow();
197
+ }
198
+
199
+ chain<O, E2>(fn: ChainFunction<T, O, E | E2>): AsyncResult<O, E | E2> {
200
+ const newResultBuilder = async (): Promise<Result<O, E | E2>> => {
201
+ const settled = await this.waitForSettled();
202
+ if (settled.state.status === 'loading' || settled.state.status === 'idle') {
203
+ throw new Error('Unexpected state after waitForSettled'); // TODO handle this case properly
204
+ }
205
+ if (settled.state.status === 'error') {
206
+ return Result.err(settled.state.error);
207
+ }
208
+
209
+ return fn(settled.state.value);
210
+ };
211
+
212
+ return AsyncResult.fromResultPromise<O, E | E2>(newResultBuilder());
213
+ }
214
+
215
+ flatChain<O, E2>(fn: FlatChainFunction<T, O, E | E2>): AsyncResult<O, E | E2> {
216
+ const newResultBuilder = async (): Promise<Result<O, E | E2>> => {
217
+ const settled = await this.waitForSettledResult();
218
+ if (settled.state.status === 'error') {
219
+ return Result.err(settled.state.error);
220
+ }
221
+
222
+ const nextAsyncResult = fn(settled.state.value);
223
+ const nextSettled = await nextAsyncResult.waitForSettledResult();
224
+ return nextSettled;
225
+ }
226
+
227
+ return AsyncResult.fromResultPromise<O, E | E2>(newResultBuilder());
228
+ }
229
+
230
+ // pipeParallel PipeFunction[] -> AsyncResult<T, E>[]
231
+ // pipeParallelAndCollapse PipeFunction[] -> AsyncResult<T[], E>
232
+
233
+ mirror(other: AsyncResult<T, E>) {
234
+ return other.listen((newState) => {
235
+ this.setState(newState.state);
236
+ }, true);
237
+ }
238
+
239
+ mirrorUntilSettled(other: AsyncResult<T, E>) {
240
+ return other.listenUntilSettled((newState) => {
241
+ this.setState(newState.state);
242
+ }, true);
243
+ }
244
+
245
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
+ static ensureAvailable<R extends readonly AsyncResult<any, any>[]>(results: R): AsyncResult<{ [K in keyof R]: R[K] extends AsyncResult<infer T, any> ? T : never }, R[number] extends AsyncResult<any, infer E> ? E : never> {
247
+ if (results.length === 0) {
248
+ // empty case — TS infers void tuple, so handle gracefully
249
+ return AsyncResult.ok(undefined as never);
250
+ }
251
+
252
+ const promise = Promise.all(results.map((r) => r.waitForSettled())).then(
253
+ (settledResults) => {
254
+ for (const res of settledResults) {
255
+ if (res.state.status === 'error') {
256
+ return Result.err(res.state.error);
257
+ }
258
+ }
259
+
260
+ const values = settledResults.map((r) => r.unwrapOrNull()!) as {
261
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
+ [K in keyof R]: R[K] extends AsyncResult<infer T, any>
263
+ ? T
264
+ : never;
265
+ };
266
+
267
+ return Result.ok(values);
268
+ }
269
+ );
270
+
271
+ return AsyncResult.fromResultPromise(promise);
272
+ }
273
+
274
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
+ *[Symbol.iterator](): Generator<AsyncResult<T, E>, T, any> {
276
+ yield this;
277
+
278
+ if (this._state.status === 'success') {
279
+ return this._state.value;
280
+ }
281
+ return undefined as T;
282
+ }
283
+
284
+ private static _runGeneratorProcessor<T, E>(iterator: Generator<AsyncResult<any, any>, T, any>): () => Promise<Result<T, E>> {
285
+ return async (): Promise<Result<T, E>> => {
286
+ let result = iterator.next();
287
+
288
+ while (!result.done) {
289
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
290
+ const yielded = result.value as AsyncResult<any, E>;
291
+ const settled = await yielded.waitForSettledResult();
292
+ if (settled.state.status === 'error') {
293
+ return Result.err(settled.state.error);
294
+ }
295
+ result = iterator.next(settled.state.value);
296
+ }
297
+
298
+ return Result.ok(result.value);
299
+ }
300
+ }
301
+
302
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
303
+ static run<T, E = ErrorBase>(generatorFunc: () => Generator<AsyncResult<any, any>, T, any>): AsyncResult<T, E> {
304
+ const iterator = generatorFunc();
305
+ return AsyncResult.fromResultPromise<T, E>(AsyncResult._runGeneratorProcessor<T, E>(iterator)());
306
+ }
307
+
308
+ runInPlace(generatorFunc: () => Generator<AsyncResult<any, any>, T, any>) {
309
+ const iterator = generatorFunc();
310
+ this.updateFromResultPromise(AsyncResult._runGeneratorProcessor<T, E>(iterator)());
311
+ }
312
+ }
@@ -0,0 +1,81 @@
1
+ import { AsyncResult, type AsyncResultState, type ChainFunction } from "./asyncResult";
2
+ import type { ErrorBase } from "./error";
3
+
4
+ type KeyedAsyncCacheRefetchOptions = {
5
+ policy: 'refetch' | 'if-error' | 'no-refetch';
6
+ };
7
+
8
+ type CacheItem<P, V, E> = {
9
+ result: AsyncResult<V, E>;
10
+ fetcherParams: P;
11
+ };
12
+
13
+ const _defaultRefetchOptions: KeyedAsyncCacheRefetchOptions = { policy: 'no-refetch' };
14
+
15
+ function defaultParamsToKey<P>(params: P): string {
16
+ if (typeof params === 'object') {
17
+ return JSON.stringify(params);
18
+ }
19
+ return String(params);
20
+ }
21
+
22
+ export class KeyedAsyncCache<P, V, E = ErrorBase> {
23
+ private _cache: Map<string, CacheItem<P, V, E>> = new Map();
24
+ private _fetcher: ChainFunction<P, V, E>;
25
+ private _paramsToKey: (params: P) => string;
26
+
27
+ constructor(fetcher: ChainFunction<P, V, E>, paramsToKey: (params: P) => string = defaultParamsToKey) {
28
+ this._fetcher = fetcher;
29
+ this._paramsToKey = paramsToKey;
30
+ }
31
+
32
+ private makeCacheItem(result: AsyncResult<V, E>, fetcherParams: P): CacheItem<P, V, E> {
33
+ return { result, fetcherParams };
34
+ }
35
+
36
+ private shouldRefetch(existingResult: CacheItem<P, V, E>, refetch: KeyedAsyncCacheRefetchOptions): boolean {
37
+ if (refetch.policy === 'refetch') {
38
+ return true;
39
+ }
40
+ if (refetch.policy === 'if-error') {
41
+ return existingResult.result.state.status === 'error';
42
+ }
43
+ return false;
44
+ }
45
+
46
+ get(params: P, refetch: KeyedAsyncCacheRefetchOptions = _defaultRefetchOptions): AsyncResult<V, E> {
47
+ const key = this._paramsToKey(params);
48
+ if (this._cache.has(key)) {
49
+ const cacheItem = this._cache.get(key)!;
50
+ if (!this.shouldRefetch(cacheItem, refetch)) {
51
+ return cacheItem.result;
52
+ } else {
53
+ cacheItem.result.updateFromResultPromise(Promise.resolve(this._fetcher(cacheItem.fetcherParams)));
54
+ return cacheItem.result;
55
+ }
56
+ }
57
+
58
+ const asyncResult = AsyncResult.fromResultPromise(Promise.resolve(this._fetcher(params)));
59
+ this._cache.set(key, this.makeCacheItem(asyncResult, params));
60
+ return asyncResult;
61
+ }
62
+
63
+ async getSettledState(params: P, refetch: KeyedAsyncCacheRefetchOptions = _defaultRefetchOptions): Promise<AsyncResultState<V, E>> {
64
+ const asyncResult = this.get(params, refetch);
65
+ await asyncResult.waitForSettled();
66
+ return asyncResult.state;
67
+ }
68
+
69
+ anyLoading(): boolean {
70
+ for (const cacheItem of this._cache.values()) {
71
+ if (cacheItem.result.isLoading()) {
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+
78
+ clear() {
79
+ this._cache.clear();
80
+ }
81
+ }
@@ -0,0 +1,23 @@
1
+ export class ErrorBase {
2
+ code: string;
3
+ message?: string | undefined;
4
+ thrownError?: any;
5
+
6
+ constructor(code: string, message?: string, thrownError?: any, log: boolean = true) {
7
+ this.code = code;
8
+ this.message = message;
9
+ this.thrownError = thrownError;
10
+
11
+ if (log) {
12
+ this.logError();
13
+ }
14
+ }
15
+
16
+ toString(): string {
17
+ return `Error ${this.code}: ${this.message ?? ''}`;
18
+ }
19
+
20
+ logError(): void {
21
+ console.error(this.toString(), this.thrownError);
22
+ }
23
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./error";
2
+ export * from "./result";
3
+ export * from "./asyncResult";
4
+ export * from "./cache";
@@ -0,0 +1,101 @@
1
+ import { ErrorBase } from "./error";
2
+
3
+ export type ResultState<T, E = ErrorBase> =
4
+ | { status: 'success'; value: T }
5
+ | { status: 'error'; error: E };
6
+
7
+
8
+ export class Result<T, E = ErrorBase> {
9
+ private _state: ResultState<T, E>;
10
+
11
+ constructor(state: ResultState<T, E>) {
12
+ this._state = state;
13
+ }
14
+
15
+ get state() {
16
+ return this._state;
17
+ }
18
+
19
+ static ok<T, E = ErrorBase>(value: T): Result<T, E> {
20
+ return new Result({ status: 'success', value });
21
+ }
22
+
23
+ static err<E>(error: E): Result<never, E> {
24
+ return new Result({ status: 'error', error });
25
+ }
26
+
27
+ static errTag(code: string, message?: string): Result<never, ErrorBase> {
28
+ return Result.err(new ErrorBase(code, message));
29
+ }
30
+
31
+ unwrapOrNull(): T | null {
32
+ if (this._state.status === 'success') {
33
+ return this._state.value;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ unwrapOrThrow(): T {
39
+ if (this._state.status === 'success') {
40
+ return this._state.value;
41
+ }
42
+ throw new Error('Tried to unwrap a Result that is not successful');
43
+ }
44
+
45
+ unwrapOr<O>(defaultValue: O): T | O {
46
+ if (this._state.status === 'success') {
47
+ return this._state.value;
48
+ }
49
+ return defaultValue;
50
+ }
51
+
52
+ isSuccess(): boolean {
53
+ return this._state.status === 'success';
54
+ }
55
+
56
+ isError(): boolean {
57
+ return this._state.status === 'error';
58
+ }
59
+
60
+ static tryPromise<T, E>(promise: Promise<T>, errorMapper: (error: unknown) => E): Promise<Result<T, E>> {
61
+ return promise
62
+ .then((value) => Result.ok<T, E>(value))
63
+ .catch((error) => Result.err(errorMapper(error)));
64
+ }
65
+
66
+ static tryFunction<T, E extends ErrorBase = ErrorBase>(fn: () => Promise<T>, errorMapper: (error: unknown) => E): Promise<Result<T, E>> {
67
+ return Result.tryPromise(fn(), errorMapper);
68
+ }
69
+
70
+ chain<O, E2>(fn: (input: T) => ResultState<O, E | E2>): Result<O, E | E2> {
71
+ if (this._state.status === 'success') {
72
+ return new Result<O, E | E2>(fn(this._state.value));
73
+ }
74
+ return Result.err<E>(this._state.error);
75
+ }
76
+
77
+ *[Symbol.iterator](): Generator<Result<T, E>, T, any> {
78
+ yield this;
79
+
80
+ if (this._state.status === 'success') {
81
+ return this._state.value;
82
+ }
83
+ return undefined as T;
84
+ }
85
+
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ static run<T, E>(generator: () => Generator<Result<any, E>, T, any>): Result<T, E> {
88
+ const iterator = generator();
89
+ let result = iterator.next();
90
+
91
+ while (!result.done) {
92
+ const yielded = result.value;
93
+ if (yielded._state.status === 'error') {
94
+ return Result.err(yielded._state.error);
95
+ }
96
+ result = iterator.next(yielded._state.value);
97
+ }
98
+
99
+ return Result.ok(result.value);
100
+ }
101
+ }
@@ -0,0 +1,99 @@
1
+ import { defineComponent, ref, watch, onUnmounted, h, type VNode } from "vue"
2
+ import type { AsyncResult, AsyncResultState } from "unwrapped/core"
3
+
4
+ export const AsyncResultLoader = defineComponent({
5
+ name: "AsyncResultLoader",
6
+
7
+ props: {
8
+ result: {
9
+ type: Object as () => AsyncResult<unknown, unknown>,
10
+ required: true
11
+ }
12
+ },
13
+
14
+ setup(props, { slots }) {
15
+ const state = ref<AsyncResultState<unknown, unknown>>(props.result.state)
16
+ let unlisten: (() => void) | null = null
17
+
18
+ // Unsubscribe on destroy
19
+ onUnmounted(() => {
20
+ if (unlisten) unlisten()
21
+ })
22
+
23
+ // Watch for prop changes & update listener
24
+ watch(
25
+ () => props.result,
26
+ (newResult) => {
27
+ if (unlisten) unlisten()
28
+ state.value = newResult.state
29
+ unlisten = newResult.listen((res) => {
30
+ state.value = res.state
31
+ })
32
+ },
33
+ { immediate: true }
34
+ )
35
+
36
+ return () => {
37
+ const s = state.value
38
+
39
+ // Choose what to render based on status
40
+ switch (s.status) {
41
+ case "loading":
42
+ return slots.loading
43
+ ? slots.loading()
44
+ : h("div", { class: "loading" }, "Loading…")
45
+
46
+ case "error":
47
+ return slots.error
48
+ ? slots.error({ error: s.error })
49
+ : h("div", { class: "error" }, `Error: ${s.error}`)
50
+
51
+ case "success":
52
+ return slots.default
53
+ ? slots.default({ value: s.value })
54
+ : null
55
+
56
+ default:
57
+ // "idle"
58
+ return slots.idle
59
+ ? slots.idle()
60
+ : h("div", { class: "idle" }, "Idle")
61
+ }
62
+ }
63
+ }
64
+ })
65
+
66
+ interface CustomSlots<E> {
67
+ loading?: () => VNode;
68
+ error?: (props: { error: E }) => VNode;
69
+ }
70
+
71
+ export function buildCustomAsyncResultLoader<T, E>(slots: CustomSlots<E>) {
72
+ const comp = defineComponent({
73
+ name: "CustomAsyncResultLoader",
74
+ props: {
75
+ result: {
76
+ type: Object as () => AsyncResult<T, E>,
77
+ required: true
78
+ }
79
+ },
80
+ setup(props, context) {
81
+ return () => {
82
+ const renderLoading = context.slots.loading ?? slots.loading ?? (() => undefined);
83
+ const renderError = context.slots.error ?? slots.error ?? (() => undefined);
84
+ return h(
85
+ AsyncResultLoader,
86
+ { result: props.result },
87
+ {
88
+ default: context.slots.default ? (propsDefault: { value: T }) => context.slots.default!(propsDefault) : undefined,
89
+
90
+ loading: () => renderLoading(),
91
+ error: ((propsError: { error: E }) => renderError(propsError))
92
+ }
93
+ )
94
+ }
95
+ }
96
+ });
97
+
98
+ return comp;
99
+ }
@@ -0,0 +1,106 @@
1
+ import { computed, onUnmounted, ref, toRef, triggerRef, watch, type Ref, type WatchSource } from "vue";
2
+ import { AsyncResult, type FlatChainFunction, type Result } from "unwrapped/core";
3
+
4
+ export function useAsyncResultRef<T, E>(asyncResult: AsyncResult<T, E>) {
5
+ const state = ref<AsyncResult<T, E>>(asyncResult) as Ref<AsyncResult<T, E>>;
6
+
7
+ const unsub = asyncResult.listen(() => {
8
+ triggerRef(state);
9
+ });
10
+
11
+ onUnmounted(() => {
12
+ unsub();
13
+ });
14
+
15
+ return state;
16
+ }
17
+
18
+ export function useReactiveResult<T, E, Inputs>(source: WatchSource<Inputs>, pipe: FlatChainFunction<Inputs, T, E>, options:{ immediate: boolean } = { immediate: true }): Ref<AsyncResult<T, E>> {
19
+ const result = new AsyncResult<T, E>();
20
+ const resultRef = useAsyncResultRef(result);
21
+
22
+ let unsub: (() => void) | null = null;
23
+
24
+ watch(source, (newInputs) => {
25
+ unsub?.();
26
+ unsub = result.mirror(pipe(newInputs));
27
+ }, { immediate: options.immediate });
28
+
29
+ onUnmounted(() => {
30
+ unsub?.();
31
+ });
32
+
33
+ return resultRef;
34
+ }
35
+
36
+ export function useAsyncResultRefFromPromise<T, E>(promise: Promise<Result<T, E>>) {
37
+ return useAsyncResultRef(AsyncResult.fromResultPromise(promise));
38
+ }
39
+
40
+ export type Action<T,E> = () => Promise<Result<T, E>>;
41
+ export function useImmediateAction<T, E>(action: Action<T, E>): Ref<AsyncResult<T, E>> {
42
+ return useAsyncResultRefFromPromise(action());
43
+ }
44
+ export interface LazyActionReturn<T, E> {
45
+ resultRef: Ref<AsyncResult<T, E>>;
46
+ trigger: () => void;
47
+ }
48
+
49
+ export function useLazyAction<T, E>(action: Action<T, E>): LazyActionReturn<T, E> {
50
+ const result = new AsyncResult<T, E>();
51
+ const resultRef = useAsyncResultRef(result);
52
+
53
+ const trigger = () => {
54
+ result.updateFromResultPromise(action());
55
+ }
56
+
57
+ return { resultRef, trigger };
58
+ }
59
+
60
+ export function useReactiveAction<I, O, E>(input: I | Ref<I> | (() => I), pipe: FlatChainFunction<I, O, E>, options:{ immediate: boolean } = { immediate: true }): Ref<AsyncResult<O, E>> {
61
+ const source = typeof input === 'function' ? computed(input as () => I) : toRef(input)
62
+
63
+ const outputRef = ref<AsyncResult<O, E>>(new AsyncResult()) as Ref<AsyncResult<O, E>>;
64
+ let unsub: (() => void) | null = null;
65
+
66
+ watch(source, () => {
67
+ unsub?.();
68
+ const newOutput = pipe(source.value);
69
+ unsub = newOutput.listen((newState) => {
70
+ outputRef.value.setState(newState.state);
71
+ triggerRef(outputRef);
72
+ });
73
+ }, { immediate: options.immediate });
74
+
75
+ onUnmounted(() => {
76
+ unsub?.();
77
+ });
78
+
79
+ return outputRef;
80
+ }
81
+
82
+ export function useGenerator<T>(generatorFunc: () => Generator<AsyncResult<any, any>, T, any>): Ref<AsyncResult<T, any>> {
83
+ const resultRef = useAsyncResultRef(AsyncResult.run(generatorFunc));
84
+ return resultRef;
85
+ }
86
+
87
+ export function useLazyGenerator<T>(generatorFunc: () => Generator<AsyncResult<any, any>, T, any>): { resultRef: Ref<AsyncResult<T, any>>, trigger: () => void } {
88
+ const result = new AsyncResult<T, any>();
89
+ const resultRef = useAsyncResultRef(result);
90
+
91
+ const trigger = () => {
92
+ result.runInPlace(generatorFunc);
93
+ }
94
+
95
+ return { resultRef, trigger };
96
+ }
97
+
98
+ export function useReactiveGenerator<T, E, Inputs>(source: WatchSource<Inputs>, generatorFunc: (args: Inputs) => Generator<AsyncResult<any, any>, T, any>, options:{ immediate: boolean } = { immediate: true }): Ref<AsyncResult<T, E>> {
99
+ const resultRef = useAsyncResultRef(new AsyncResult<T, E>());
100
+
101
+ watch(source, (newInputs) => {
102
+ resultRef.value.runInPlace(() => generatorFunc(newInputs));
103
+ }, { immediate: options.immediate });
104
+
105
+ return resultRef;
106
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./composables"
2
+ export * from "./components/asyncResultLoader"
package/tsconfig.json CHANGED
@@ -17,5 +17,10 @@
17
17
  "noEmit": true,
18
18
  /* If your code runs in the DOM: */
19
19
  "lib": ["es2022", "dom", "dom.iterable"],
20
- }
20
+ "baseUrl": ".",
21
+ "paths": {
22
+ "unwrapped/core": ["./src/core/index.ts"]
23
+ }
24
+ },
25
+ "include": ["src"]
21
26
  }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsup"
2
+
3
+ export default defineConfig([
4
+ {
5
+ entry: ["src/core/index.ts"],
6
+ format: ["cjs", "esm"],
7
+ sourcemap: true,
8
+ dts: true,
9
+ outDir: "dist/core"
10
+ },
11
+ ])
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "tsup"
2
+
3
+ export default defineConfig([
4
+ {
5
+ entry: ["src/vue/index.ts"],
6
+ format: ["cjs", "esm"],
7
+ sourcemap: true,
8
+ dts: true,
9
+ outDir: "dist/vue",
10
+ external: ["vue", "unwrapped/core"],
11
+ platform: "browser"
12
+ }
13
+ ])
package/dist/index.d.mts DELETED
@@ -1,3 +0,0 @@
1
- declare function test(): void;
2
-
3
- export { test };
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- declare function test(): void;
2
-
3
- export { test };
package/dist/index.js DELETED
@@ -1,32 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- test: () => test
24
- });
25
- module.exports = __toCommonJS(index_exports);
26
- function test() {
27
- console.log("test");
28
- }
29
- // Annotate the CommonJS export names for ESM import in node:
30
- 0 && (module.exports = {
31
- test
32
- });
package/dist/index.mjs DELETED
@@ -1,7 +0,0 @@
1
- // index.ts
2
- function test() {
3
- console.log("test");
4
- }
5
- export {
6
- test
7
- };