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