@comapeo/core-react 2.0.0 → 3.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.
@@ -1,14 +1,14 @@
1
1
  import type { MapeoClientApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' };
2
- import { type ReactNode } from 'react';
2
+ import { type JSX, type ReactNode } from 'react';
3
3
  export declare const ClientApiContext: import("react").Context<import("@comapeo/ipc/dist/client.js", { with: { "resolution-mode": "import" } }).MapeoClientApi | null>;
4
4
  /**
5
5
  * Create a context provider that holds a CoMapeo API client instance.
6
6
  *
7
- * @param opts
8
- * @param {ReactNode} opts.children React children node
9
- * @param {MapeoClientApi} opts.clientApi Client API instance
7
+ * @param opts.children React children node
8
+ * @param opts.clientApi Client API instance
9
+ *
10
10
  */
11
11
  export declare function ClientApiProvider({ children, clientApi, }: {
12
12
  children: ReactNode;
13
13
  clientApi: MapeoClientApi;
14
- }): import("react").FunctionComponentElement<import("react").ProviderProps<import("@comapeo/ipc/dist/client.js", { with: { "resolution-mode": "import" } }).MapeoClientApi | null>>;
14
+ }): JSX.Element;
@@ -7,9 +7,9 @@ exports.ClientApiContext = (0, react_1.createContext)(null);
7
7
  /**
8
8
  * Create a context provider that holds a CoMapeo API client instance.
9
9
  *
10
- * @param opts
11
- * @param {ReactNode} opts.children React children node
12
- * @param {MapeoClientApi} opts.clientApi Client API instance
10
+ * @param opts.children React children node
11
+ * @param opts.clientApi Client API instance
12
+ *
13
13
  */
14
14
  function ClientApiProvider({ children, clientApi, }) {
15
15
  return (0, react_1.createElement)(exports.ClientApiContext.Provider, { value: clientApi }, children);
@@ -1,5 +1,5 @@
1
1
  import type { MapeoDoc } from '@comapeo/schema' with { 'resolution-mode': 'import' };
2
- import { type WriteableDocumentType } from '../lib/react-query/documents.js';
2
+ import type { WriteableDocumentType } from '../lib/types.js';
3
3
  type ReadHookResult<D> = {
4
4
  data: D;
5
5
  error: Error | null;
@@ -106,9 +106,9 @@ export declare function useManyDocs<D extends WriteableDocumentType>({ projectId
106
106
  docType: D;
107
107
  includeDeleted?: boolean;
108
108
  lang?: string;
109
- }): ReadHookResult<Extract<MapeoDoc, {
109
+ }): ReadHookResult<Array<Extract<MapeoDoc, {
110
110
  schemaName: D;
111
- }>>;
111
+ }>>>;
112
112
  /**
113
113
  * Create a document for a project.
114
114
  *
@@ -119,10 +119,10 @@ export declare function useCreateDocument<D extends WriteableDocumentType>({ doc
119
119
  docType: D;
120
120
  projectId: string;
121
121
  }): {
122
- mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/react-query/documents.js").WriteableDocument<D> & {
122
+ mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/types.js").WriteableDocument<D> & {
123
123
  forks: Array<string>;
124
124
  }, Error, {
125
- value: Omit<import("../lib/react-query/documents.js").WriteableValue<D>, "schemaName">;
125
+ value: Omit<import("../lib/types.js").WriteableValue<D>, "schemaName">;
126
126
  }, unknown>;
127
127
  reset: () => void;
128
128
  status: "pending" | "error" | "success" | "idle";
@@ -137,11 +137,11 @@ export declare function useUpdateDocument<D extends WriteableDocumentType>({ doc
137
137
  docType: D;
138
138
  projectId: string;
139
139
  }): {
140
- mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/react-query/documents.js").WriteableDocument<D> & {
140
+ mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/types.js").WriteableDocument<D> & {
141
141
  forks: Array<string>;
142
142
  }, Error, {
143
143
  versionId: string;
144
- value: Omit<import("../lib/react-query/documents.js").WriteableValue<D>, "schemaName">;
144
+ value: Omit<import("../lib/types.js").WriteableValue<D>, "schemaName">;
145
145
  }, unknown>;
146
146
  reset: () => void;
147
147
  status: "pending" | "error" | "success" | "idle";
@@ -156,7 +156,7 @@ export declare function useDeleteDocument<D extends WriteableDocumentType>({ doc
156
156
  docType: D;
157
157
  projectId: string;
158
158
  }): {
159
- mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/react-query/documents.js").WriteableDocument<D> & {
159
+ mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/types.js").WriteableDocument<D> & {
160
160
  forks: Array<string>;
161
161
  }, Error, {
162
162
  docId: string;
@@ -1,5 +1,6 @@
1
1
  import type { BitmapOpts, SvgOpts } from '@comapeo/core/dist/icon-api.js' with { 'resolution-mode': 'import' };
2
2
  import type { BlobId } from '@comapeo/core/dist/types.js' with { 'resolution-mode': 'import' };
3
+ import { type SyncState } from '../lib/sync.js';
3
4
  /**
4
5
  * Retrieve the project settings for a project.
5
6
  *
@@ -332,3 +333,52 @@ export declare function useCreateBlob({ projectId }: {
332
333
  reset: () => void;
333
334
  status: "pending" | "error" | "success" | "idle";
334
335
  };
336
+ /**
337
+ * Hook to subscribe to the current sync state.
338
+ *
339
+ * Creates a global singleton for each project, to minimize traffic over IPC -
340
+ * this hook can safely be used in more than one place without attaching
341
+ * additional listeners across the IPC channel.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * function Example() {
346
+ * const syncState = useSyncState({ projectId });
347
+ *
348
+ * if (!syncState) {
349
+ * // Sync information hasn't been loaded yet
350
+ * }
351
+ *
352
+ * // Actual info about sync state is available...
353
+ * }
354
+ * ```
355
+ *
356
+ * @param opts.projectId Project public ID
357
+ */
358
+ export declare function useSyncState({ projectId, }: {
359
+ projectId: string;
360
+ }): SyncState | null;
361
+ /**
362
+ * Provides the progress of data sync for sync-enabled connected peers
363
+ *
364
+ * @returns `null` if no sync state events have been received. Otherwise returns a value between 0 and 1 (inclusive)
365
+ */
366
+ export declare function useDataSyncProgress({ projectId, }: {
367
+ projectId: string;
368
+ }): number | null;
369
+ export declare function useStartSync({ projectId }: {
370
+ projectId: string;
371
+ }): {
372
+ mutate: import("@tanstack/react-query").UseMutateFunction<void, Error, {
373
+ autostopDataSyncAfter: number | null;
374
+ } | undefined, unknown>;
375
+ reset: () => void;
376
+ status: "pending" | "error" | "success" | "idle";
377
+ };
378
+ export declare function useStopSync({ projectId }: {
379
+ projectId: string;
380
+ }): {
381
+ mutate: import("@tanstack/react-query").UseMutateFunction<void, Error, void, unknown>;
382
+ reset: () => void;
383
+ status: "pending" | "error" | "success" | "idle";
384
+ };
@@ -14,8 +14,14 @@ exports.useLeaveProject = useLeaveProject;
14
14
  exports.useImportProjectConfig = useImportProjectConfig;
15
15
  exports.useUpdateProjectSettings = useUpdateProjectSettings;
16
16
  exports.useCreateBlob = useCreateBlob;
17
+ exports.useSyncState = useSyncState;
18
+ exports.useDataSyncProgress = useDataSyncProgress;
19
+ exports.useStartSync = useStartSync;
20
+ exports.useStopSync = useStopSync;
17
21
  const react_query_1 = require("@tanstack/react-query");
22
+ const react_1 = require("react");
18
23
  const projects_js_1 = require("../lib/react-query/projects.js");
24
+ const sync_js_1 = require("../lib/sync.js");
19
25
  const client_js_1 = require("./client.js");
20
26
  /**
21
27
  * Retrieve the project settings for a project.
@@ -318,3 +324,59 @@ function useCreateBlob({ projectId }) {
318
324
  const { mutate, reset, status } = (0, react_query_1.useMutation)((0, projects_js_1.createBlobMutationOptions)({ projectApi }));
319
325
  return { mutate, reset, status };
320
326
  }
327
+ const PROJECT_SYNC_STORE_MAP = new WeakMap();
328
+ function useSyncStore({ projectId }) {
329
+ const { data: projectApi } = useSingleProject({ projectId });
330
+ let syncStore = PROJECT_SYNC_STORE_MAP.get(projectApi);
331
+ if (!syncStore) {
332
+ syncStore = new sync_js_1.SyncStore(projectApi);
333
+ PROJECT_SYNC_STORE_MAP.set(projectApi, syncStore);
334
+ }
335
+ return syncStore;
336
+ }
337
+ /**
338
+ * Hook to subscribe to the current sync state.
339
+ *
340
+ * Creates a global singleton for each project, to minimize traffic over IPC -
341
+ * this hook can safely be used in more than one place without attaching
342
+ * additional listeners across the IPC channel.
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * function Example() {
347
+ * const syncState = useSyncState({ projectId });
348
+ *
349
+ * if (!syncState) {
350
+ * // Sync information hasn't been loaded yet
351
+ * }
352
+ *
353
+ * // Actual info about sync state is available...
354
+ * }
355
+ * ```
356
+ *
357
+ * @param opts.projectId Project public ID
358
+ */
359
+ function useSyncState({ projectId, }) {
360
+ const syncStore = useSyncStore({ projectId });
361
+ const { subscribe, getStateSnapshot } = syncStore;
362
+ return (0, react_1.useSyncExternalStore)(subscribe, getStateSnapshot);
363
+ }
364
+ /**
365
+ * Provides the progress of data sync for sync-enabled connected peers
366
+ *
367
+ * @returns `null` if no sync state events have been received. Otherwise returns a value between 0 and 1 (inclusive)
368
+ */
369
+ function useDataSyncProgress({ projectId, }) {
370
+ const { subscribe, getDataProgressSnapshot } = useSyncStore({ projectId });
371
+ return (0, react_1.useSyncExternalStore)(subscribe, getDataProgressSnapshot);
372
+ }
373
+ function useStartSync({ projectId }) {
374
+ const { data: projectApi } = useSingleProject({ projectId });
375
+ const { mutate, reset, status } = (0, react_query_1.useMutation)((0, projects_js_1.startSyncMutationOptions)({ projectApi }));
376
+ return { mutate, reset, status };
377
+ }
378
+ function useStopSync({ projectId }) {
379
+ const { data: projectApi } = useSingleProject({ projectId });
380
+ const { mutate, reset, status } = (0, react_query_1.useMutation)((0, projects_js_1.stopSyncMutationOptions)({ projectApi }));
381
+ return { mutate, reset, status };
382
+ }
@@ -3,10 +3,6 @@ export { useClientApi, useIsArchiveDevice, useOwnDeviceInfo, useSetIsArchiveDevi
3
3
  export { useCreateDocument, useDeleteDocument, useManyDocs, useSingleDocByDocId, useSingleDocByVersionId, useUpdateDocument, } from './hooks/documents.js';
4
4
  export { useAcceptInvite, useRejectInvite, useRequestCancelInvite, useSendInvite, } from './hooks/invites.js';
5
5
  export { useMapStyleUrl } from './hooks/maps.js';
6
- export { useAddServerPeer, useAttachmentUrl, useCreateBlob, useCreateProject, useDocumentCreatedBy, useIconUrl, useImportProjectConfig, useLeaveProject, useManyMembers, useManyProjects, useProjectSettings, useSingleMember, useSingleProject, useUpdateProjectSettings, } from './hooks/projects.js';
7
- export { getClientQueryKey, getDeviceInfoQueryKey, getIsArchiveDeviceQueryKey, } from './lib/react-query/client.js';
8
- export { getDocumentByDocIdQueryKey, getDocumentByVersionIdQueryKey, getDocumentsQueryKey, getManyDocumentsQueryKey, type WriteableDocument, type WriteableDocumentType, type WriteableValue, } from './lib/react-query/documents.js';
9
- export { getInvitesQueryKey, getPendingInvitesQueryKey, } from './lib/react-query/invites.js';
10
- export { getMapsQueryKey, getStyleJsonUrlQueryKey, } from './lib/react-query/maps.js';
11
- export { getAttachmentUrlQueryKey, getDocumentCreatedByQueryKey, getIconUrlQueryKey, getMemberByIdQueryKey, getMembersQueryKey, getProjectByIdQueryKey, getProjectRoleQueryKey, getProjectSettingsQueryKey, getProjectsQueryKey, } from './lib/react-query/projects.js';
12
- export { ROOT_QUERY_KEY } from './lib/react-query/shared.js';
6
+ export { useAddServerPeer, useAttachmentUrl, useCreateBlob, useCreateProject, useDataSyncProgress, useDocumentCreatedBy, useIconUrl, useImportProjectConfig, useLeaveProject, useManyMembers, useManyProjects, useProjectSettings, useSingleMember, useSingleProject, useStartSync, useStopSync, useSyncState, useUpdateProjectSettings, } from './hooks/projects.js';
7
+ export { type SyncState } from './lib/sync.js';
8
+ export { type WriteableDocument, type WriteableDocumentType, type WriteableValue, } from './lib/types.js';
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getProjectRoleQueryKey = exports.getProjectByIdQueryKey = exports.getMembersQueryKey = exports.getMemberByIdQueryKey = exports.getIconUrlQueryKey = exports.getDocumentCreatedByQueryKey = exports.getAttachmentUrlQueryKey = exports.getStyleJsonUrlQueryKey = exports.getMapsQueryKey = exports.getPendingInvitesQueryKey = exports.getInvitesQueryKey = exports.getManyDocumentsQueryKey = exports.getDocumentsQueryKey = exports.getDocumentByVersionIdQueryKey = exports.getDocumentByDocIdQueryKey = exports.getIsArchiveDeviceQueryKey = exports.getDeviceInfoQueryKey = exports.getClientQueryKey = exports.useUpdateProjectSettings = exports.useSingleProject = exports.useSingleMember = exports.useProjectSettings = exports.useManyProjects = exports.useManyMembers = exports.useLeaveProject = exports.useImportProjectConfig = exports.useIconUrl = exports.useDocumentCreatedBy = exports.useCreateProject = exports.useCreateBlob = exports.useAttachmentUrl = exports.useAddServerPeer = exports.useMapStyleUrl = exports.useSendInvite = exports.useRequestCancelInvite = exports.useRejectInvite = exports.useAcceptInvite = exports.useUpdateDocument = exports.useSingleDocByVersionId = exports.useSingleDocByDocId = exports.useManyDocs = exports.useDeleteDocument = exports.useCreateDocument = exports.useSetOwnDeviceInfo = exports.useSetIsArchiveDevice = exports.useOwnDeviceInfo = exports.useIsArchiveDevice = exports.useClientApi = exports.ClientApiProvider = exports.ClientApiContext = void 0;
4
- exports.ROOT_QUERY_KEY = exports.getProjectsQueryKey = exports.getProjectSettingsQueryKey = void 0;
3
+ exports.useUpdateProjectSettings = exports.useSyncState = exports.useStopSync = exports.useStartSync = exports.useSingleProject = exports.useSingleMember = exports.useProjectSettings = exports.useManyProjects = exports.useManyMembers = exports.useLeaveProject = exports.useImportProjectConfig = exports.useIconUrl = exports.useDocumentCreatedBy = exports.useDataSyncProgress = exports.useCreateProject = exports.useCreateBlob = exports.useAttachmentUrl = exports.useAddServerPeer = exports.useMapStyleUrl = exports.useSendInvite = exports.useRequestCancelInvite = exports.useRejectInvite = exports.useAcceptInvite = exports.useUpdateDocument = exports.useSingleDocByVersionId = exports.useSingleDocByDocId = exports.useManyDocs = exports.useDeleteDocument = exports.useCreateDocument = exports.useSetOwnDeviceInfo = exports.useSetIsArchiveDevice = exports.useOwnDeviceInfo = exports.useIsArchiveDevice = exports.useClientApi = exports.ClientApiProvider = exports.ClientApiContext = void 0;
5
4
  var ClientApi_js_1 = require("./contexts/ClientApi.js");
6
5
  Object.defineProperty(exports, "ClientApiContext", { enumerable: true, get: function () { return ClientApi_js_1.ClientApiContext; } });
7
6
  Object.defineProperty(exports, "ClientApiProvider", { enumerable: true, get: function () { return ClientApi_js_1.ClientApiProvider; } });
@@ -30,6 +29,7 @@ Object.defineProperty(exports, "useAddServerPeer", { enumerable: true, get: func
30
29
  Object.defineProperty(exports, "useAttachmentUrl", { enumerable: true, get: function () { return projects_js_1.useAttachmentUrl; } });
31
30
  Object.defineProperty(exports, "useCreateBlob", { enumerable: true, get: function () { return projects_js_1.useCreateBlob; } });
32
31
  Object.defineProperty(exports, "useCreateProject", { enumerable: true, get: function () { return projects_js_1.useCreateProject; } });
32
+ Object.defineProperty(exports, "useDataSyncProgress", { enumerable: true, get: function () { return projects_js_1.useDataSyncProgress; } });
33
33
  Object.defineProperty(exports, "useDocumentCreatedBy", { enumerable: true, get: function () { return projects_js_1.useDocumentCreatedBy; } });
34
34
  Object.defineProperty(exports, "useIconUrl", { enumerable: true, get: function () { return projects_js_1.useIconUrl; } });
35
35
  Object.defineProperty(exports, "useImportProjectConfig", { enumerable: true, get: function () { return projects_js_1.useImportProjectConfig; } });
@@ -39,31 +39,7 @@ Object.defineProperty(exports, "useManyProjects", { enumerable: true, get: funct
39
39
  Object.defineProperty(exports, "useProjectSettings", { enumerable: true, get: function () { return projects_js_1.useProjectSettings; } });
40
40
  Object.defineProperty(exports, "useSingleMember", { enumerable: true, get: function () { return projects_js_1.useSingleMember; } });
41
41
  Object.defineProperty(exports, "useSingleProject", { enumerable: true, get: function () { return projects_js_1.useSingleProject; } });
42
+ Object.defineProperty(exports, "useStartSync", { enumerable: true, get: function () { return projects_js_1.useStartSync; } });
43
+ Object.defineProperty(exports, "useStopSync", { enumerable: true, get: function () { return projects_js_1.useStopSync; } });
44
+ Object.defineProperty(exports, "useSyncState", { enumerable: true, get: function () { return projects_js_1.useSyncState; } });
42
45
  Object.defineProperty(exports, "useUpdateProjectSettings", { enumerable: true, get: function () { return projects_js_1.useUpdateProjectSettings; } });
43
- var client_js_2 = require("./lib/react-query/client.js");
44
- Object.defineProperty(exports, "getClientQueryKey", { enumerable: true, get: function () { return client_js_2.getClientQueryKey; } });
45
- Object.defineProperty(exports, "getDeviceInfoQueryKey", { enumerable: true, get: function () { return client_js_2.getDeviceInfoQueryKey; } });
46
- Object.defineProperty(exports, "getIsArchiveDeviceQueryKey", { enumerable: true, get: function () { return client_js_2.getIsArchiveDeviceQueryKey; } });
47
- var documents_js_2 = require("./lib/react-query/documents.js");
48
- Object.defineProperty(exports, "getDocumentByDocIdQueryKey", { enumerable: true, get: function () { return documents_js_2.getDocumentByDocIdQueryKey; } });
49
- Object.defineProperty(exports, "getDocumentByVersionIdQueryKey", { enumerable: true, get: function () { return documents_js_2.getDocumentByVersionIdQueryKey; } });
50
- Object.defineProperty(exports, "getDocumentsQueryKey", { enumerable: true, get: function () { return documents_js_2.getDocumentsQueryKey; } });
51
- Object.defineProperty(exports, "getManyDocumentsQueryKey", { enumerable: true, get: function () { return documents_js_2.getManyDocumentsQueryKey; } });
52
- var invites_js_2 = require("./lib/react-query/invites.js");
53
- Object.defineProperty(exports, "getInvitesQueryKey", { enumerable: true, get: function () { return invites_js_2.getInvitesQueryKey; } });
54
- Object.defineProperty(exports, "getPendingInvitesQueryKey", { enumerable: true, get: function () { return invites_js_2.getPendingInvitesQueryKey; } });
55
- var maps_js_2 = require("./lib/react-query/maps.js");
56
- Object.defineProperty(exports, "getMapsQueryKey", { enumerable: true, get: function () { return maps_js_2.getMapsQueryKey; } });
57
- Object.defineProperty(exports, "getStyleJsonUrlQueryKey", { enumerable: true, get: function () { return maps_js_2.getStyleJsonUrlQueryKey; } });
58
- var projects_js_2 = require("./lib/react-query/projects.js");
59
- Object.defineProperty(exports, "getAttachmentUrlQueryKey", { enumerable: true, get: function () { return projects_js_2.getAttachmentUrlQueryKey; } });
60
- Object.defineProperty(exports, "getDocumentCreatedByQueryKey", { enumerable: true, get: function () { return projects_js_2.getDocumentCreatedByQueryKey; } });
61
- Object.defineProperty(exports, "getIconUrlQueryKey", { enumerable: true, get: function () { return projects_js_2.getIconUrlQueryKey; } });
62
- Object.defineProperty(exports, "getMemberByIdQueryKey", { enumerable: true, get: function () { return projects_js_2.getMemberByIdQueryKey; } });
63
- Object.defineProperty(exports, "getMembersQueryKey", { enumerable: true, get: function () { return projects_js_2.getMembersQueryKey; } });
64
- Object.defineProperty(exports, "getProjectByIdQueryKey", { enumerable: true, get: function () { return projects_js_2.getProjectByIdQueryKey; } });
65
- Object.defineProperty(exports, "getProjectRoleQueryKey", { enumerable: true, get: function () { return projects_js_2.getProjectRoleQueryKey; } });
66
- Object.defineProperty(exports, "getProjectSettingsQueryKey", { enumerable: true, get: function () { return projects_js_2.getProjectSettingsQueryKey; } });
67
- Object.defineProperty(exports, "getProjectsQueryKey", { enumerable: true, get: function () { return projects_js_2.getProjectsQueryKey; } });
68
- var shared_js_1 = require("./lib/react-query/shared.js");
69
- Object.defineProperty(exports, "ROOT_QUERY_KEY", { enumerable: true, get: function () { return shared_js_1.ROOT_QUERY_KEY; } });
@@ -1,13 +1,6 @@
1
1
  import type { MapeoProjectApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' };
2
- import type { MapeoDoc, MapeoValue } from '@comapeo/schema' with { 'resolution-mode': 'import' };
3
2
  import { type QueryClient } from '@tanstack/react-query';
4
- export type WriteableDocumentType = Extract<MapeoDoc['schemaName'], 'field' | 'observation' | 'preset' | 'track' | 'remoteDetectionAlert'>;
5
- export type WriteableValue<D extends WriteableDocumentType> = Extract<MapeoValue, {
6
- schemaName: D;
7
- }>;
8
- export type WriteableDocument<D extends WriteableDocumentType> = Extract<MapeoDoc, {
9
- schemaName: D;
10
- }>;
3
+ import { WriteableDocument, WriteableDocumentType, WriteableValue } from '../types.js';
11
4
  export declare function getDocumentsQueryKey<D extends WriteableDocumentType>({ projectId, docType, }: {
12
5
  projectId: string;
13
6
  docType: D;
@@ -330,3 +330,19 @@ export declare function createBlobMutationOptions({ projectApi, }: {
330
330
  networkMode: "always";
331
331
  retry: false;
332
332
  };
333
+ export declare function startSyncMutationOptions({ projectApi, }: {
334
+ projectApi: MapeoProjectApi;
335
+ }): {
336
+ mutationFn: (opts: {
337
+ autostopDataSyncAfter: number | null;
338
+ } | undefined) => Promise<void>;
339
+ networkMode: "always";
340
+ retry: false;
341
+ };
342
+ export declare function stopSyncMutationOptions({ projectApi, }: {
343
+ projectApi: MapeoProjectApi;
344
+ }): {
345
+ mutationFn: () => Promise<void>;
346
+ networkMode: "always";
347
+ retry: false;
348
+ };
@@ -24,6 +24,8 @@ exports.leaveProjectMutationOptions = leaveProjectMutationOptions;
24
24
  exports.importProjectConfigMutationOptions = importProjectConfigMutationOptions;
25
25
  exports.updateProjectSettingsMutationOptions = updateProjectSettingsMutationOptions;
26
26
  exports.createBlobMutationOptions = createBlobMutationOptions;
27
+ exports.startSyncMutationOptions = startSyncMutationOptions;
28
+ exports.stopSyncMutationOptions = stopSyncMutationOptions;
27
29
  const react_query_1 = require("@tanstack/react-query");
28
30
  const shared_js_1 = require("./shared.js");
29
31
  function getProjectsQueryKey() {
@@ -233,3 +235,21 @@ function createBlobMutationOptions({ projectApi, }) {
233
235
  },
234
236
  };
235
237
  }
238
+ function startSyncMutationOptions({ projectApi, }) {
239
+ return {
240
+ ...(0, shared_js_1.baseMutationOptions)(),
241
+ mutationFn: async (opts) => {
242
+ // Have to avoid passing `undefined` explicitly
243
+ // See https://github.com/digidem/rpc-reflector/issues/21
244
+ return opts ? projectApi.$sync.start(opts) : projectApi.$sync.start();
245
+ },
246
+ };
247
+ }
248
+ function stopSyncMutationOptions({ projectApi, }) {
249
+ return {
250
+ ...(0, shared_js_1.baseMutationOptions)(),
251
+ mutationFn: async () => {
252
+ return projectApi.$sync.stop();
253
+ },
254
+ };
255
+ }
@@ -0,0 +1,10 @@
1
+ import type { MapeoProjectApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' };
2
+ export type SyncState = Awaited<ReturnType<MapeoProjectApi['$sync']['getState']>>;
3
+ export declare function getDataSyncCountForDevice(syncStateForDevice: SyncState['remoteDeviceSyncState'][string]): number;
4
+ export declare class SyncStore {
5
+ #private;
6
+ constructor(project: MapeoProjectApi);
7
+ subscribe: (listener: () => void) => () => void;
8
+ getStateSnapshot: () => import("@comapeo/core/dist/sync/sync-api.js", { with: { "resolution-mode": "import" } }).State | null;
9
+ getDataProgressSnapshot: () => number | null;
10
+ }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SyncStore = void 0;
4
+ exports.getDataSyncCountForDevice = getDataSyncCountForDevice;
5
+ function getDataSyncCountForDevice(syncStateForDevice) {
6
+ const { data } = syncStateForDevice;
7
+ return data.want + data.wanted;
8
+ }
9
+ class SyncStore {
10
+ #project;
11
+ #listeners = new Set();
12
+ #isSubscribedInternal = false;
13
+ #error = null;
14
+ #state = null;
15
+ // Used for calculating sync progress
16
+ #perDeviceMaxSyncCount = new Map();
17
+ constructor(project) {
18
+ this.#project = project;
19
+ }
20
+ subscribe = (listener) => {
21
+ this.#listeners.add(listener);
22
+ if (!this.#isSubscribedInternal)
23
+ this.#startSubscription();
24
+ return () => {
25
+ this.#listeners.delete(listener);
26
+ if (this.#listeners.size === 0)
27
+ this.#stopSubscription();
28
+ };
29
+ };
30
+ getStateSnapshot = () => {
31
+ if (this.#error)
32
+ throw this.#error;
33
+ return this.#state;
34
+ };
35
+ getDataProgressSnapshot = () => {
36
+ if (this.#state === null) {
37
+ return null;
38
+ }
39
+ let currentSyncCount = 0;
40
+ let totalMaxSyncCount = 0;
41
+ let otherEnabledDevicesExist = false;
42
+ for (const [deviceId, deviceSyncState] of Object.entries(this.#state.remoteDeviceSyncState)) {
43
+ if (deviceSyncState.data.isSyncEnabled) {
44
+ otherEnabledDevicesExist = true;
45
+ }
46
+ else {
47
+ continue;
48
+ }
49
+ const existingMaxCount = this.#perDeviceMaxSyncCount.get(deviceId);
50
+ if (typeof existingMaxCount === 'number' && existingMaxCount > 0) {
51
+ currentSyncCount = getDataSyncCountForDevice(deviceSyncState);
52
+ totalMaxSyncCount += existingMaxCount;
53
+ }
54
+ }
55
+ if (!otherEnabledDevicesExist) {
56
+ return null;
57
+ }
58
+ if (totalMaxSyncCount === 0) {
59
+ return 1;
60
+ }
61
+ const ratio = (totalMaxSyncCount - currentSyncCount) / totalMaxSyncCount;
62
+ if (ratio <= 0)
63
+ return 0;
64
+ if (ratio >= 1)
65
+ return 1;
66
+ return clamp(ratio, 0.01, 0.99);
67
+ };
68
+ #notifyListeners() {
69
+ for (const listener of this.#listeners) {
70
+ listener();
71
+ }
72
+ }
73
+ #onSyncState = (state) => {
74
+ const dataSyncWasEnabled = this.#state
75
+ ? this.#state.data.isSyncEnabled
76
+ : false;
77
+ // Reset map keeping track of counts used for progress if data sync is toggled
78
+ if (dataSyncWasEnabled !== state.data.isSyncEnabled) {
79
+ this.#perDeviceMaxSyncCount.clear();
80
+ }
81
+ else {
82
+ // Remove devices from #perDeviceMaxSyncCount that are no longer found in the new sync state
83
+ for (const deviceId of this.#perDeviceMaxSyncCount.keys()) {
84
+ if (!Object.hasOwn(state.remoteDeviceSyncState, deviceId)) {
85
+ this.#perDeviceMaxSyncCount.delete(deviceId);
86
+ }
87
+ }
88
+ }
89
+ for (const [deviceId, stateForDevice] of Object.entries(state.remoteDeviceSyncState)) {
90
+ const existingCount = this.#perDeviceMaxSyncCount.get(deviceId);
91
+ const newCount = getDataSyncCountForDevice(stateForDevice);
92
+ if (existingCount === undefined || existingCount < newCount) {
93
+ this.#perDeviceMaxSyncCount.set(deviceId, newCount);
94
+ }
95
+ }
96
+ this.#state = state;
97
+ this.#error = null;
98
+ this.#notifyListeners();
99
+ };
100
+ #startSubscription = () => {
101
+ this.#project.$sync.on('sync-state', this.#onSyncState);
102
+ this.#isSubscribedInternal = true;
103
+ this.#project.$sync
104
+ .getState()
105
+ .then(this.#onSyncState)
106
+ .catch((e) => {
107
+ this.#error = e;
108
+ this.#notifyListeners();
109
+ });
110
+ };
111
+ #stopSubscription = () => {
112
+ this.#isSubscribedInternal = false;
113
+ this.#project.$sync.off('sync-state', this.#onSyncState);
114
+ };
115
+ }
116
+ exports.SyncStore = SyncStore;
117
+ function clamp(value, min, max) {
118
+ return Math.max(min, Math.min(value, max));
119
+ }
@@ -0,0 +1,9 @@
1
+ import type { MapeoDoc, MapeoValue } from '@comapeo/schema' with { 'resolution-mode': 'import' };
2
+ export type WriteableDocumentType = Extract<MapeoDoc['schemaName'], 'field' | 'observation' | 'preset' | 'track' | 'remoteDetectionAlert'>;
3
+ export type WriteableValue<D extends WriteableDocumentType> = Extract<MapeoValue, {
4
+ schemaName: D;
5
+ }>;
6
+ export type WriteableDocument<D extends WriteableDocumentType> = Extract<MapeoDoc, {
7
+ schemaName: D;
8
+ }>;
9
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,14 +1,14 @@
1
1
  import type { MapeoClientApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' };
2
- import { type ReactNode } from 'react';
2
+ import { type JSX, type ReactNode } from 'react';
3
3
  export declare const ClientApiContext: import("react").Context<import("@comapeo/ipc/dist/client.js").MapeoClientApi | null>;
4
4
  /**
5
5
  * Create a context provider that holds a CoMapeo API client instance.
6
6
  *
7
- * @param opts
8
- * @param {ReactNode} opts.children React children node
9
- * @param {MapeoClientApi} opts.clientApi Client API instance
7
+ * @param opts.children React children node
8
+ * @param opts.clientApi Client API instance
9
+ *
10
10
  */
11
11
  export declare function ClientApiProvider({ children, clientApi, }: {
12
12
  children: ReactNode;
13
13
  clientApi: MapeoClientApi;
14
- }): import("react").FunctionComponentElement<import("react").ProviderProps<import("@comapeo/ipc/dist/client.js").MapeoClientApi | null>>;
14
+ }): JSX.Element;
@@ -3,9 +3,9 @@ export const ClientApiContext = createContext(null);
3
3
  /**
4
4
  * Create a context provider that holds a CoMapeo API client instance.
5
5
  *
6
- * @param opts
7
- * @param {ReactNode} opts.children React children node
8
- * @param {MapeoClientApi} opts.clientApi Client API instance
6
+ * @param opts.children React children node
7
+ * @param opts.clientApi Client API instance
8
+ *
9
9
  */
10
10
  export function ClientApiProvider({ children, clientApi, }) {
11
11
  return createElement(ClientApiContext.Provider, { value: clientApi }, children);
@@ -1,5 +1,5 @@
1
1
  import type { MapeoDoc } from '@comapeo/schema' with { 'resolution-mode': 'import' };
2
- import { type WriteableDocumentType } from '../lib/react-query/documents.js';
2
+ import type { WriteableDocumentType } from '../lib/types.js';
3
3
  type ReadHookResult<D> = {
4
4
  data: D;
5
5
  error: Error | null;
@@ -106,9 +106,9 @@ export declare function useManyDocs<D extends WriteableDocumentType>({ projectId
106
106
  docType: D;
107
107
  includeDeleted?: boolean;
108
108
  lang?: string;
109
- }): ReadHookResult<Extract<MapeoDoc, {
109
+ }): ReadHookResult<Array<Extract<MapeoDoc, {
110
110
  schemaName: D;
111
- }>>;
111
+ }>>>;
112
112
  /**
113
113
  * Create a document for a project.
114
114
  *
@@ -119,10 +119,10 @@ export declare function useCreateDocument<D extends WriteableDocumentType>({ doc
119
119
  docType: D;
120
120
  projectId: string;
121
121
  }): {
122
- mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/react-query/documents.js").WriteableDocument<D> & {
122
+ mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/types.js").WriteableDocument<D> & {
123
123
  forks: Array<string>;
124
124
  }, Error, {
125
- value: Omit<import("../lib/react-query/documents.js").WriteableValue<D>, "schemaName">;
125
+ value: Omit<import("../lib/types.js").WriteableValue<D>, "schemaName">;
126
126
  }, unknown>;
127
127
  reset: () => void;
128
128
  status: "pending" | "error" | "success" | "idle";
@@ -137,11 +137,11 @@ export declare function useUpdateDocument<D extends WriteableDocumentType>({ doc
137
137
  docType: D;
138
138
  projectId: string;
139
139
  }): {
140
- mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/react-query/documents.js").WriteableDocument<D> & {
140
+ mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/types.js").WriteableDocument<D> & {
141
141
  forks: Array<string>;
142
142
  }, Error, {
143
143
  versionId: string;
144
- value: Omit<import("../lib/react-query/documents.js").WriteableValue<D>, "schemaName">;
144
+ value: Omit<import("../lib/types.js").WriteableValue<D>, "schemaName">;
145
145
  }, unknown>;
146
146
  reset: () => void;
147
147
  status: "pending" | "error" | "success" | "idle";
@@ -156,7 +156,7 @@ export declare function useDeleteDocument<D extends WriteableDocumentType>({ doc
156
156
  docType: D;
157
157
  projectId: string;
158
158
  }): {
159
- mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/react-query/documents.js").WriteableDocument<D> & {
159
+ mutate: import("@tanstack/react-query").UseMutateFunction<import("../lib/types.js").WriteableDocument<D> & {
160
160
  forks: Array<string>;
161
161
  }, Error, {
162
162
  docId: string;
@@ -1,5 +1,6 @@
1
1
  import type { BitmapOpts, SvgOpts } from '@comapeo/core/dist/icon-api.js' with { 'resolution-mode': 'import' };
2
2
  import type { BlobId } from '@comapeo/core/dist/types.js' with { 'resolution-mode': 'import' };
3
+ import { type SyncState } from '../lib/sync.js';
3
4
  /**
4
5
  * Retrieve the project settings for a project.
5
6
  *
@@ -332,3 +333,52 @@ export declare function useCreateBlob({ projectId }: {
332
333
  reset: () => void;
333
334
  status: "pending" | "error" | "success" | "idle";
334
335
  };
336
+ /**
337
+ * Hook to subscribe to the current sync state.
338
+ *
339
+ * Creates a global singleton for each project, to minimize traffic over IPC -
340
+ * this hook can safely be used in more than one place without attaching
341
+ * additional listeners across the IPC channel.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * function Example() {
346
+ * const syncState = useSyncState({ projectId });
347
+ *
348
+ * if (!syncState) {
349
+ * // Sync information hasn't been loaded yet
350
+ * }
351
+ *
352
+ * // Actual info about sync state is available...
353
+ * }
354
+ * ```
355
+ *
356
+ * @param opts.projectId Project public ID
357
+ */
358
+ export declare function useSyncState({ projectId, }: {
359
+ projectId: string;
360
+ }): SyncState | null;
361
+ /**
362
+ * Provides the progress of data sync for sync-enabled connected peers
363
+ *
364
+ * @returns `null` if no sync state events have been received. Otherwise returns a value between 0 and 1 (inclusive)
365
+ */
366
+ export declare function useDataSyncProgress({ projectId, }: {
367
+ projectId: string;
368
+ }): number | null;
369
+ export declare function useStartSync({ projectId }: {
370
+ projectId: string;
371
+ }): {
372
+ mutate: import("@tanstack/react-query").UseMutateFunction<void, Error, {
373
+ autostopDataSyncAfter: number | null;
374
+ } | undefined, unknown>;
375
+ reset: () => void;
376
+ status: "pending" | "error" | "success" | "idle";
377
+ };
378
+ export declare function useStopSync({ projectId }: {
379
+ projectId: string;
380
+ }): {
381
+ mutate: import("@tanstack/react-query").UseMutateFunction<void, Error, void, unknown>;
382
+ reset: () => void;
383
+ status: "pending" | "error" | "success" | "idle";
384
+ };
@@ -1,5 +1,7 @@
1
1
  import { useMutation, useQueryClient, useSuspenseQuery, } from '@tanstack/react-query';
2
- import { addServerPeerMutationOptions, attachmentUrlQueryOptions, createBlobMutationOptions, createProjectMutationOptions, documentCreatedByQueryOptions, iconUrlQueryOptions, importProjectConfigMutationOptions, leaveProjectMutationOptions, projectByIdQueryOptions, projectMemberByIdQueryOptions, projectMembersQueryOptions, projectSettingsQueryOptions, projectsQueryOptions, updateProjectSettingsMutationOptions, } from '../lib/react-query/projects.js';
2
+ import { useSyncExternalStore } from 'react';
3
+ import { addServerPeerMutationOptions, attachmentUrlQueryOptions, createBlobMutationOptions, createProjectMutationOptions, documentCreatedByQueryOptions, iconUrlQueryOptions, importProjectConfigMutationOptions, leaveProjectMutationOptions, projectByIdQueryOptions, projectMemberByIdQueryOptions, projectMembersQueryOptions, projectSettingsQueryOptions, projectsQueryOptions, startSyncMutationOptions, stopSyncMutationOptions, updateProjectSettingsMutationOptions, } from '../lib/react-query/projects.js';
4
+ import { SyncStore } from '../lib/sync.js';
3
5
  import { useClientApi } from './client.js';
4
6
  /**
5
7
  * Retrieve the project settings for a project.
@@ -302,3 +304,59 @@ export function useCreateBlob({ projectId }) {
302
304
  const { mutate, reset, status } = useMutation(createBlobMutationOptions({ projectApi }));
303
305
  return { mutate, reset, status };
304
306
  }
307
+ const PROJECT_SYNC_STORE_MAP = new WeakMap();
308
+ function useSyncStore({ projectId }) {
309
+ const { data: projectApi } = useSingleProject({ projectId });
310
+ let syncStore = PROJECT_SYNC_STORE_MAP.get(projectApi);
311
+ if (!syncStore) {
312
+ syncStore = new SyncStore(projectApi);
313
+ PROJECT_SYNC_STORE_MAP.set(projectApi, syncStore);
314
+ }
315
+ return syncStore;
316
+ }
317
+ /**
318
+ * Hook to subscribe to the current sync state.
319
+ *
320
+ * Creates a global singleton for each project, to minimize traffic over IPC -
321
+ * this hook can safely be used in more than one place without attaching
322
+ * additional listeners across the IPC channel.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * function Example() {
327
+ * const syncState = useSyncState({ projectId });
328
+ *
329
+ * if (!syncState) {
330
+ * // Sync information hasn't been loaded yet
331
+ * }
332
+ *
333
+ * // Actual info about sync state is available...
334
+ * }
335
+ * ```
336
+ *
337
+ * @param opts.projectId Project public ID
338
+ */
339
+ export function useSyncState({ projectId, }) {
340
+ const syncStore = useSyncStore({ projectId });
341
+ const { subscribe, getStateSnapshot } = syncStore;
342
+ return useSyncExternalStore(subscribe, getStateSnapshot);
343
+ }
344
+ /**
345
+ * Provides the progress of data sync for sync-enabled connected peers
346
+ *
347
+ * @returns `null` if no sync state events have been received. Otherwise returns a value between 0 and 1 (inclusive)
348
+ */
349
+ export function useDataSyncProgress({ projectId, }) {
350
+ const { subscribe, getDataProgressSnapshot } = useSyncStore({ projectId });
351
+ return useSyncExternalStore(subscribe, getDataProgressSnapshot);
352
+ }
353
+ export function useStartSync({ projectId }) {
354
+ const { data: projectApi } = useSingleProject({ projectId });
355
+ const { mutate, reset, status } = useMutation(startSyncMutationOptions({ projectApi }));
356
+ return { mutate, reset, status };
357
+ }
358
+ export function useStopSync({ projectId }) {
359
+ const { data: projectApi } = useSingleProject({ projectId });
360
+ const { mutate, reset, status } = useMutation(stopSyncMutationOptions({ projectApi }));
361
+ return { mutate, reset, status };
362
+ }
@@ -3,10 +3,6 @@ export { useClientApi, useIsArchiveDevice, useOwnDeviceInfo, useSetIsArchiveDevi
3
3
  export { useCreateDocument, useDeleteDocument, useManyDocs, useSingleDocByDocId, useSingleDocByVersionId, useUpdateDocument, } from './hooks/documents.js';
4
4
  export { useAcceptInvite, useRejectInvite, useRequestCancelInvite, useSendInvite, } from './hooks/invites.js';
5
5
  export { useMapStyleUrl } from './hooks/maps.js';
6
- export { useAddServerPeer, useAttachmentUrl, useCreateBlob, useCreateProject, useDocumentCreatedBy, useIconUrl, useImportProjectConfig, useLeaveProject, useManyMembers, useManyProjects, useProjectSettings, useSingleMember, useSingleProject, useUpdateProjectSettings, } from './hooks/projects.js';
7
- export { getClientQueryKey, getDeviceInfoQueryKey, getIsArchiveDeviceQueryKey, } from './lib/react-query/client.js';
8
- export { getDocumentByDocIdQueryKey, getDocumentByVersionIdQueryKey, getDocumentsQueryKey, getManyDocumentsQueryKey, type WriteableDocument, type WriteableDocumentType, type WriteableValue, } from './lib/react-query/documents.js';
9
- export { getInvitesQueryKey, getPendingInvitesQueryKey, } from './lib/react-query/invites.js';
10
- export { getMapsQueryKey, getStyleJsonUrlQueryKey, } from './lib/react-query/maps.js';
11
- export { getAttachmentUrlQueryKey, getDocumentCreatedByQueryKey, getIconUrlQueryKey, getMemberByIdQueryKey, getMembersQueryKey, getProjectByIdQueryKey, getProjectRoleQueryKey, getProjectSettingsQueryKey, getProjectsQueryKey, } from './lib/react-query/projects.js';
12
- export { ROOT_QUERY_KEY } from './lib/react-query/shared.js';
6
+ export { useAddServerPeer, useAttachmentUrl, useCreateBlob, useCreateProject, useDataSyncProgress, useDocumentCreatedBy, useIconUrl, useImportProjectConfig, useLeaveProject, useManyMembers, useManyProjects, useProjectSettings, useSingleMember, useSingleProject, useStartSync, useStopSync, useSyncState, useUpdateProjectSettings, } from './hooks/projects.js';
7
+ export { type SyncState } from './lib/sync.js';
8
+ export { type WriteableDocument, type WriteableDocumentType, type WriteableValue, } from './lib/types.js';
package/dist/esm/index.js CHANGED
@@ -3,10 +3,4 @@ export { useClientApi, useIsArchiveDevice, useOwnDeviceInfo, useSetIsArchiveDevi
3
3
  export { useCreateDocument, useDeleteDocument, useManyDocs, useSingleDocByDocId, useSingleDocByVersionId, useUpdateDocument, } from './hooks/documents.js';
4
4
  export { useAcceptInvite, useRejectInvite, useRequestCancelInvite, useSendInvite, } from './hooks/invites.js';
5
5
  export { useMapStyleUrl } from './hooks/maps.js';
6
- export { useAddServerPeer, useAttachmentUrl, useCreateBlob, useCreateProject, useDocumentCreatedBy, useIconUrl, useImportProjectConfig, useLeaveProject, useManyMembers, useManyProjects, useProjectSettings, useSingleMember, useSingleProject, useUpdateProjectSettings, } from './hooks/projects.js';
7
- export { getClientQueryKey, getDeviceInfoQueryKey, getIsArchiveDeviceQueryKey, } from './lib/react-query/client.js';
8
- export { getDocumentByDocIdQueryKey, getDocumentByVersionIdQueryKey, getDocumentsQueryKey, getManyDocumentsQueryKey, } from './lib/react-query/documents.js';
9
- export { getInvitesQueryKey, getPendingInvitesQueryKey, } from './lib/react-query/invites.js';
10
- export { getMapsQueryKey, getStyleJsonUrlQueryKey, } from './lib/react-query/maps.js';
11
- export { getAttachmentUrlQueryKey, getDocumentCreatedByQueryKey, getIconUrlQueryKey, getMemberByIdQueryKey, getMembersQueryKey, getProjectByIdQueryKey, getProjectRoleQueryKey, getProjectSettingsQueryKey, getProjectsQueryKey, } from './lib/react-query/projects.js';
12
- export { ROOT_QUERY_KEY } from './lib/react-query/shared.js';
6
+ export { useAddServerPeer, useAttachmentUrl, useCreateBlob, useCreateProject, useDataSyncProgress, useDocumentCreatedBy, useIconUrl, useImportProjectConfig, useLeaveProject, useManyMembers, useManyProjects, useProjectSettings, useSingleMember, useSingleProject, useStartSync, useStopSync, useSyncState, useUpdateProjectSettings, } from './hooks/projects.js';
@@ -1,13 +1,6 @@
1
1
  import type { MapeoProjectApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' };
2
- import type { MapeoDoc, MapeoValue } from '@comapeo/schema' with { 'resolution-mode': 'import' };
3
2
  import { type QueryClient } from '@tanstack/react-query';
4
- export type WriteableDocumentType = Extract<MapeoDoc['schemaName'], 'field' | 'observation' | 'preset' | 'track' | 'remoteDetectionAlert'>;
5
- export type WriteableValue<D extends WriteableDocumentType> = Extract<MapeoValue, {
6
- schemaName: D;
7
- }>;
8
- export type WriteableDocument<D extends WriteableDocumentType> = Extract<MapeoDoc, {
9
- schemaName: D;
10
- }>;
3
+ import { WriteableDocument, WriteableDocumentType, WriteableValue } from '../types.js';
11
4
  export declare function getDocumentsQueryKey<D extends WriteableDocumentType>({ projectId, docType, }: {
12
5
  projectId: string;
13
6
  docType: D;
@@ -330,3 +330,19 @@ export declare function createBlobMutationOptions({ projectApi, }: {
330
330
  networkMode: "always";
331
331
  retry: false;
332
332
  };
333
+ export declare function startSyncMutationOptions({ projectApi, }: {
334
+ projectApi: MapeoProjectApi;
335
+ }): {
336
+ mutationFn: (opts: {
337
+ autostopDataSyncAfter: number | null;
338
+ } | undefined) => Promise<void>;
339
+ networkMode: "always";
340
+ retry: false;
341
+ };
342
+ export declare function stopSyncMutationOptions({ projectApi, }: {
343
+ projectApi: MapeoProjectApi;
344
+ }): {
345
+ mutationFn: () => Promise<void>;
346
+ networkMode: "always";
347
+ retry: false;
348
+ };
@@ -207,3 +207,21 @@ export function createBlobMutationOptions({ projectApi, }) {
207
207
  },
208
208
  };
209
209
  }
210
+ export function startSyncMutationOptions({ projectApi, }) {
211
+ return {
212
+ ...baseMutationOptions(),
213
+ mutationFn: async (opts) => {
214
+ // Have to avoid passing `undefined` explicitly
215
+ // See https://github.com/digidem/rpc-reflector/issues/21
216
+ return opts ? projectApi.$sync.start(opts) : projectApi.$sync.start();
217
+ },
218
+ };
219
+ }
220
+ export function stopSyncMutationOptions({ projectApi, }) {
221
+ return {
222
+ ...baseMutationOptions(),
223
+ mutationFn: async () => {
224
+ return projectApi.$sync.stop();
225
+ },
226
+ };
227
+ }
@@ -0,0 +1,10 @@
1
+ import type { MapeoProjectApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' };
2
+ export type SyncState = Awaited<ReturnType<MapeoProjectApi['$sync']['getState']>>;
3
+ export declare function getDataSyncCountForDevice(syncStateForDevice: SyncState['remoteDeviceSyncState'][string]): number;
4
+ export declare class SyncStore {
5
+ #private;
6
+ constructor(project: MapeoProjectApi);
7
+ subscribe: (listener: () => void) => () => void;
8
+ getStateSnapshot: () => import("@comapeo/core/dist/sync/sync-api.js").State | null;
9
+ getDataProgressSnapshot: () => number | null;
10
+ }
@@ -0,0 +1,114 @@
1
+ export function getDataSyncCountForDevice(syncStateForDevice) {
2
+ const { data } = syncStateForDevice;
3
+ return data.want + data.wanted;
4
+ }
5
+ export class SyncStore {
6
+ #project;
7
+ #listeners = new Set();
8
+ #isSubscribedInternal = false;
9
+ #error = null;
10
+ #state = null;
11
+ // Used for calculating sync progress
12
+ #perDeviceMaxSyncCount = new Map();
13
+ constructor(project) {
14
+ this.#project = project;
15
+ }
16
+ subscribe = (listener) => {
17
+ this.#listeners.add(listener);
18
+ if (!this.#isSubscribedInternal)
19
+ this.#startSubscription();
20
+ return () => {
21
+ this.#listeners.delete(listener);
22
+ if (this.#listeners.size === 0)
23
+ this.#stopSubscription();
24
+ };
25
+ };
26
+ getStateSnapshot = () => {
27
+ if (this.#error)
28
+ throw this.#error;
29
+ return this.#state;
30
+ };
31
+ getDataProgressSnapshot = () => {
32
+ if (this.#state === null) {
33
+ return null;
34
+ }
35
+ let currentSyncCount = 0;
36
+ let totalMaxSyncCount = 0;
37
+ let otherEnabledDevicesExist = false;
38
+ for (const [deviceId, deviceSyncState] of Object.entries(this.#state.remoteDeviceSyncState)) {
39
+ if (deviceSyncState.data.isSyncEnabled) {
40
+ otherEnabledDevicesExist = true;
41
+ }
42
+ else {
43
+ continue;
44
+ }
45
+ const existingMaxCount = this.#perDeviceMaxSyncCount.get(deviceId);
46
+ if (typeof existingMaxCount === 'number' && existingMaxCount > 0) {
47
+ currentSyncCount = getDataSyncCountForDevice(deviceSyncState);
48
+ totalMaxSyncCount += existingMaxCount;
49
+ }
50
+ }
51
+ if (!otherEnabledDevicesExist) {
52
+ return null;
53
+ }
54
+ if (totalMaxSyncCount === 0) {
55
+ return 1;
56
+ }
57
+ const ratio = (totalMaxSyncCount - currentSyncCount) / totalMaxSyncCount;
58
+ if (ratio <= 0)
59
+ return 0;
60
+ if (ratio >= 1)
61
+ return 1;
62
+ return clamp(ratio, 0.01, 0.99);
63
+ };
64
+ #notifyListeners() {
65
+ for (const listener of this.#listeners) {
66
+ listener();
67
+ }
68
+ }
69
+ #onSyncState = (state) => {
70
+ const dataSyncWasEnabled = this.#state
71
+ ? this.#state.data.isSyncEnabled
72
+ : false;
73
+ // Reset map keeping track of counts used for progress if data sync is toggled
74
+ if (dataSyncWasEnabled !== state.data.isSyncEnabled) {
75
+ this.#perDeviceMaxSyncCount.clear();
76
+ }
77
+ else {
78
+ // Remove devices from #perDeviceMaxSyncCount that are no longer found in the new sync state
79
+ for (const deviceId of this.#perDeviceMaxSyncCount.keys()) {
80
+ if (!Object.hasOwn(state.remoteDeviceSyncState, deviceId)) {
81
+ this.#perDeviceMaxSyncCount.delete(deviceId);
82
+ }
83
+ }
84
+ }
85
+ for (const [deviceId, stateForDevice] of Object.entries(state.remoteDeviceSyncState)) {
86
+ const existingCount = this.#perDeviceMaxSyncCount.get(deviceId);
87
+ const newCount = getDataSyncCountForDevice(stateForDevice);
88
+ if (existingCount === undefined || existingCount < newCount) {
89
+ this.#perDeviceMaxSyncCount.set(deviceId, newCount);
90
+ }
91
+ }
92
+ this.#state = state;
93
+ this.#error = null;
94
+ this.#notifyListeners();
95
+ };
96
+ #startSubscription = () => {
97
+ this.#project.$sync.on('sync-state', this.#onSyncState);
98
+ this.#isSubscribedInternal = true;
99
+ this.#project.$sync
100
+ .getState()
101
+ .then(this.#onSyncState)
102
+ .catch((e) => {
103
+ this.#error = e;
104
+ this.#notifyListeners();
105
+ });
106
+ };
107
+ #stopSubscription = () => {
108
+ this.#isSubscribedInternal = false;
109
+ this.#project.$sync.off('sync-state', this.#onSyncState);
110
+ };
111
+ }
112
+ function clamp(value, min, max) {
113
+ return Math.max(min, Math.min(value, max));
114
+ }
@@ -0,0 +1,9 @@
1
+ import type { MapeoDoc, MapeoValue } from '@comapeo/schema' with { 'resolution-mode': 'import' };
2
+ export type WriteableDocumentType = Extract<MapeoDoc['schemaName'], 'field' | 'observation' | 'preset' | 'track' | 'remoteDetectionAlert'>;
3
+ export type WriteableValue<D extends WriteableDocumentType> = Extract<MapeoValue, {
4
+ schemaName: D;
5
+ }>;
6
+ export type WriteableDocument<D extends WriteableDocumentType> = Extract<MapeoDoc, {
7
+ schemaName: D;
8
+ }>;
9
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/docs/API.md CHANGED
@@ -20,6 +20,10 @@
20
20
  - [useImportProjectConfig](#useimportprojectconfig)
21
21
  - [useUpdateProjectSettings](#useupdateprojectsettings)
22
22
  - [useCreateBlob](#usecreateblob)
23
+ - [useSyncState](#usesyncstate)
24
+ - [useDataSyncProgress](#usedatasyncprogress)
25
+ - [useStartSync](#usestartsync)
26
+ - [useStopSync](#usestopsync)
23
27
  - [useSingleDocByDocId](#usesingledocbydocid)
24
28
  - [useSingleDocByVersionId](#usesingledocbyversionid)
25
29
  - [useManyDocs](#usemanydocs)
@@ -38,7 +42,7 @@ Create a context provider that holds a CoMapeo API client instance.
38
42
 
39
43
  | Function | Type |
40
44
  | ---------- | ---------- |
41
- | `ClientApiProvider` | `({ children, clientApi, }: { children: ReactNode; clientApi: MapeoClientApi; }) => FunctionComponentElement<ProviderProps<MapeoClientApi or null>>` |
45
+ | `ClientApiProvider` | `({ children, clientApi, }: { children: ReactNode; clientApi: MapeoClientApi; }) => Element` |
42
46
 
43
47
  Parameters:
44
48
 
@@ -427,13 +431,65 @@ Create a blob for a project.
427
431
 
428
432
  | Function | Type |
429
433
  | ---------- | ---------- |
430
- | `useCreateBlob` | `({ projectId }: { projectId: string; }) => { mutate: UseMutateFunction<{ driveId: string; name: string; type: "audio" or "video" or "photo"; hash: string; }, Error, { original: string; preview?: string or undefined; thumbnail?: string or undefined; metadata: Metadata; }, unknown>; reset: () => void; status: "pending" or ...` |
434
+ | `useCreateBlob` | `({ projectId }: { projectId: string; }) => { mutate: UseMutateFunction<{ driveId: string; name: string; type: "photo" or "audio" or "video"; hash: string; }, Error, { original: string; preview?: string or undefined; thumbnail?: string or undefined; metadata: Metadata; }, unknown>; reset: () => void; status: "pending" or ...` |
431
435
 
432
436
  Parameters:
433
437
 
434
438
  * `opts.projectId`: Public project ID of project to apply to changes to.
435
439
 
436
440
 
441
+ ### useSyncState
442
+
443
+ Hook to subscribe to the current sync state.
444
+
445
+ Creates a global singleton for each project, to minimize traffic over IPC -
446
+ this hook can safely be used in more than one place without attaching
447
+ additional listeners across the IPC channel.
448
+
449
+ | Function | Type |
450
+ | ---------- | ---------- |
451
+ | `useSyncState` | `({ projectId, }: { projectId: string; }) => State or null` |
452
+
453
+ Parameters:
454
+
455
+ * `opts.projectId`: Project public ID
456
+
457
+
458
+ Examples:
459
+
460
+ ```ts
461
+ function Example() {
462
+ const syncState = useSyncState({ projectId });
463
+
464
+ if (!syncState) {
465
+ // Sync information hasn't been loaded yet
466
+ }
467
+
468
+ // Actual info about sync state is available...
469
+ }
470
+ ```
471
+
472
+
473
+ ### useDataSyncProgress
474
+
475
+ Provides the progress of data sync for sync-enabled connected peers
476
+
477
+ | Function | Type |
478
+ | ---------- | ---------- |
479
+ | `useDataSyncProgress` | `({ projectId, }: { projectId: string; }) => number or null` |
480
+
481
+ ### useStartSync
482
+
483
+ | Function | Type |
484
+ | ---------- | ---------- |
485
+ | `useStartSync` | `({ projectId }: { projectId: string; }) => { mutate: UseMutateFunction<void, Error, { autostopDataSyncAfter: number or null; } or undefined, unknown>; reset: () => void; status: "pending" or ... 2 more ... or "idle"; }` |
486
+
487
+ ### useStopSync
488
+
489
+ | Function | Type |
490
+ | ---------- | ---------- |
491
+ | `useStopSync` | `({ projectId }: { projectId: string; }) => { mutate: UseMutateFunction<void, Error, void, unknown>; reset: () => void; status: "pending" or "error" or "success" or "idle"; }` |
492
+
437
493
  ### useSingleDocByDocId
438
494
 
439
495
  Retrieve a single document from the database based on the document's document ID.
@@ -508,7 +564,7 @@ Retrieve all documents of a specific `docType`.
508
564
 
509
565
  | Function | Type |
510
566
  | ---------- | ---------- |
511
- | `useManyDocs` | `<D extends WriteableDocumentType>({ projectId, docType, includeDeleted, lang, }: { projectId: string; docType: D; includeDeleted?: boolean or undefined; lang?: string or undefined; }) => ReadHookResult<Extract<{ schemaName: "deviceInfo"; name: string; deviceType: "device_type_unspecified" or ... 4 more ... or "UNRECOGNI...` |
567
+ | `useManyDocs` | `<D extends WriteableDocumentType>({ projectId, docType, includeDeleted, lang, }: { projectId: string; docType: D; includeDeleted?: boolean or undefined; lang?: string or undefined; }) => ReadHookResult<(Extract<{ schemaName: "deviceInfo"; name: string; deviceType: "device_type_unspecified" or ... 4 more ... or "UNRECOGN...` |
512
568
 
513
569
  Parameters:
514
570
 
@@ -678,3 +734,28 @@ function ExampleWithRefreshToken() {
678
734
  | `ClientApiContext` | `Context<MapeoClientApi or null>` |
679
735
 
680
736
 
737
+
738
+ ## Types
739
+
740
+ - [WriteableDocumentType](#writeabledocumenttype)
741
+ - [WriteableValue](#writeablevalue)
742
+ - [WriteableDocument](#writeabledocument)
743
+
744
+ ### WriteableDocumentType
745
+
746
+ | Type | Type |
747
+ | ---------- | ---------- |
748
+ | `WriteableDocumentType` | `Extract< MapeoDoc['schemaName'], 'field' or 'observation' or 'preset' or 'track' or 'remoteDetectionAlert' >` |
749
+
750
+ ### WriteableValue
751
+
752
+ | Type | Type |
753
+ | ---------- | ---------- |
754
+ | `WriteableValue` | `Extract< MapeoValue, { schemaName: D } >` |
755
+
756
+ ### WriteableDocument
757
+
758
+ | Type | Type |
759
+ | ---------- | ---------- |
760
+ | `WriteableDocument` | `Extract< MapeoDoc, { schemaName: D } >` |
761
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comapeo/core-react",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "React wrapper for working with @comapeo/core",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,7 +35,6 @@
35
35
  "types": "./dist/commonjs/index.d.ts",
36
36
  "module": "./dist/esm/index.js",
37
37
  "files": [
38
- "CHANGELOG.md",
39
38
  "dist/",
40
39
  "docs/API.md"
41
40
  ],
@@ -48,16 +47,14 @@
48
47
  }
49
48
  },
50
49
  "scripts": {
51
- "prepare:husky": "husky",
52
- "prepare:tshy": "tshy",
53
- "prepare": "npm-run-all --parallel prepare:*",
50
+ "docs:generate": "tsdoc --src=src/contexts/*,src/hooks/*,src/lib/types.ts --dest=docs/API.md --noemoji --types",
54
51
  "lint:eslint": "eslint --cache .",
55
52
  "lint:format": "prettier --cache --check .",
56
53
  "lint": "npm-run-all --parallel --continue-on-error --print-label --aggregate-output lint:*",
57
- "types": "tsc",
58
- "test:unit": "vitest run",
59
- "test": "npm-run-all --parallel --continue-on-error --print-label --aggregate-output types test:*",
60
- "docs:generate": "tsdoc --src=src/contexts/*,src/hooks/* --dest=docs/API.md --noemoji --types"
54
+ "prepack": "tshy",
55
+ "prepare": "husky",
56
+ "test": "vitest run",
57
+ "types": "tsc"
61
58
  },
62
59
  "peerDependencies": {
63
60
  "@comapeo/core": "*",
@@ -67,30 +64,33 @@
67
64
  "react": "^18 || ^19"
68
65
  },
69
66
  "devDependencies": {
70
- "@eslint/compat": "1.2.5",
71
- "@eslint/js": "9.18.0",
67
+ "@comapeo/core": "2.3.1",
68
+ "@comapeo/ipc": "2.1.0",
69
+ "@comapeo/schema": "1.4.1",
70
+ "@eslint/compat": "1.2.7",
71
+ "@eslint/js": "9.23.0",
72
72
  "@ianvs/prettier-plugin-sort-imports": "4.4.1",
73
73
  "@mapeo/crypto": "1.0.0-alpha.10",
74
- "@tanstack/eslint-plugin-query": "5.62.16",
74
+ "@tanstack/eslint-plugin-query": "5.68.0",
75
+ "@tanstack/react-query": "5.69.0",
75
76
  "@testing-library/dom": "10.4.0",
76
- "@testing-library/react": "16.1.0",
77
- "@types/lint-staged": "13.3.0",
78
- "@types/node": "22.10.5",
79
- "@types/react": "19.0.6",
80
- "@types/react-dom": "19.0.3",
81
- "commit-and-tag-version": "12.5.0",
82
- "eslint": "9.18.0",
77
+ "@testing-library/react": "16.2.0",
78
+ "@types/node": "22.13.13",
79
+ "@types/react": "19.0.12",
80
+ "@types/react-dom": "19.0.4",
81
+ "eslint": "9.23.0",
83
82
  "fastify": "4.29.0",
84
- "globals": "15.14.0",
83
+ "globals": "16.0.0",
85
84
  "husky": "9.1.7",
86
- "lint-staged": "15.3.0",
85
+ "lint-staged": "15.5.0",
87
86
  "npm-run-all2": "7.0.2",
88
- "prettier": "3.4.2",
87
+ "prettier": "3.5.3",
89
88
  "random-access-memory": "6.2.1",
90
- "tsdoc-markdown": "1.1.0",
89
+ "react": "19.0.0",
90
+ "tsdoc-markdown": "1.2.0",
91
91
  "tshy": "3.0.2",
92
- "typescript": "5.7.3",
93
- "typescript-eslint": "8.19.1",
94
- "vitest": "2.1.8"
92
+ "typescript": "5.8.2",
93
+ "typescript-eslint": "8.27.0",
94
+ "vitest": "3.0.9"
95
95
  }
96
96
  }
package/CHANGELOG.md DELETED
@@ -1,23 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
-
5
- ## [0.1.2](https://github.com/digidem/comapeo-core-react/compare/v0.1.1...v0.1.2) (2024-12-12)
6
-
7
-
8
- ### Bug Fixes
9
-
10
- * fix return value of getIsArchiveDeviceQueryKey() ([8742fc2](https://github.com/digidem/comapeo-core-react/commit/8742fc24379564f64f9ec25c4c2e9179083d659d))
11
-
12
- ## [0.1.1](https://github.com/digidem/comapeo-core-react/compare/v0.1.0...v0.1.1) (2024-12-12)
13
-
14
-
15
- ### Bug Fixes
16
-
17
- * adjust peer dep specification for react query ([df090c3](https://github.com/digidem/comapeo-core-react/commit/df090c3b57032cf51d25b77afaa0e562163297c5))
18
-
19
- ## 0.1.0 (2024-12-12)
20
-
21
- ### Features
22
-
23
- - initial implementation of many tanstack-query queries ([8a6aed7](https://github.com/digidem/comapeo-core-react/commit/8a6aed78f646c0f536551ac4a0423ff5d7780b33))