@comapeo/core-react 10.0.0 → 11.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.
- package/dist/commonjs/contexts/MapShares.d.ts +1 -1
- package/dist/commonjs/hooks/documents.d.ts +1 -0
- package/dist/commonjs/hooks/documents.js +0 -1
- package/dist/commonjs/hooks/maps.d.ts +33 -15
- package/dist/commonjs/hooks/maps.js +33 -15
- package/dist/commonjs/index.d.ts +1 -1
- package/dist/commonjs/index.js +6 -2
- package/dist/commonjs/lib/map-shares-stores.d.ts +120 -3
- package/dist/commonjs/lib/map-shares-stores.js +170 -8
- package/dist/esm/contexts/MapShares.d.ts +1 -1
- package/dist/esm/hooks/documents.d.ts +1 -0
- package/dist/esm/hooks/documents.js +0 -1
- package/dist/esm/hooks/maps.d.ts +33 -15
- package/dist/esm/hooks/maps.js +33 -15
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/lib/map-shares-stores.d.ts +120 -3
- package/dist/esm/lib/map-shares-stores.js +166 -7
- package/docs/API.md +4 -9
- package/package.json +6 -6
|
@@ -42,7 +42,7 @@ export declare function useReceivedMapSharesState<T>(selector: (state: Array<Rec
|
|
|
42
42
|
* @internal
|
|
43
43
|
*/
|
|
44
44
|
export declare function useSentMapSharesActions(): {
|
|
45
|
-
createAndSend({
|
|
45
|
+
createAndSend({ receiverDeviceId, mapId, }: import("../lib/map-shares-stores.js").CreateAndSendMapShareOptions): Promise<import("@comapeo/map-server").MapShareState>;
|
|
46
46
|
cancel({ shareId }: import("../lib/map-shares-stores.js").CancelMapShareOptions): Promise<void>;
|
|
47
47
|
};
|
|
48
48
|
/**
|
|
@@ -123,6 +123,7 @@ export declare function useUpdateDocument<D extends WriteableDocumentType>({ doc
|
|
|
123
123
|
projectId: string;
|
|
124
124
|
}): FilteredMutationResult<UseMutationResult<Awaited<ReturnType<MapeoProjectApi[D]['update']>>, Error, {
|
|
125
125
|
value: Omit<WriteableValue<D>, 'schemaName'>;
|
|
126
|
+
versionId: string;
|
|
126
127
|
}>>;
|
|
127
128
|
/**
|
|
128
129
|
* Delete a document within a project.
|
|
@@ -190,7 +190,6 @@ export function useUpdateDocument({
|
|
|
190
190
|
docType, projectId, }) {
|
|
191
191
|
const queryClient = useQueryClient();
|
|
192
192
|
const { data: projectApi } = useSingleProject({ projectId });
|
|
193
|
-
// @ts-expect-error Not sure why TS complains here
|
|
194
193
|
return filterMutationResult(useMutation({
|
|
195
194
|
...baseMutationOptions(),
|
|
196
195
|
mutationFn: async ({ versionId, value, }) => {
|
package/dist/esm/hooks/maps.d.ts
CHANGED
|
@@ -156,15 +156,25 @@ export declare function useSingleReceivedMapShare({ shareId }: {
|
|
|
156
156
|
/**
|
|
157
157
|
* Accept and download a map share that has been received. The mutate promise
|
|
158
158
|
* resolves once the map _starts_ downloading, before it finishes downloading.
|
|
159
|
-
* Use `
|
|
159
|
+
* Use `useManyReceivedMapShares` or `useSingleReceivedMapShare` to track
|
|
160
|
+
* download progress and final status.
|
|
160
161
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
162
|
+
* If the sender canceled the share before the receiver calls this, the
|
|
163
|
+
* mutation will still resolve (the download starts), but the share status will
|
|
164
|
+
* end up as `'canceled'` rather than `'completed'`. This is the only way the
|
|
165
|
+
* receiver discovers that a share has been canceled — check `share.status`
|
|
166
|
+
* after the download settles.
|
|
167
|
+
*
|
|
168
|
+
* @throws {MapShareCanceledError} If the share is already known to be canceled
|
|
169
|
+
* (i.e. `status` is `'canceled'` in the store, e.g. after a previous
|
|
170
|
+
* download attempt discovered the cancellation).
|
|
171
|
+
* @throws {InvalidStatusTransitionError} If the share is not in a valid state
|
|
172
|
+
* to start downloading (e.g. already downloading, completed, or declined).
|
|
163
173
|
*
|
|
164
174
|
* @example
|
|
165
175
|
* ```tsx
|
|
166
176
|
* function AcceptButton({ shareId }: { shareId: string }) {
|
|
167
|
-
* const { mutate: accept } =
|
|
177
|
+
* const { mutate: accept } = useDownloadReceivedMapShare()
|
|
168
178
|
*
|
|
169
179
|
* return <button onClick={() => accept({ shareId })}>Accept</button>
|
|
170
180
|
* }
|
|
@@ -189,17 +199,25 @@ export declare function useDownloadReceivedMapShare(): Pick<import("@tanstack/re
|
|
|
189
199
|
}, "error" | "status" | "mutate" | "reset" | "mutateAsync">;
|
|
190
200
|
/**
|
|
191
201
|
* Decline a map share that has been received. Notifies the sender that the
|
|
192
|
-
* share was declined.
|
|
202
|
+
* share was declined. The share status is only updated to `'declined'` after
|
|
203
|
+
* the server confirms the decline — there is no optimistic update.
|
|
204
|
+
*
|
|
205
|
+
* If the sender canceled the share before the decline reaches the server, the
|
|
206
|
+
* share status will transition to `'canceled'` (not `'error'`) and the
|
|
207
|
+
* mutation will throw a `MapShareCanceledError`.
|
|
193
208
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
209
|
+
* @throws {MapShareCanceledError} If the share is already known to be
|
|
210
|
+
* canceled, or if the server reports that the sender canceled the share
|
|
211
|
+
* while the decline was in flight. In both cases `share.status` will be
|
|
212
|
+
* `'canceled'`.
|
|
213
|
+
* @throws {InvalidStatusTransitionError} If the share is not in
|
|
214
|
+
* `status='pending'` (e.g. already downloading, completed, or declined).
|
|
197
215
|
*
|
|
198
216
|
* @example
|
|
199
217
|
* ```tsx
|
|
200
218
|
* import { DeclineReason } from '@comapeo/core-react'
|
|
201
219
|
* function DeclineButton({ shareId }: { shareId: string }) {
|
|
202
|
-
* const { mutate: decline } =
|
|
220
|
+
* const { mutate: decline } = useDeclineReceivedMapShare()
|
|
203
221
|
*
|
|
204
222
|
* return (
|
|
205
223
|
* <button onClick={() => decline({ shareId, reason: DeclineReason.user_rejected })}>
|
|
@@ -229,13 +247,15 @@ export declare function useDeclineReceivedMapShare(): Pick<import("@tanstack/rea
|
|
|
229
247
|
/**
|
|
230
248
|
* Abort an in-progress map share download.
|
|
231
249
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
250
|
+
* @throws {MapShareCanceledError} If the share is already known to be canceled.
|
|
251
|
+
* @throws {InvalidStatusTransitionError} If the share is not in
|
|
252
|
+
* `status='downloading'` (e.g. still pending, already completed, or
|
|
253
|
+
* declined).
|
|
234
254
|
*
|
|
235
255
|
* @example
|
|
236
256
|
* ```tsx
|
|
237
257
|
* function AbortButton({ shareId }: { shareId: string }) {
|
|
238
|
-
* const { mutate: abort } =
|
|
258
|
+
* const { mutate: abort } = useAbortReceivedMapShareDownload()
|
|
239
259
|
*
|
|
240
260
|
* return <button onClick={() => abort({ shareId })}>Cancel Download</button>
|
|
241
261
|
* }
|
|
@@ -264,8 +284,6 @@ export declare function useAbortReceivedMapShareDownload(): Pick<import("@tansta
|
|
|
264
284
|
* mutation resolves with the created map share object, including its ID, which
|
|
265
285
|
* can be used to track the share status with `useSingleSentMapShare`.
|
|
266
286
|
*
|
|
267
|
-
* @param opts.projectId Public ID of project for sending the map share: you can only send map shares to users on the same project
|
|
268
|
-
*
|
|
269
287
|
* @example
|
|
270
288
|
* ```tsx
|
|
271
289
|
* function SendMapButton({ projectId, deviceId }: { projectId: string; deviceId: string }) {
|
|
@@ -274,7 +292,7 @@ export declare function useAbortReceivedMapShareDownload(): Pick<import("@tansta
|
|
|
274
292
|
* return (
|
|
275
293
|
* <button
|
|
276
294
|
* onClick={() =>
|
|
277
|
-
* send({
|
|
295
|
+
* send({ receiverDeviceId: deviceId, mapId: 'custom' }, {
|
|
278
296
|
* onSuccess: (mapShare) => {
|
|
279
297
|
* console.log('Share sent with id', mapShare.shareId)
|
|
280
298
|
* }
|
package/dist/esm/hooks/maps.js
CHANGED
|
@@ -168,15 +168,25 @@ export function useSingleReceivedMapShare({ shareId }) {
|
|
|
168
168
|
/**
|
|
169
169
|
* Accept and download a map share that has been received. The mutate promise
|
|
170
170
|
* resolves once the map _starts_ downloading, before it finishes downloading.
|
|
171
|
-
* Use `
|
|
171
|
+
* Use `useManyReceivedMapShares` or `useSingleReceivedMapShare` to track
|
|
172
|
+
* download progress and final status.
|
|
172
173
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
174
|
+
* If the sender canceled the share before the receiver calls this, the
|
|
175
|
+
* mutation will still resolve (the download starts), but the share status will
|
|
176
|
+
* end up as `'canceled'` rather than `'completed'`. This is the only way the
|
|
177
|
+
* receiver discovers that a share has been canceled — check `share.status`
|
|
178
|
+
* after the download settles.
|
|
179
|
+
*
|
|
180
|
+
* @throws {MapShareCanceledError} If the share is already known to be canceled
|
|
181
|
+
* (i.e. `status` is `'canceled'` in the store, e.g. after a previous
|
|
182
|
+
* download attempt discovered the cancellation).
|
|
183
|
+
* @throws {InvalidStatusTransitionError} If the share is not in a valid state
|
|
184
|
+
* to start downloading (e.g. already downloading, completed, or declined).
|
|
175
185
|
*
|
|
176
186
|
* @example
|
|
177
187
|
* ```tsx
|
|
178
188
|
* function AcceptButton({ shareId }: { shareId: string }) {
|
|
179
|
-
* const { mutate: accept } =
|
|
189
|
+
* const { mutate: accept } = useDownloadReceivedMapShare()
|
|
180
190
|
*
|
|
181
191
|
* return <button onClick={() => accept({ shareId })}>Accept</button>
|
|
182
192
|
* }
|
|
@@ -193,17 +203,25 @@ export function useDownloadReceivedMapShare() {
|
|
|
193
203
|
}
|
|
194
204
|
/**
|
|
195
205
|
* Decline a map share that has been received. Notifies the sender that the
|
|
196
|
-
* share was declined.
|
|
206
|
+
* share was declined. The share status is only updated to `'declined'` after
|
|
207
|
+
* the server confirms the decline — there is no optimistic update.
|
|
208
|
+
*
|
|
209
|
+
* If the sender canceled the share before the decline reaches the server, the
|
|
210
|
+
* share status will transition to `'canceled'` (not `'error'`) and the
|
|
211
|
+
* mutation will throw a `MapShareCanceledError`.
|
|
197
212
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
213
|
+
* @throws {MapShareCanceledError} If the share is already known to be
|
|
214
|
+
* canceled, or if the server reports that the sender canceled the share
|
|
215
|
+
* while the decline was in flight. In both cases `share.status` will be
|
|
216
|
+
* `'canceled'`.
|
|
217
|
+
* @throws {InvalidStatusTransitionError} If the share is not in
|
|
218
|
+
* `status='pending'` (e.g. already downloading, completed, or declined).
|
|
201
219
|
*
|
|
202
220
|
* @example
|
|
203
221
|
* ```tsx
|
|
204
222
|
* import { DeclineReason } from '@comapeo/core-react'
|
|
205
223
|
* function DeclineButton({ shareId }: { shareId: string }) {
|
|
206
|
-
* const { mutate: decline } =
|
|
224
|
+
* const { mutate: decline } = useDeclineReceivedMapShare()
|
|
207
225
|
*
|
|
208
226
|
* return (
|
|
209
227
|
* <button onClick={() => decline({ shareId, reason: DeclineReason.user_rejected })}>
|
|
@@ -225,13 +243,15 @@ export function useDeclineReceivedMapShare() {
|
|
|
225
243
|
/**
|
|
226
244
|
* Abort an in-progress map share download.
|
|
227
245
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
246
|
+
* @throws {MapShareCanceledError} If the share is already known to be canceled.
|
|
247
|
+
* @throws {InvalidStatusTransitionError} If the share is not in
|
|
248
|
+
* `status='downloading'` (e.g. still pending, already completed, or
|
|
249
|
+
* declined).
|
|
230
250
|
*
|
|
231
251
|
* @example
|
|
232
252
|
* ```tsx
|
|
233
253
|
* function AbortButton({ shareId }: { shareId: string }) {
|
|
234
|
-
* const { mutate: abort } =
|
|
254
|
+
* const { mutate: abort } = useAbortReceivedMapShareDownload()
|
|
235
255
|
*
|
|
236
256
|
* return <button onClick={() => abort({ shareId })}>Cancel Download</button>
|
|
237
257
|
* }
|
|
@@ -255,8 +275,6 @@ export function useAbortReceivedMapShareDownload() {
|
|
|
255
275
|
* mutation resolves with the created map share object, including its ID, which
|
|
256
276
|
* can be used to track the share status with `useSingleSentMapShare`.
|
|
257
277
|
*
|
|
258
|
-
* @param opts.projectId Public ID of project for sending the map share: you can only send map shares to users on the same project
|
|
259
|
-
*
|
|
260
278
|
* @example
|
|
261
279
|
* ```tsx
|
|
262
280
|
* function SendMapButton({ projectId, deviceId }: { projectId: string; deviceId: string }) {
|
|
@@ -265,7 +283,7 @@ export function useAbortReceivedMapShareDownload() {
|
|
|
265
283
|
* return (
|
|
266
284
|
* <button
|
|
267
285
|
* onClick={() =>
|
|
268
|
-
* send({
|
|
286
|
+
* send({ receiverDeviceId: deviceId, mapId: 'custom' }, {
|
|
269
287
|
* onSuccess: (mapShare) => {
|
|
270
288
|
* console.log('Share sent with id', mapShare.shareId)
|
|
271
289
|
* }
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { useCreateDocument, useDeleteDocument, useManyDocs, usePresetsSelection,
|
|
|
4
4
|
export { useAcceptInvite, useManyInvites, useRejectInvite, useRequestCancelInvite, useSendInvite, useSingleInvite, } from './hooks/invites.js';
|
|
5
5
|
export { useMapStyleUrl, useImportCustomMapFile, useRemoveCustomMapFile, useGetCustomMapInfo, useManyReceivedMapShares, useSingleReceivedMapShare, useDeclineReceivedMapShare, useDownloadReceivedMapShare, useAbortReceivedMapShareDownload, useSendMapShare, useCancelSentMapShare, useSingleSentMapShare, } from './hooks/maps.js';
|
|
6
6
|
export type { SentMapShareState, ReceivedMapShareState, AbortMapShareOptions, CancelMapShareOptions, DeclineMapShareOptions, DownloadMapShareOptions, CreateAndSendMapShareOptions, } from './lib/map-shares-stores.js';
|
|
7
|
-
export { DeclineReason } from './lib/map-shares-stores.js';
|
|
7
|
+
export { DeclineReason, MapShareErrorCode, getErrorCode, MapShareCanceledError, InvalidStatusTransitionError, } from './lib/map-shares-stores.js';
|
|
8
8
|
export { useAddServerPeer, useAttachmentUrl, useConnectSyncServers, useCreateBlob, useCreateProject, useDataSyncProgress, useDisconnectSyncServers, useDocumentCreatedBy, useIconUrl, useImportProjectCategories, useImportProjectConfig, useLeaveProject, useManyMembers, useManyProjects, useOwnRoleInProject, useProjectOwnRoleChangeListener, useProjectSettings, useRemoveServerPeer, useRemoveMember, useSetAutostopDataSyncTimeout, useSingleMember, useSingleProject, useStartSync, useStopSync, useSyncState, useUpdateProjectSettings, useChangeMemberRole, useExportGeoJSON, useExportZipFile, } from './hooks/projects.js';
|
|
9
9
|
export type { SyncState } from './lib/sync.js';
|
|
10
10
|
export type { WriteableDocument, WriteableDocumentType, WriteableValue, } from './lib/types.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -3,6 +3,6 @@ export { useClientApi, useIsArchiveDevice, useOwnDeviceInfo, useSetIsArchiveDevi
|
|
|
3
3
|
export { useCreateDocument, useDeleteDocument, useManyDocs, usePresetsSelection, useSingleDocByDocId, useSingleDocByVersionId, useUpdateDocument, } from './hooks/documents.js';
|
|
4
4
|
export { useAcceptInvite, useManyInvites, useRejectInvite, useRequestCancelInvite, useSendInvite, useSingleInvite, } from './hooks/invites.js';
|
|
5
5
|
export { useMapStyleUrl, useImportCustomMapFile, useRemoveCustomMapFile, useGetCustomMapInfo, useManyReceivedMapShares, useSingleReceivedMapShare, useDeclineReceivedMapShare, useDownloadReceivedMapShare, useAbortReceivedMapShareDownload, useSendMapShare, useCancelSentMapShare, useSingleSentMapShare, } from './hooks/maps.js';
|
|
6
|
-
export { DeclineReason } from './lib/map-shares-stores.js';
|
|
6
|
+
export { DeclineReason, MapShareErrorCode, getErrorCode, MapShareCanceledError, InvalidStatusTransitionError, } from './lib/map-shares-stores.js';
|
|
7
7
|
export { useAddServerPeer, useAttachmentUrl, useConnectSyncServers, useCreateBlob, useCreateProject, useDataSyncProgress, useDisconnectSyncServers, useDocumentCreatedBy, useIconUrl, useImportProjectCategories, useImportProjectConfig, useLeaveProject, useManyMembers, useManyProjects, useOwnRoleInProject, useProjectOwnRoleChangeListener, useProjectSettings, useRemoveServerPeer, useRemoveMember, useSetAutostopDataSyncTimeout, useSingleMember, useSingleProject, useStartSync, useStopSync, useSyncState, useUpdateProjectSettings, useChangeMemberRole, useExportGeoJSON, useExportZipFile, } from './hooks/projects.js';
|
|
8
8
|
export { HTTPError, isHTTPError } from './lib/http.js';
|
|
@@ -9,6 +9,107 @@ export type ReceivedMapShareState = DistributedIntersection<Simplify<MapShare>,
|
|
|
9
9
|
export type SentMapShareState = ServerMapShareState;
|
|
10
10
|
export type ReceivedMapSharesStore = ReturnType<typeof createReceivedMapSharesStore>;
|
|
11
11
|
export type SentMapSharesStore = ReturnType<typeof createSentMapSharesStore>;
|
|
12
|
+
/**
|
|
13
|
+
* Error codes for map share operations. Use with {@link getErrorCode} to safely
|
|
14
|
+
* check the error code of an unknown error thrown by a map share mutation, or
|
|
15
|
+
* to check the `error.code` on a share in `status='error'`.
|
|
16
|
+
*
|
|
17
|
+
* ## Receiver errors
|
|
18
|
+
*
|
|
19
|
+
* **Mutation errors** (thrown by receiver hooks, check via
|
|
20
|
+
* `getErrorCode(mutation.error)`):
|
|
21
|
+
* - `MAP_SHARE_CANCELED` — the sender canceled the share
|
|
22
|
+
* - `INVALID_STATUS_TRANSITION` — the action is not valid for the share's
|
|
23
|
+
* current status (e.g. declining a share that is already downloading)
|
|
24
|
+
* - `MAP_SHARE_NOT_FOUND` — no share with the given `shareId` exists
|
|
25
|
+
* - `DOWNLOAD_NOT_FOUND` — abort was called but no download is tracked for
|
|
26
|
+
* this share
|
|
27
|
+
*
|
|
28
|
+
* **Share state errors** (on received `share.error.code` when
|
|
29
|
+
* `share.status === 'error'`):
|
|
30
|
+
* - `DOWNLOAD_ERROR` — the download failed (network, disk, or server error)
|
|
31
|
+
* - `DECLINE_CANNOT_CONNECT` — the decline was accepted locally but the sender
|
|
32
|
+
* could not be reached to notify them
|
|
33
|
+
*
|
|
34
|
+
* ## Sender errors
|
|
35
|
+
*
|
|
36
|
+
* **Mutation errors** (thrown by sender hooks, check via
|
|
37
|
+
* `getErrorCode(mutation.error)`):
|
|
38
|
+
* - `INVALID_STATUS_TRANSITION` — the action is not valid for the share's
|
|
39
|
+
* current status (e.g. canceling a share that is already canceled)
|
|
40
|
+
* - `MAP_SHARE_NOT_FOUND` — no share with the given `shareId` exists
|
|
41
|
+
*
|
|
42
|
+
* **Share state errors** (on sent `share.error.code` when
|
|
43
|
+
* `share.status === 'error'`):
|
|
44
|
+
* - `CANCEL_SHARE_NOT_CANCELABLE` — the cancel request reached the server but
|
|
45
|
+
* the share is already in a final state (e.g. completed or declined)
|
|
46
|
+
*
|
|
47
|
+
* ## Common
|
|
48
|
+
*
|
|
49
|
+
* - `UNKNOWN_ERROR` — fallback when the original error has no specific code.
|
|
50
|
+
* Can appear as both a mutation error and a share state error for either
|
|
51
|
+
* sender or receiver.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* import { getErrorCode, MapShareErrorCode } from '@comapeo/core-react'
|
|
56
|
+
*
|
|
57
|
+
* // Receiver: checking a mutation error
|
|
58
|
+
* const decline = useDeclineReceivedMapShare()
|
|
59
|
+
* // ... after mutation fails:
|
|
60
|
+
* if (getErrorCode(decline.error) === MapShareErrorCode.MAP_SHARE_CANCELED) {
|
|
61
|
+
* // Show "this share was canceled by the sender"
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* // Receiver: checking a share state error
|
|
68
|
+
* const share = useSingleReceivedMapShare({ shareId })
|
|
69
|
+
* if (share.status === 'error') {
|
|
70
|
+
* if (share.error.code === MapShareErrorCode.DOWNLOAD_ERROR) {
|
|
71
|
+
* // Show "download failed, try again?"
|
|
72
|
+
* }
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare const MapShareErrorCode: {
|
|
77
|
+
/** Receiver: the sender canceled the share before the action could complete */
|
|
78
|
+
readonly MAP_SHARE_CANCELED: "MAP_SHARE_CANCELED";
|
|
79
|
+
/** Receiver/Sender: the action is not valid for the share's current status */
|
|
80
|
+
readonly INVALID_STATUS_TRANSITION: "INVALID_STATUS_TRANSITION";
|
|
81
|
+
/** Receiver/Sender: no map share with the given `shareId` exists in the store */
|
|
82
|
+
readonly MAP_SHARE_NOT_FOUND: "MAP_SHARE_NOT_FOUND";
|
|
83
|
+
/** Receiver: abort was called but no download is tracked for this share */
|
|
84
|
+
readonly DOWNLOAD_NOT_FOUND: "DOWNLOAD_NOT_FOUND";
|
|
85
|
+
/** Receiver: the download failed due to a network, disk, or server error */
|
|
86
|
+
readonly DOWNLOAD_ERROR: "DOWNLOAD_ERROR";
|
|
87
|
+
/** Receiver: could not connect to the sender to notify them of the decline */
|
|
88
|
+
readonly DECLINE_CANNOT_CONNECT: "DECLINE_CANNOT_CONNECT";
|
|
89
|
+
/** Sender: cancel failed because the share is already in a final state on the server */
|
|
90
|
+
readonly CANCEL_SHARE_NOT_CANCELABLE: "CANCEL_SHARE_NOT_CANCELABLE";
|
|
91
|
+
/** Receiver/Sender: fallback code when the original error has no specific code */
|
|
92
|
+
readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Safely extract the `code` property from an unknown error. Returns `undefined`
|
|
96
|
+
* if the value is not an Error or has no string `code` property.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* import { getErrorCode, MapShareErrorCode } from '@comapeo/core-react'
|
|
101
|
+
*
|
|
102
|
+
* try {
|
|
103
|
+
* await decline.mutateAsync({ shareId, reason: 'user_rejected' })
|
|
104
|
+
* } catch (e) {
|
|
105
|
+
* const code = getErrorCode(e)
|
|
106
|
+
* if (code === MapShareErrorCode.MAP_SHARE_CANCELED) {
|
|
107
|
+
* // handle cancellation
|
|
108
|
+
* }
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function getErrorCode(error: unknown): string | undefined;
|
|
12
113
|
/** Known reasons for declining a map share */
|
|
13
114
|
export declare const DeclineReason: {
|
|
14
115
|
/** User explicitly rejected the map share */
|
|
@@ -35,8 +136,6 @@ export type AbortMapShareOptions = {
|
|
|
35
136
|
};
|
|
36
137
|
/** Options for creating and sending a map share */
|
|
37
138
|
export type CreateAndSendMapShareOptions = {
|
|
38
|
-
/** Public ID of the project to send the share on behalf of */
|
|
39
|
-
projectId: string;
|
|
40
139
|
/** Device ID of the recipient */
|
|
41
140
|
receiverDeviceId: string;
|
|
42
141
|
/** ID of the map to share - not needed until we support multiple maps */
|
|
@@ -47,6 +146,24 @@ export type CancelMapShareOptions = {
|
|
|
47
146
|
/** ID of the map share to cancel */
|
|
48
147
|
shareId: string;
|
|
49
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* Thrown when a receiver action (download, decline, or abort) is attempted on a
|
|
151
|
+
* map share that has been canceled by the sender. Has `code: 'MAP_SHARE_CANCELED'`.
|
|
152
|
+
*/
|
|
153
|
+
export declare class MapShareCanceledError extends Error {
|
|
154
|
+
code: "MAP_SHARE_CANCELED";
|
|
155
|
+
constructor(shareId: string);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Thrown when an action is attempted on a map share whose current status does
|
|
159
|
+
* not allow the requested transition (e.g. declining a share that is already
|
|
160
|
+
* downloading, or aborting a share that is still pending).
|
|
161
|
+
* Has `code: 'INVALID_STATUS_TRANSITION'`.
|
|
162
|
+
*/
|
|
163
|
+
export declare class InvalidStatusTransitionError extends Error {
|
|
164
|
+
code: "INVALID_STATUS_TRANSITION";
|
|
165
|
+
constructor(current: string, next: string);
|
|
166
|
+
}
|
|
50
167
|
/**
|
|
51
168
|
* Store and actions for received map shares.
|
|
52
169
|
*/
|
|
@@ -73,7 +190,7 @@ export declare function createSentMapSharesStore({ clientApi, mapServerApi, }: {
|
|
|
73
190
|
subscribe: (listener: () => void) => () => boolean;
|
|
74
191
|
getSnapshot: () => ServerMapShareState[];
|
|
75
192
|
actions: {
|
|
76
|
-
createAndSend({
|
|
193
|
+
createAndSend({ receiverDeviceId, mapId, }: CreateAndSendMapShareOptions): Promise<ServerMapShareState>;
|
|
77
194
|
cancel({ shareId }: CancelMapShareOptions): Promise<void>;
|
|
78
195
|
};
|
|
79
196
|
};
|
|
@@ -8,6 +8,118 @@ import { invalidateMapQueries } from './react-query.js';
|
|
|
8
8
|
// functions - if the documentation comments are added inline for the store
|
|
9
9
|
// actions, they do not show for the mutate() function in hooks.
|
|
10
10
|
// ============================================
|
|
11
|
+
/**
|
|
12
|
+
* Error codes for map share operations. Use with {@link getErrorCode} to safely
|
|
13
|
+
* check the error code of an unknown error thrown by a map share mutation, or
|
|
14
|
+
* to check the `error.code` on a share in `status='error'`.
|
|
15
|
+
*
|
|
16
|
+
* ## Receiver errors
|
|
17
|
+
*
|
|
18
|
+
* **Mutation errors** (thrown by receiver hooks, check via
|
|
19
|
+
* `getErrorCode(mutation.error)`):
|
|
20
|
+
* - `MAP_SHARE_CANCELED` — the sender canceled the share
|
|
21
|
+
* - `INVALID_STATUS_TRANSITION` — the action is not valid for the share's
|
|
22
|
+
* current status (e.g. declining a share that is already downloading)
|
|
23
|
+
* - `MAP_SHARE_NOT_FOUND` — no share with the given `shareId` exists
|
|
24
|
+
* - `DOWNLOAD_NOT_FOUND` — abort was called but no download is tracked for
|
|
25
|
+
* this share
|
|
26
|
+
*
|
|
27
|
+
* **Share state errors** (on received `share.error.code` when
|
|
28
|
+
* `share.status === 'error'`):
|
|
29
|
+
* - `DOWNLOAD_ERROR` — the download failed (network, disk, or server error)
|
|
30
|
+
* - `DECLINE_CANNOT_CONNECT` — the decline was accepted locally but the sender
|
|
31
|
+
* could not be reached to notify them
|
|
32
|
+
*
|
|
33
|
+
* ## Sender errors
|
|
34
|
+
*
|
|
35
|
+
* **Mutation errors** (thrown by sender hooks, check via
|
|
36
|
+
* `getErrorCode(mutation.error)`):
|
|
37
|
+
* - `INVALID_STATUS_TRANSITION` — the action is not valid for the share's
|
|
38
|
+
* current status (e.g. canceling a share that is already canceled)
|
|
39
|
+
* - `MAP_SHARE_NOT_FOUND` — no share with the given `shareId` exists
|
|
40
|
+
*
|
|
41
|
+
* **Share state errors** (on sent `share.error.code` when
|
|
42
|
+
* `share.status === 'error'`):
|
|
43
|
+
* - `CANCEL_SHARE_NOT_CANCELABLE` — the cancel request reached the server but
|
|
44
|
+
* the share is already in a final state (e.g. completed or declined)
|
|
45
|
+
*
|
|
46
|
+
* ## Common
|
|
47
|
+
*
|
|
48
|
+
* - `UNKNOWN_ERROR` — fallback when the original error has no specific code.
|
|
49
|
+
* Can appear as both a mutation error and a share state error for either
|
|
50
|
+
* sender or receiver.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import { getErrorCode, MapShareErrorCode } from '@comapeo/core-react'
|
|
55
|
+
*
|
|
56
|
+
* // Receiver: checking a mutation error
|
|
57
|
+
* const decline = useDeclineReceivedMapShare()
|
|
58
|
+
* // ... after mutation fails:
|
|
59
|
+
* if (getErrorCode(decline.error) === MapShareErrorCode.MAP_SHARE_CANCELED) {
|
|
60
|
+
* // Show "this share was canceled by the sender"
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* // Receiver: checking a share state error
|
|
67
|
+
* const share = useSingleReceivedMapShare({ shareId })
|
|
68
|
+
* if (share.status === 'error') {
|
|
69
|
+
* if (share.error.code === MapShareErrorCode.DOWNLOAD_ERROR) {
|
|
70
|
+
* // Show "download failed, try again?"
|
|
71
|
+
* }
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export const MapShareErrorCode = {
|
|
76
|
+
// --- Receiver mutation errors (thrown by receiver actions) ---
|
|
77
|
+
/** Receiver: the sender canceled the share before the action could complete */
|
|
78
|
+
MAP_SHARE_CANCELED: 'MAP_SHARE_CANCELED',
|
|
79
|
+
/** Receiver/Sender: the action is not valid for the share's current status */
|
|
80
|
+
INVALID_STATUS_TRANSITION: 'INVALID_STATUS_TRANSITION',
|
|
81
|
+
/** Receiver/Sender: no map share with the given `shareId` exists in the store */
|
|
82
|
+
MAP_SHARE_NOT_FOUND: 'MAP_SHARE_NOT_FOUND',
|
|
83
|
+
/** Receiver: abort was called but no download is tracked for this share */
|
|
84
|
+
DOWNLOAD_NOT_FOUND: 'DOWNLOAD_NOT_FOUND',
|
|
85
|
+
// --- Receiver share state errors (in share.error.code) ---
|
|
86
|
+
/** Receiver: the download failed due to a network, disk, or server error */
|
|
87
|
+
DOWNLOAD_ERROR: 'DOWNLOAD_ERROR',
|
|
88
|
+
/** Receiver: could not connect to the sender to notify them of the decline */
|
|
89
|
+
DECLINE_CANNOT_CONNECT: 'DECLINE_CANNOT_CONNECT',
|
|
90
|
+
// --- Sender share state errors (in share.error.code) ---
|
|
91
|
+
/** Sender: cancel failed because the share is already in a final state on the server */
|
|
92
|
+
CANCEL_SHARE_NOT_CANCELABLE: 'CANCEL_SHARE_NOT_CANCELABLE',
|
|
93
|
+
// --- Common ---
|
|
94
|
+
/** Receiver/Sender: fallback code when the original error has no specific code */
|
|
95
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Safely extract the `code` property from an unknown error. Returns `undefined`
|
|
99
|
+
* if the value is not an Error or has no string `code` property.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* import { getErrorCode, MapShareErrorCode } from '@comapeo/core-react'
|
|
104
|
+
*
|
|
105
|
+
* try {
|
|
106
|
+
* await decline.mutateAsync({ shareId, reason: 'user_rejected' })
|
|
107
|
+
* } catch (e) {
|
|
108
|
+
* const code = getErrorCode(e)
|
|
109
|
+
* if (code === MapShareErrorCode.MAP_SHARE_CANCELED) {
|
|
110
|
+
* // handle cancellation
|
|
111
|
+
* }
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function getErrorCode(error) {
|
|
116
|
+
if (error instanceof Error &&
|
|
117
|
+
'code' in error &&
|
|
118
|
+
typeof error.code === 'string') {
|
|
119
|
+
return error.code;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
11
123
|
/** Known reasons for declining a map share */
|
|
12
124
|
export const DeclineReason = {
|
|
13
125
|
/** User explicitly rejected the map share */
|
|
@@ -15,6 +127,30 @@ export const DeclineReason = {
|
|
|
15
127
|
/** Device storage is full */
|
|
16
128
|
storage_full: 'storage_full',
|
|
17
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* Thrown when a receiver action (download, decline, or abort) is attempted on a
|
|
132
|
+
* map share that has been canceled by the sender. Has `code: 'MAP_SHARE_CANCELED'`.
|
|
133
|
+
*/
|
|
134
|
+
export class MapShareCanceledError extends Error {
|
|
135
|
+
code = 'MAP_SHARE_CANCELED';
|
|
136
|
+
constructor(shareId) {
|
|
137
|
+
super(`Map share ${shareId} has been canceled by the sender`);
|
|
138
|
+
this.name = 'MapShareCanceledError';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Thrown when an action is attempted on a map share whose current status does
|
|
143
|
+
* not allow the requested transition (e.g. declining a share that is already
|
|
144
|
+
* downloading, or aborting a share that is still pending).
|
|
145
|
+
* Has `code: 'INVALID_STATUS_TRANSITION'`.
|
|
146
|
+
*/
|
|
147
|
+
export class InvalidStatusTransitionError extends Error {
|
|
148
|
+
code = 'INVALID_STATUS_TRANSITION';
|
|
149
|
+
constructor(current, next) {
|
|
150
|
+
super(`Invalid status transition from ${current} to ${next}`);
|
|
151
|
+
this.name = 'InvalidStatusTransitionError';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
18
154
|
/**
|
|
19
155
|
* This is like a mini zustand store. Keeping the map shares in an external
|
|
20
156
|
* store avoids unnecessary re-renders of the entire app when map shares are
|
|
@@ -135,6 +271,13 @@ export function createReceivedMapSharesStore({ clientApi, mapServerApi, queryCli
|
|
|
135
271
|
const actions = {
|
|
136
272
|
async download({ shareId }) {
|
|
137
273
|
const mapShare = get(shareId);
|
|
274
|
+
// This path should be be impossible, because the map share only receives
|
|
275
|
+
// status updates from the receiver after the download starts, but adding
|
|
276
|
+
// for completeness, and the edge-case of the receiving trying to
|
|
277
|
+
// download() a second time.
|
|
278
|
+
if (mapShare.status === 'canceled') {
|
|
279
|
+
throw new MapShareCanceledError(shareId);
|
|
280
|
+
}
|
|
138
281
|
update(shareId, { status: 'downloading', bytesDownloaded: 0 });
|
|
139
282
|
try {
|
|
140
283
|
const downloadIdPromise = mapServerApi
|
|
@@ -176,22 +319,39 @@ export function createReceivedMapSharesStore({ clientApi, mapServerApi, queryCli
|
|
|
176
319
|
},
|
|
177
320
|
async decline({ shareId, reason }) {
|
|
178
321
|
const mapShare = get(shareId);
|
|
179
|
-
|
|
322
|
+
if (mapShare.status === 'canceled') {
|
|
323
|
+
throw new MapShareCanceledError(shareId);
|
|
324
|
+
}
|
|
325
|
+
if (mapShare.status !== 'pending') {
|
|
326
|
+
throw new InvalidStatusTransitionError(mapShare.status, 'declined');
|
|
327
|
+
}
|
|
180
328
|
try {
|
|
181
|
-
await mapServerApi
|
|
329
|
+
await mapServerApi
|
|
330
|
+
.post(`mapShares/${shareId}/decline`, {
|
|
182
331
|
json: {
|
|
183
332
|
senderDeviceId: mapShare.senderDeviceId,
|
|
184
333
|
mapShareUrls: mapShare.mapShareUrls,
|
|
185
334
|
reason,
|
|
186
335
|
},
|
|
187
|
-
})
|
|
336
|
+
})
|
|
337
|
+
.json();
|
|
338
|
+
update(shareId, { status: 'declined', reason });
|
|
188
339
|
}
|
|
189
340
|
catch (e) {
|
|
341
|
+
const error = ensureError(e);
|
|
342
|
+
if ('code' in error && error.code === 'MAP_SHARE_CANCELED') {
|
|
343
|
+
update(shareId, { status: 'canceled' });
|
|
344
|
+
throw new MapShareCanceledError(shareId);
|
|
345
|
+
}
|
|
190
346
|
handleError(shareId, e);
|
|
191
347
|
throw e;
|
|
192
348
|
}
|
|
193
349
|
},
|
|
194
350
|
async abort({ shareId }) {
|
|
351
|
+
const mapShare = get(shareId);
|
|
352
|
+
if (mapShare.status === 'canceled') {
|
|
353
|
+
throw new MapShareCanceledError(shareId);
|
|
354
|
+
}
|
|
195
355
|
update(shareId, { status: 'aborted' });
|
|
196
356
|
try {
|
|
197
357
|
const downloadId = await downloads.get(shareId);
|
|
@@ -221,15 +381,14 @@ export function createReceivedMapSharesStore({ clientApi, mapServerApi, queryCli
|
|
|
221
381
|
export function createSentMapSharesStore({ clientApi, mapServerApi, }) {
|
|
222
382
|
const { subscribe, getSnapshot, update, add, handleError, monitor } = createMapSharesStore({ mapServerApi });
|
|
223
383
|
const actions = {
|
|
224
|
-
async createAndSend({
|
|
384
|
+
async createAndSend({ receiverDeviceId, mapId = CUSTOM_MAP_ID, }) {
|
|
225
385
|
const mapShare = await mapServerApi
|
|
226
386
|
.post('mapShares', {
|
|
227
387
|
json: { receiverDeviceId, mapId },
|
|
228
388
|
})
|
|
229
389
|
.json();
|
|
230
390
|
try {
|
|
231
|
-
|
|
232
|
-
await project.$sendMapShare(mapShare);
|
|
391
|
+
await clientApi.sendMapShare(mapShare);
|
|
233
392
|
}
|
|
234
393
|
catch (e) {
|
|
235
394
|
await mapServerApi.post(`mapShares/${mapShare.shareId}/cancel`);
|
|
@@ -271,7 +430,7 @@ const allowedStatusTransitions = {
|
|
|
271
430
|
*/
|
|
272
431
|
function assertValidStatusTransition(current, next) {
|
|
273
432
|
if (!allowedStatusTransitions[current].includes(next)) {
|
|
274
|
-
throw new
|
|
433
|
+
throw new InvalidStatusTransitionError(current, next);
|
|
275
434
|
}
|
|
276
435
|
}
|
|
277
436
|
const finalStatuses = [
|