This repository has been archived by the owner on Aug 1, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 51
/
remote.ts
142 lines (123 loc) · 4.25 KB
/
remote.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Copyright © 2021 The Radicle Upstream Contributors
//
// This file is part of radicle-upstream, distributed under the GPLv3
// with Radicle Linking Exception. For full terms see the included
// LICENSE file.
import { derived, get, writable, Readable } from "svelte/store";
import * as error from "./error";
export enum Status {
NotAsked = "NOT_ASKED",
Loading = "LOADING",
Error = "ERROR",
Success = "SUCCESS",
}
export type Data<T> =
| { status: Status.NotAsked }
| { status: Status.Loading }
| { status: Status.Success; data: T }
| { status: Status.Error; error: error.Error };
// Shorthand for casting these states
export type ErrorState = { status: Status.Error; error: Error };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type SuccessState = { status: Status.Success; data: any };
export const is = <T>(data: Data<T>, status: Status): boolean => {
return data.status === status;
};
// A Store is a typesafe svelte readable store that exposes `updateStatus`
// and `update`. It's like a Writable but it can't be externally `set`, and
// it only accepts data that conforms to the `RemoteData` interface
//
// a Readable store of Remote Data based on type T
export interface Store<T> extends Readable<Data<T>> {
is: (status: Status) => boolean;
loading: () => void;
success: (response: T) => void;
error: (error: error.Error) => void;
readable: Readable<Data<T>>;
// Try and unwrap the underlying store's data value.
// Returns the data of type T if the store's data
// is in `SuccessState`, undefined otherwise.
unwrap: () => T | undefined;
start: (start: StartStopNotifier<Data<T>>) => void;
reset: () => void;
}
// We should only be updating in this direction: NotAsked => Loading, Loading -> Success | Error
type UpdateableStatus = Status.Loading | Status.Success | Status.Error;
interface Update<T> {
(status: Status.Loading): void;
(status: Status.Success, payload: T): void;
(status: Status.Error, payload: error.Error): void;
}
declare type Subscriber<T> = (value: T) => void;
declare type Unsubscriber = () => void;
declare type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void;
// TODO(sos): add @param docs here, consider making generic type T required
export const createStore = <T>(): Store<T> => {
let starter: StartStopNotifier<Data<T>> | null;
const initialState = { status: Status.NotAsked } as Data<T>;
const internalStore = writable(initialState, set => {
if (starter) {
return starter(set);
} else {
return () => {
set({ status: Status.NotAsked });
};
}
});
// eslint-disable-next-line @typescript-eslint/unbound-method
const { subscribe, update } = internalStore;
const updateInternalStore: Update<T> = (
status: UpdateableStatus,
payload?: T | error.Error
) => {
let val: Data<T>;
switch (status) {
case Status.Loading:
val = { status: Status.Loading };
break;
case Status.Success:
val = { status: Status.Success, data: payload as T };
break;
case Status.Error: {
val = { status: Status.Error, error: payload as error.Error };
break;
}
}
update(() => {
return val;
});
};
const resetInternalStore = () => update(() => ({ status: Status.NotAsked }));
const is = (status: Status): boolean => {
return get(internalStore).status === status;
};
const unwrap = (): T | undefined => {
return is(Status.Success)
? (get(internalStore) as SuccessState).data
: undefined;
};
return {
is,
subscribe,
success: (response: T): void =>
updateInternalStore(Status.Success, response),
loading: (): void => updateInternalStore(Status.Loading),
error: (err: error.Error): void => {
if (err.code !== error.Code.RequestAbortError) {
updateInternalStore(Status.Error, err);
}
},
readable: derived(internalStore, $store => $store),
unwrap,
start: (start: StartStopNotifier<Data<T>>): void => {
starter = start;
},
reset: resetInternalStore,
};
};
export const fetch = <T>(store: Store<T>, req: Promise<T>): void => {
if (get(store).status === Status.NotAsked) {
store.loading();
}
req.then(store.success).catch(err => store.error(error.fromUnknown(err)));
};