@comapeo/core-react 8.0.0 → 9.0.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.
Files changed (43) hide show
  1. package/README.md +38 -0
  2. package/dist/commonjs/contexts/ClientApi.d.ts +5 -5
  3. package/dist/commonjs/contexts/ClientApi.js +6 -5
  4. package/dist/commonjs/contexts/ComapeoCore.d.ts +8 -0
  5. package/dist/commonjs/contexts/ComapeoCore.js +9 -0
  6. package/dist/commonjs/contexts/MapServer.d.ts +69 -0
  7. package/dist/commonjs/contexts/MapServer.js +92 -0
  8. package/dist/commonjs/contexts/MapShares.d.ts +52 -0
  9. package/dist/commonjs/contexts/MapShares.js +74 -0
  10. package/dist/commonjs/hooks/maps.d.ts +460 -3
  11. package/dist/commonjs/hooks/maps.js +261 -4
  12. package/dist/commonjs/index.d.ts +5 -2
  13. package/dist/commonjs/index.js +20 -3
  14. package/dist/commonjs/lib/http.d.ts +45 -0
  15. package/dist/commonjs/lib/http.js +103 -0
  16. package/dist/commonjs/lib/map-shares-stores.d.ts +80 -0
  17. package/dist/commonjs/lib/map-shares-stores.js +299 -0
  18. package/dist/commonjs/lib/react-query/maps.d.ts +66 -20
  19. package/dist/commonjs/lib/react-query/maps.js +113 -11
  20. package/dist/commonjs/lib/react-query/mutation-result.d.ts +8 -0
  21. package/dist/commonjs/lib/react-query/mutation-result.js +22 -0
  22. package/dist/esm/contexts/ClientApi.d.ts +5 -5
  23. package/dist/esm/contexts/ClientApi.js +6 -5
  24. package/dist/esm/contexts/ComapeoCore.d.ts +8 -0
  25. package/dist/esm/contexts/ComapeoCore.js +6 -0
  26. package/dist/esm/contexts/MapServer.d.ts +69 -0
  27. package/dist/esm/contexts/MapServer.js +86 -0
  28. package/dist/esm/contexts/MapShares.d.ts +52 -0
  29. package/dist/esm/contexts/MapShares.js +65 -0
  30. package/dist/esm/hooks/maps.d.ts +460 -3
  31. package/dist/esm/hooks/maps.js +252 -6
  32. package/dist/esm/index.d.ts +5 -2
  33. package/dist/esm/index.js +4 -2
  34. package/dist/esm/lib/http.d.ts +45 -0
  35. package/dist/esm/lib/http.js +98 -0
  36. package/dist/esm/lib/map-shares-stores.d.ts +80 -0
  37. package/dist/esm/lib/map-shares-stores.js +291 -0
  38. package/dist/esm/lib/react-query/maps.d.ts +66 -20
  39. package/dist/esm/lib/react-query/maps.js +109 -12
  40. package/dist/esm/lib/react-query/mutation-result.d.ts +8 -0
  41. package/dist/esm/lib/react-query/mutation-result.js +19 -0
  42. package/docs/API.md +567 -60
  43. package/package.json +15 -4
@@ -0,0 +1,291 @@
1
+ import { CUSTOM_MAP_ID } from '@comapeo/map-server/constants.js';
2
+ import { errors } from '@comapeo/map-server/errors.js';
3
+ import ensureError from 'ensure-error';
4
+ import { invalidateMapQueries } from './react-query/maps.js';
5
+ // ============================================
6
+ // ACTION OPTIONS TYPES
7
+ // These are defined here so that VSCode tooltips work for the mutation
8
+ // functions - if the documentation comments are added inline for the store
9
+ // actions, they do not show for the mutate() function in hooks.
10
+ // ============================================
11
+ /** Known reasons for declining a map share */
12
+ export const DeclineReason = {
13
+ /** User explicitly rejected the map share */
14
+ user_rejected: 'user_rejected',
15
+ /** Device storage is full */
16
+ storage_full: 'storage_full',
17
+ };
18
+ /**
19
+ * This is like a mini zustand store. Keeping the map shares in an external
20
+ * store avoids unnecessary re-renders of the entire app when map shares are
21
+ * updated (e.g. if we kept the state in the context), and it avoids potential
22
+ * tearing issues with concurrent rendering.
23
+ *
24
+ * This is the base store for both sent and received map shares, since they
25
+ * share a lot of logic around managing the map shares and monitoring their
26
+ * status.
27
+ */
28
+ function createMapSharesStore({ mapServerApi }) {
29
+ let mapShares = [];
30
+ const listeners = new Set();
31
+ function update(shareId, stateUpdate) {
32
+ const index = mapShares.findIndex((s) => s.shareId === shareId);
33
+ const existing = mapShares[index];
34
+ if (!existing) {
35
+ throw new errors.MAP_SHARE_NOT_FOUND(`Map share with id ${shareId} not found`);
36
+ }
37
+ assertValidStatusTransition(existing.status, stateUpdate.status);
38
+ mapShares[index] = { ...existing, ...stateUpdate };
39
+ const isDownloadProgressUpdate = stateUpdate.status === 'downloading' && existing.status === 'downloading';
40
+ // IMPORTANT: For download progress updates, the store state is mutated, so
41
+ // maintains Object.is equality. This means that components listening to the
42
+ // store state without a selector _will not update_ when download progress
43
+ // updates. However, all other updates will result in a re-render, and using
44
+ // a selector to listen to an individual map share will also update during
45
+ // download progress.
46
+ if (!isDownloadProgressUpdate) {
47
+ mapShares = [...mapShares];
48
+ }
49
+ emit();
50
+ }
51
+ function add(mapShare) {
52
+ mapShares = [...mapShares, mapShare];
53
+ emit();
54
+ }
55
+ function get(shareId) {
56
+ const mapShare = mapShares.find((share) => share.shareId === shareId);
57
+ if (!mapShare) {
58
+ throw new errors.MAP_SHARE_NOT_FOUND(`Map share with id ${shareId} not found`);
59
+ }
60
+ return mapShare;
61
+ }
62
+ function handleError(shareId, cause) {
63
+ const error = ensureError(cause);
64
+ const errorCode = 'code' in error ? String(error.code) : 'UNKNOWN_ERROR';
65
+ try {
66
+ update(shareId, {
67
+ status: 'error',
68
+ error: {
69
+ message: error.message,
70
+ code: errorCode,
71
+ },
72
+ });
73
+ }
74
+ catch (e) {
75
+ // TODO: log errors with Sentry
76
+ console.error(`Failed to update map share ${shareId} with error ${errorCode}:`, e);
77
+ }
78
+ }
79
+ async function monitor(mapShareId, path) {
80
+ // TODO: add a timeout in case the download stalls and never completes
81
+ return new Promise((resolve, reject) => {
82
+ const es = mapServerApi.createEventSource({
83
+ url: path,
84
+ onMessage({ data }) {
85
+ try {
86
+ const stateUpdate = JSON.parse(data);
87
+ update(mapShareId, stateUpdate);
88
+ if (isFinalStatus(stateUpdate.status)) {
89
+ es.close();
90
+ resolve(stateUpdate);
91
+ }
92
+ }
93
+ catch (e) {
94
+ // NB: Don't handleError here - because we optimistically update the
95
+ // status, some of the updates from the event source will throw for
96
+ // being an invalid status transition, but we can just ignore those
97
+ // errors.
98
+ // TODO: Custom errors for status transitions, and only ignore those
99
+ es.close();
100
+ reject(e);
101
+ }
102
+ },
103
+ });
104
+ });
105
+ }
106
+ function emit() {
107
+ listeners.forEach((l) => l());
108
+ }
109
+ return {
110
+ subscribe(listener) {
111
+ listeners.add(listener);
112
+ return () => listeners.delete(listener);
113
+ },
114
+ getSnapshot() {
115
+ return mapShares;
116
+ },
117
+ update,
118
+ add,
119
+ get,
120
+ handleError,
121
+ monitor,
122
+ };
123
+ }
124
+ /**
125
+ * Store and actions for received map shares.
126
+ */
127
+ export function createReceivedMapSharesStore({ clientApi, mapServerApi, queryClient, }) {
128
+ const { subscribe, getSnapshot, update, add, get, handleError, monitor } = createMapSharesStore({ mapServerApi });
129
+ // Tracks downloads in progress so they can be aborted. Currently there is no
130
+ // cleanup, but this is unlikely to be an issue in practice.
131
+ const downloads = new Map();
132
+ clientApi.on('map-share', (mapShare) => {
133
+ add({ ...mapShare, status: 'pending' });
134
+ });
135
+ const actions = {
136
+ async download({ shareId }) {
137
+ const mapShare = get(shareId);
138
+ update(shareId, { status: 'downloading', bytesDownloaded: 0 });
139
+ try {
140
+ const downloadIdPromise = mapServerApi
141
+ .post(`downloads`, {
142
+ json: {
143
+ senderDeviceId: mapShare.senderDeviceId,
144
+ shareId: mapShare.shareId,
145
+ mapShareUrls: mapShare.mapShareUrls,
146
+ estimatedSizeBytes: mapShare.estimatedSizeBytes,
147
+ },
148
+ })
149
+ .json()
150
+ .then(({ downloadId }) => downloadId);
151
+ // Not strictly necessary, because the `await downloadIdPromise` in the
152
+ // same tick below ensures that this is handled, but this protects
153
+ // against a refactor which could add another async function between
154
+ // setting the promise on the map and awaiting the downloadIdPromise,
155
+ // which would result in an unhandled rejection without this.
156
+ downloadIdPromise.catch(noop);
157
+ downloads.set(shareId, downloadIdPromise);
158
+ const downloadId = await downloadIdPromise;
159
+ monitor(shareId, `downloads/${downloadId}/events`)
160
+ .then((stateUpdate) => {
161
+ downloads.delete(shareId);
162
+ // Invalidate map queries when download completes to trigger reload of map
163
+ if (stateUpdate.status === 'completed') {
164
+ return invalidateMapQueries(queryClient, {
165
+ mapId: mapShare.mapId,
166
+ });
167
+ }
168
+ })
169
+ .catch(noop);
170
+ }
171
+ catch (e) {
172
+ downloads.delete(shareId);
173
+ handleError(shareId, e);
174
+ throw e;
175
+ }
176
+ },
177
+ async decline({ shareId, reason }) {
178
+ const mapShare = get(shareId);
179
+ update(shareId, { status: 'declined', reason });
180
+ try {
181
+ await mapServerApi.post(`mapShares/${shareId}/decline`, {
182
+ json: {
183
+ senderDeviceId: mapShare.senderDeviceId,
184
+ mapShareUrls: mapShare.mapShareUrls,
185
+ reason,
186
+ },
187
+ });
188
+ }
189
+ catch (e) {
190
+ handleError(shareId, e);
191
+ throw e;
192
+ }
193
+ },
194
+ async abort({ shareId }) {
195
+ update(shareId, { status: 'aborted' });
196
+ try {
197
+ const downloadId = await downloads.get(shareId);
198
+ if (!downloadId) {
199
+ throw new errors.DOWNLOAD_NOT_FOUND(`No download in progress for map share with id ${shareId}`);
200
+ }
201
+ await mapServerApi.post(`downloads/${downloadId}/abort`);
202
+ }
203
+ catch (e) {
204
+ handleError(shareId, e);
205
+ throw e;
206
+ }
207
+ finally {
208
+ downloads.delete(shareId);
209
+ }
210
+ },
211
+ };
212
+ return {
213
+ subscribe,
214
+ getSnapshot,
215
+ actions,
216
+ };
217
+ }
218
+ /**
219
+ * Store and actions for sent map share.
220
+ */
221
+ export function createSentMapSharesStore({ clientApi, mapServerApi, }) {
222
+ const { subscribe, getSnapshot, update, add, handleError, monitor } = createMapSharesStore({ mapServerApi });
223
+ const actions = {
224
+ async createAndSend({ projectId, receiverDeviceId, mapId = CUSTOM_MAP_ID, }) {
225
+ const mapShare = await mapServerApi
226
+ .post('mapShares', {
227
+ json: { receiverDeviceId, mapId },
228
+ })
229
+ .json();
230
+ try {
231
+ const project = await clientApi.getProject(projectId);
232
+ await project.$sendMapShare(mapShare);
233
+ }
234
+ catch (e) {
235
+ await mapServerApi.post(`mapShares/${mapShare.shareId}/cancel`);
236
+ throw e;
237
+ }
238
+ add(mapShare);
239
+ monitor(mapShare.shareId, `mapShares/${mapShare.shareId}/events`).catch(noop);
240
+ return mapShare;
241
+ },
242
+ async cancel({ shareId }) {
243
+ update(shareId, { status: 'canceled' });
244
+ try {
245
+ await mapServerApi.post(`mapShares/${shareId}/cancel`);
246
+ }
247
+ catch (e) {
248
+ handleError(shareId, e);
249
+ throw e;
250
+ }
251
+ },
252
+ };
253
+ return {
254
+ subscribe,
255
+ getSnapshot,
256
+ actions,
257
+ };
258
+ }
259
+ const allowedStatusTransitions = {
260
+ pending: ['pending', 'downloading', 'declined', 'canceled', 'error'],
261
+ downloading: ['downloading', 'aborted', 'completed', 'canceled', 'error'],
262
+ completed: ['error'],
263
+ canceled: ['error'],
264
+ aborted: ['error'],
265
+ declined: ['error'],
266
+ error: ['error'],
267
+ };
268
+ /**
269
+ * Asserts that the transition from current to next status is valid. Throws if
270
+ * the transition is invalid.
271
+ */
272
+ function assertValidStatusTransition(current, next) {
273
+ if (!allowedStatusTransitions[current].includes(next)) {
274
+ throw new Error(`Invalid status transition from ${current} to ${next}`);
275
+ }
276
+ }
277
+ const finalStatuses = [
278
+ 'declined',
279
+ 'canceled',
280
+ 'aborted',
281
+ 'completed',
282
+ 'error',
283
+ ];
284
+ /**
285
+ * Returns true if the status is a final status, meaning that no further updates
286
+ * should be expected for the map share.
287
+ */
288
+ function isFinalStatus(status) {
289
+ return finalStatuses.includes(status);
290
+ }
291
+ function noop() { }
@@ -1,26 +1,72 @@
1
- import type { MapeoClientApi } from '@comapeo/ipc' with {
2
- 'resolution-mode': 'import'
1
+ import { type QueryClient, type UseMutationOptions } from '@tanstack/react-query';
2
+ import type { MapServerApi } from '../../contexts/MapServer.js';
3
+ import type { ReceivedMapSharesStore, SentMapSharesStore } from '../map-shares-stores.js';
4
+ type CompatFile = Omit<File, 'lastModified' | 'webkitRelativePath'>;
5
+ type ExpoFileDuckType = CompatFile & {
6
+ exists: boolean;
3
7
  };
4
- export declare function getMapsQueryKey(): readonly ["@comapeo/core-react", "maps"];
5
- export declare function getStyleJsonUrlQueryKey({ refreshToken, }: {
6
- refreshToken?: string;
7
- }): readonly ["@comapeo/core-react", "maps", "stylejson_url", {
8
- readonly refreshToken: string | undefined;
9
- }];
10
- export declare function mapStyleJsonUrlQueryOptions({ clientApi, refreshToken, }: {
11
- clientApi: MapeoClientApi;
12
- refreshToken?: string;
13
- }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<string, Error, string, readonly ["@comapeo/core-react", "maps", "stylejson_url", {
14
- readonly refreshToken: string | undefined;
15
- }]>, "queryFn"> & {
16
- queryFn?: import("@tanstack/react-query").QueryFunction<string, readonly ["@comapeo/core-react", "maps", "stylejson_url", {
17
- readonly refreshToken: string | undefined;
18
- }], never> | undefined;
8
+ export declare function getMapQueryKey({ mapId }: {
9
+ mapId: string;
10
+ }): readonly ["@comapeo/core-react", "maps", string];
11
+ export declare function getStyleJsonUrlQueryKey({ mapId }: {
12
+ mapId: string;
13
+ }): readonly ["@comapeo/core-react", "maps", string, "stylejson_url"];
14
+ /**
15
+ * Invalidate queries for this map and the default map (which internally
16
+ * redirects to custom) so that they will be refetched with the new map data.
17
+ */
18
+ export declare function invalidateMapQueries(queryClient: QueryClient, { mapId }: {
19
+ mapId: string;
20
+ }): Promise<void>;
21
+ export declare function mapStyleJsonUrlQueryOptions({ mapServerApi, mapId, }: {
22
+ mapServerApi: MapServerApi;
23
+ mapId?: string;
24
+ }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<string, Error, string, readonly ["@comapeo/core-react", "maps", string, "stylejson_url"]>, "queryFn"> & {
25
+ queryFn?: import("@tanstack/react-query").QueryFunction<string, readonly ["@comapeo/core-react", "maps", string, "stylejson_url"], never> | undefined;
19
26
  } & {
20
- queryKey: readonly ["@comapeo/core-react", "maps", "stylejson_url", {
21
- readonly refreshToken: string | undefined;
22
- }] & {
27
+ queryKey: readonly ["@comapeo/core-react", "maps", string, "stylejson_url"] & {
23
28
  [dataTagSymbol]: string;
24
29
  [dataTagErrorSymbol]: Error;
25
30
  };
26
31
  };
32
+ export declare function mapInfoQueryOptions({ mapServerApi, mapId, }: {
33
+ mapServerApi: MapServerApi;
34
+ mapId?: string;
35
+ }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<unknown, Error, unknown, readonly ["@comapeo/core-react", "maps", string, "info"]>, "queryFn"> & {
36
+ queryFn?: import("@tanstack/react-query").QueryFunction<unknown, readonly ["@comapeo/core-react", "maps", string, "info"], never> | undefined;
37
+ } & {
38
+ queryKey: readonly ["@comapeo/core-react", "maps", string, "info"] & {
39
+ [dataTagSymbol]: unknown;
40
+ [dataTagErrorSymbol]: Error;
41
+ };
42
+ };
43
+ export declare function mapImportMutationOptions({ mapServerApi, queryClient, }: {
44
+ mapServerApi: MapServerApi;
45
+ queryClient: QueryClient;
46
+ }): {
47
+ mutationFn: ({ file }: {
48
+ file: File | ExpoFileDuckType;
49
+ }) => Promise<Response>;
50
+ onSuccess: () => Promise<void>;
51
+ networkMode: "always";
52
+ retry: false;
53
+ };
54
+ export declare function mapRemoveMutationOptions({ mapServerApi, queryClient, }: {
55
+ mapServerApi: MapServerApi;
56
+ queryClient: QueryClient;
57
+ }): {
58
+ mutationFn: () => Promise<Response>;
59
+ onSuccess: () => Promise<void>;
60
+ networkMode: "always";
61
+ retry: false;
62
+ };
63
+ /**
64
+ * Mutation options for actions on sent map shares
65
+ */
66
+ export declare function mapSharesMutationOptions<TAction extends SentMapSharesStore['actions'][keyof SentMapSharesStore['actions']] | keyof ReceivedMapSharesStore['actions'][keyof ReceivedMapSharesStore['actions']]>(options: {
67
+ action: Exclude<TAction, SentMapSharesStore['actions']['createAndSend']>;
68
+ } | {
69
+ action: SentMapSharesStore['actions']['createAndSend'];
70
+ projectId: string;
71
+ }): UseMutationOptions<ReturnType<TAction>, Error, TAction extends SentMapSharesStore['actions']['createAndSend'] ? Parameters<TAction>[0] : Omit<Parameters<TAction>[0], 'projectId'>>;
72
+ export {};
@@ -1,22 +1,119 @@
1
- import { queryOptions } from '@tanstack/react-query';
2
- import { baseQueryOptions, ROOT_QUERY_KEY } from './shared.js';
3
- export function getMapsQueryKey() {
4
- return [ROOT_QUERY_KEY, 'maps'];
1
+ import { CUSTOM_MAP_ID, DEFAULT_MAP_ID } from '@comapeo/map-server/constants.js';
2
+ import { queryOptions, } from '@tanstack/react-query';
3
+ import { baseMutationOptions, baseQueryOptions, ROOT_QUERY_KEY, } from './shared.js';
4
+ // ============================================
5
+ // QUERY KEYS
6
+ // ============================================
7
+ const MAPS_ROOT_QUERY_KEY = [ROOT_QUERY_KEY, 'maps'];
8
+ export function getMapQueryKey({ mapId }) {
9
+ return [...MAPS_ROOT_QUERY_KEY, mapId];
5
10
  }
6
- export function getStyleJsonUrlQueryKey({ refreshToken, }) {
7
- return [ROOT_QUERY_KEY, 'maps', 'stylejson_url', { refreshToken }];
11
+ export function getStyleJsonUrlQueryKey({ mapId }) {
12
+ return [...getMapQueryKey({ mapId }), 'stylejson_url'];
8
13
  }
9
- export function mapStyleJsonUrlQueryOptions({ clientApi, refreshToken, }) {
14
+ /**
15
+ * Invalidate queries for this map and the default map (which internally
16
+ * redirects to custom) so that they will be refetched with the new map data.
17
+ */
18
+ export async function invalidateMapQueries(queryClient, { mapId }) {
19
+ await Promise.all([
20
+ queryClient.invalidateQueries({
21
+ queryKey: getMapQueryKey({ mapId }),
22
+ }),
23
+ queryClient.invalidateQueries({
24
+ queryKey: getMapQueryKey({ mapId: DEFAULT_MAP_ID }),
25
+ }),
26
+ ]);
27
+ }
28
+ // ============================================
29
+ // QUERY OPTIONS
30
+ // ============================================
31
+ export function mapStyleJsonUrlQueryOptions({ mapServerApi, mapId = DEFAULT_MAP_ID, }) {
32
+ if (mapId !== DEFAULT_MAP_ID) {
33
+ throw new Error('Custom map IDs are not supported yet');
34
+ }
10
35
  return queryOptions({
11
36
  ...baseQueryOptions(),
12
- queryKey: getStyleJsonUrlQueryKey({ refreshToken }),
37
+ queryKey: getStyleJsonUrlQueryKey({ mapId }),
13
38
  queryFn: async () => {
14
- const result = await clientApi.getMapStyleJsonUrl();
15
- if (!refreshToken)
16
- return result;
39
+ const result = await mapServerApi.getMapStyleJsonUrl(mapId);
17
40
  const u = new URL(result);
18
- u.searchParams.set('refresh_token', refreshToken);
41
+ // This ensures that every time this query is refetched, it will have a different search param, forcing the map to reload.
42
+ u.searchParams.set('refresh_token', Date.now().toString());
19
43
  return u.href;
20
44
  },
45
+ // Keep this cached until the cache is manually invalidated by a map upload
46
+ staleTime: Infinity,
47
+ gcTime: Infinity,
48
+ });
49
+ }
50
+ export function mapInfoQueryOptions({ mapServerApi, mapId = DEFAULT_MAP_ID, }) {
51
+ if (mapId !== CUSTOM_MAP_ID) {
52
+ throw new Error('Only custom map ID is currently supported');
53
+ }
54
+ return queryOptions({
55
+ ...baseQueryOptions(),
56
+ queryKey: [...getMapQueryKey({ mapId }), 'info'],
57
+ queryFn: async () => {
58
+ return mapServerApi.get(`maps/${mapId}/info`).json();
59
+ },
60
+ // Keep this cached until the cache is manually invalidated by a map upload
61
+ staleTime: Infinity,
62
+ gcTime: Infinity,
21
63
  });
22
64
  }
65
+ // ============================================
66
+ // MUTATION OPTIONS
67
+ // ============================================
68
+ export function mapImportMutationOptions({ mapServerApi, queryClient, }) {
69
+ // TODO: Support importing to custom map IDs, to support multiple maps.
70
+ const mapId = CUSTOM_MAP_ID;
71
+ return {
72
+ ...baseMutationOptions(),
73
+ mutationFn: async ({ file }) => {
74
+ if ('exists' in file && !file.exists) {
75
+ throw new Error('File does not exist or is not accessible');
76
+ }
77
+ return mapServerApi.put(`maps/${mapId}`, {
78
+ body: file,
79
+ headers: {
80
+ 'Content-Type': 'application/octet-stream',
81
+ },
82
+ });
83
+ },
84
+ onSuccess: async () => {
85
+ await invalidateMapQueries(queryClient, { mapId });
86
+ },
87
+ };
88
+ }
89
+ export function mapRemoveMutationOptions({ mapServerApi, queryClient, }) {
90
+ // TODO: Support removing from custom map IDs, to support multiple maps.
91
+ const mapId = CUSTOM_MAP_ID;
92
+ return {
93
+ ...baseMutationOptions(),
94
+ mutationFn: async () => {
95
+ return mapServerApi.delete(`maps/${mapId}`);
96
+ },
97
+ onSuccess: async () => {
98
+ await invalidateMapQueries(queryClient, { mapId });
99
+ },
100
+ };
101
+ }
102
+ /**
103
+ * Mutation options for actions on sent map shares
104
+ */
105
+ export function mapSharesMutationOptions(options) {
106
+ return {
107
+ ...baseMutationOptions(),
108
+ mutationFn: async (variables) => {
109
+ // For consistency with other hooks, we use `projectId` as a parameter of
110
+ // the hook, rather than a parameter of the mutate function.
111
+ const actionOptions = 'projectId' in options
112
+ ? { ...variables, projectId: options.projectId }
113
+ : variables;
114
+ return options.action(
115
+ // @ts-expect-error - TS can't help us here
116
+ actionOptions);
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,8 @@
1
+ import type { UseMutationResult } from '@tanstack/react-query';
2
+ import type { DistributedPick } from 'type-fest';
3
+ /**
4
+ * Filters a `UseMutationResult` to only include a subset of its keys, and uses
5
+ * `DistributedPick` to preserve the discriminated union types of the mutation
6
+ * result based on the `status` property.
7
+ */
8
+ export declare function filterMutationResult<TResult extends UseMutationResult<any, any, any, any>>(mutationResult: TResult): DistributedPick<TResult, "error" | "status" | "mutate" | "reset" | "mutateAsync">;
@@ -0,0 +1,19 @@
1
+ const PICKED_MUTATION_RESULT_KEYS = [
2
+ 'error',
3
+ 'mutate',
4
+ 'mutateAsync',
5
+ 'reset',
6
+ 'status',
7
+ ];
8
+ /**
9
+ * Filters a `UseMutationResult` to only include a subset of its keys, and uses
10
+ * `DistributedPick` to preserve the discriminated union types of the mutation
11
+ * result based on the `status` property.
12
+ */
13
+ export function filterMutationResult(mutationResult) {
14
+ const filteredResult = {};
15
+ for (const key of PICKED_MUTATION_RESULT_KEYS) {
16
+ filteredResult[key] = mutationResult[key];
17
+ }
18
+ return filteredResult;
19
+ }