@zmdb/svelte 1.0.0-beta.1
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 +674 -0
- package/README.md +82 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +22 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +9 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/mutation.d.ts +3 -0
- package/dist/mutation.d.ts.map +1 -0
- package/dist/mutation.js +81 -0
- package/dist/mutation.js.map +1 -0
- package/dist/query.d.ts +4 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +134 -0
- package/dist/query.js.map +1 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
- package/src/context.ts +35 -0
- package/src/errors.ts +8 -0
- package/src/index.ts +13 -0
- package/src/lifecycle.ts +9 -0
- package/src/mutation.ts +92 -0
- package/src/query.ts +153 -0
- package/src/types.ts +45 -0
package/src/mutation.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
import { SvelteAdapterError } from './errors.js';
|
|
4
|
+
import { isAbort, lifecycleAbort } from './lifecycle.js';
|
|
5
|
+
import type { MutationRunner, MutationSnapshot, SvelteMutationStore } from './types.js';
|
|
6
|
+
|
|
7
|
+
interface MutationRequest {
|
|
8
|
+
readonly controller: AbortController;
|
|
9
|
+
readonly epoch: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createMutationStore<Client, Input, Output>(
|
|
13
|
+
client: Client,
|
|
14
|
+
run: MutationRunner<Client, Input, Output>,
|
|
15
|
+
): SvelteMutationStore<Input, Output> {
|
|
16
|
+
let snapshot: MutationSnapshot = Object.freeze({ error: undefined, pending: false });
|
|
17
|
+
const state = writable(snapshot);
|
|
18
|
+
const requests = new Map<number, MutationRequest>();
|
|
19
|
+
let subscribers = 0;
|
|
20
|
+
let destroyed = false;
|
|
21
|
+
let epoch = 0;
|
|
22
|
+
let sequence = 0;
|
|
23
|
+
let latestSequence = 0;
|
|
24
|
+
|
|
25
|
+
const publish = (next: MutationSnapshot): void => {
|
|
26
|
+
snapshot = Object.freeze(next);
|
|
27
|
+
state.set(snapshot);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const pendingInCurrentEpoch = (): boolean => [...requests.values()].some(request => request.epoch === epoch);
|
|
31
|
+
|
|
32
|
+
const cancelActive = (reason: string): void => {
|
|
33
|
+
epoch += 1;
|
|
34
|
+
const abortReason = lifecycleAbort(reason);
|
|
35
|
+
for (const request of requests.values()) {
|
|
36
|
+
if (!request.controller.signal.aborted) request.controller.abort(abortReason);
|
|
37
|
+
}
|
|
38
|
+
if (snapshot.pending) publish({ error: snapshot.error, pending: false });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const store: SvelteMutationStore<Input, Output> = {
|
|
42
|
+
subscribe(runSubscriber, invalidate) {
|
|
43
|
+
subscribers += 1;
|
|
44
|
+
const unsubscribe = state.subscribe(runSubscriber, invalidate);
|
|
45
|
+
let subscribed = true;
|
|
46
|
+
return () => {
|
|
47
|
+
if (!subscribed) return;
|
|
48
|
+
subscribed = false;
|
|
49
|
+
unsubscribe();
|
|
50
|
+
subscribers -= 1;
|
|
51
|
+
if (subscribers === 0 && requests.size > 0) {
|
|
52
|
+
cancelActive('@zmdb/svelte mutation cancelled after its final subscriber left');
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
async mutate(input) {
|
|
57
|
+
if (destroyed) throw new SvelteAdapterError('@zmdb/svelte mutation store is destroyed');
|
|
58
|
+
const selectedEpoch = epoch;
|
|
59
|
+
const selectedSequence = ++sequence;
|
|
60
|
+
latestSequence = selectedSequence;
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
requests.set(selectedSequence, { controller, epoch: selectedEpoch });
|
|
63
|
+
publish({ error: undefined, pending: true });
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return await run(client, input, controller.signal);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (
|
|
69
|
+
!destroyed &&
|
|
70
|
+
selectedEpoch === epoch &&
|
|
71
|
+
selectedSequence === latestSequence &&
|
|
72
|
+
!isAbort(error, controller.signal)
|
|
73
|
+
) {
|
|
74
|
+
publish({ error, pending: pendingInCurrentEpoch() });
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
} finally {
|
|
78
|
+
requests.delete(selectedSequence);
|
|
79
|
+
if (!destroyed && selectedEpoch === epoch) {
|
|
80
|
+
publish({ error: snapshot.error, pending: pendingInCurrentEpoch() });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
destroy() {
|
|
85
|
+
if (destroyed) return;
|
|
86
|
+
destroyed = true;
|
|
87
|
+
cancelActive('@zmdb/svelte mutation cancelled because its owner was destroyed');
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return Object.freeze(store);
|
|
92
|
+
}
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
import type { Readable, Unsubscriber } from 'svelte/store';
|
|
3
|
+
|
|
4
|
+
import { SvelteAdapterError } from './errors.js';
|
|
5
|
+
import { isAbort, lifecycleAbort } from './lifecycle.js';
|
|
6
|
+
import type { QueryLoader, QuerySnapshot, SvelteQueryStore } from './types.js';
|
|
7
|
+
|
|
8
|
+
interface QueryRequest {
|
|
9
|
+
readonly controller: AbortController;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function property(value: unknown, name: PropertyKey): unknown {
|
|
13
|
+
return (typeof value === 'object' && value !== null) || typeof value === 'function'
|
|
14
|
+
? Reflect.get(value, name)
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isReadable<Input>(value: Input | Readable<Input>): value is Readable<Input> {
|
|
19
|
+
return typeof property(value, 'subscribe') === 'function';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createQueryStore<Client, Input, Output>(
|
|
23
|
+
client: Client,
|
|
24
|
+
input: Input | Readable<Input>,
|
|
25
|
+
load: QueryLoader<Client, Input, Output>,
|
|
26
|
+
): SvelteQueryStore<Output> {
|
|
27
|
+
let snapshot: QuerySnapshot<Output> = Object.freeze({
|
|
28
|
+
data: undefined,
|
|
29
|
+
error: undefined,
|
|
30
|
+
loading: false,
|
|
31
|
+
});
|
|
32
|
+
const state = writable(snapshot);
|
|
33
|
+
let subscribers = 0;
|
|
34
|
+
let active = false;
|
|
35
|
+
let destroyed = false;
|
|
36
|
+
let generation = 0;
|
|
37
|
+
let request: QueryRequest | undefined;
|
|
38
|
+
let inputUnsubscribe: Unsubscriber | undefined;
|
|
39
|
+
let currentInput: { readonly value: Input } | undefined;
|
|
40
|
+
|
|
41
|
+
const publish = (next: QuerySnapshot<Output>): void => {
|
|
42
|
+
snapshot = Object.freeze(next);
|
|
43
|
+
state.set(snapshot);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const abortSelected = (reason: Error): void => {
|
|
47
|
+
const selected = request;
|
|
48
|
+
request = undefined;
|
|
49
|
+
if (selected !== undefined && !selected.controller.signal.aborted) selected.controller.abort(reason);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const launch = async (selectedInput: Input, clearData: boolean, reason: string): Promise<void> => {
|
|
53
|
+
if (destroyed) throw new SvelteAdapterError('@zmdb/svelte query store is destroyed');
|
|
54
|
+
if (!active) throw new SvelteAdapterError('@zmdb/svelte query store is inactive; subscribe before refreshing');
|
|
55
|
+
|
|
56
|
+
abortSelected(lifecycleAbort(reason));
|
|
57
|
+
const selectedGeneration = ++generation;
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
request = { controller };
|
|
60
|
+
publish({
|
|
61
|
+
data: clearData ? undefined : snapshot.data,
|
|
62
|
+
error: undefined,
|
|
63
|
+
loading: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const data = await load(client, selectedInput, controller.signal);
|
|
68
|
+
if (!active || destroyed || selectedGeneration !== generation) return;
|
|
69
|
+
request = undefined;
|
|
70
|
+
publish({ data, error: undefined, loading: false });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (active && !destroyed && selectedGeneration === generation) {
|
|
73
|
+
request = undefined;
|
|
74
|
+
if (!isAbort(error, controller.signal)) {
|
|
75
|
+
publish({ data: snapshot.data, error, loading: false });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const startForInput = (selectedInput: Input): void => {
|
|
83
|
+
const changed = currentInput !== undefined && !Object.is(currentInput.value, selectedInput);
|
|
84
|
+
currentInput = { value: selectedInput };
|
|
85
|
+
void launch(
|
|
86
|
+
selectedInput,
|
|
87
|
+
changed,
|
|
88
|
+
changed
|
|
89
|
+
? '@zmdb/svelte query superseded by an input-store change'
|
|
90
|
+
: '@zmdb/svelte query restarted for a new subscription',
|
|
91
|
+
).catch(() => undefined);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const activate = (): void => {
|
|
95
|
+
if (active || destroyed) return;
|
|
96
|
+
active = true;
|
|
97
|
+
if (isReadable(input)) {
|
|
98
|
+
inputUnsubscribe = input.subscribe(startForInput);
|
|
99
|
+
} else {
|
|
100
|
+
startForInput(input);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const deactivate = (reason: string): void => {
|
|
105
|
+
if (!active) return;
|
|
106
|
+
active = false;
|
|
107
|
+
generation += 1;
|
|
108
|
+
inputUnsubscribe?.();
|
|
109
|
+
inputUnsubscribe = undefined;
|
|
110
|
+
abortSelected(lifecycleAbort(reason));
|
|
111
|
+
if (snapshot.loading) publish({ data: snapshot.data, error: snapshot.error, loading: false });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const store: SvelteQueryStore<Output> = {
|
|
115
|
+
subscribe(run, invalidate) {
|
|
116
|
+
if (subscribers === 0) activate();
|
|
117
|
+
subscribers += 1;
|
|
118
|
+
const unsubscribe = state.subscribe(run, invalidate);
|
|
119
|
+
let subscribed = true;
|
|
120
|
+
return () => {
|
|
121
|
+
if (!subscribed) return;
|
|
122
|
+
subscribed = false;
|
|
123
|
+
unsubscribe();
|
|
124
|
+
subscribers -= 1;
|
|
125
|
+
if (subscribers === 0) {
|
|
126
|
+
deactivate('@zmdb/svelte query cancelled after its final subscriber left');
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
refresh() {
|
|
131
|
+
if (!active) {
|
|
132
|
+
return Promise.reject(
|
|
133
|
+
new SvelteAdapterError(
|
|
134
|
+
destroyed
|
|
135
|
+
? '@zmdb/svelte query store is destroyed'
|
|
136
|
+
: '@zmdb/svelte query store is inactive; subscribe before refreshing',
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (currentInput === undefined) {
|
|
141
|
+
return Promise.reject(new SvelteAdapterError('@zmdb/svelte query store has no current input'));
|
|
142
|
+
}
|
|
143
|
+
return launch(currentInput.value, false, '@zmdb/svelte query superseded by refresh');
|
|
144
|
+
},
|
|
145
|
+
destroy() {
|
|
146
|
+
if (destroyed) return;
|
|
147
|
+
destroyed = true;
|
|
148
|
+
deactivate('@zmdb/svelte query cancelled because its owner was destroyed');
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return Object.freeze(store);
|
|
153
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Readable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
export type QueryLoader<Client, Input, Output> = (
|
|
4
|
+
client: Client,
|
|
5
|
+
input: Input,
|
|
6
|
+
signal: AbortSignal,
|
|
7
|
+
) => PromiseLike<Output>;
|
|
8
|
+
|
|
9
|
+
export type MutationRunner<Client, Input, Output> = (
|
|
10
|
+
client: Client,
|
|
11
|
+
input: Input,
|
|
12
|
+
signal: AbortSignal,
|
|
13
|
+
) => PromiseLike<Output>;
|
|
14
|
+
|
|
15
|
+
export interface QuerySnapshot<Output> {
|
|
16
|
+
readonly data: Output | undefined;
|
|
17
|
+
readonly error: unknown;
|
|
18
|
+
readonly loading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MutationSnapshot {
|
|
22
|
+
readonly error: unknown;
|
|
23
|
+
readonly pending: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SvelteQueryStore<Output> extends Readable<QuerySnapshot<Output>> {
|
|
27
|
+
refresh(): Promise<void>;
|
|
28
|
+
destroy(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SvelteMutationStore<Input, Output> extends Readable<MutationSnapshot> {
|
|
32
|
+
mutate(input: Input): Promise<Output>;
|
|
33
|
+
destroy(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ZmdbSvelteBindings<Client> {
|
|
37
|
+
getClient(): Client;
|
|
38
|
+
setClient(client: Client): Client;
|
|
39
|
+
hasClient(): boolean;
|
|
40
|
+
query<Input, Output>(
|
|
41
|
+
input: Input | Readable<Input>,
|
|
42
|
+
load: QueryLoader<Client, Input, Output>,
|
|
43
|
+
): SvelteQueryStore<Output>;
|
|
44
|
+
mutation<Input, Output>(run: MutationRunner<Client, Input, Output>): SvelteMutationStore<Input, Output>;
|
|
45
|
+
}
|