@tearleads/client-sdk 0.1.87 → 0.1.89
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/Tearleads.js +1 -1
- package/Tearleads.js.map +1 -1
- package/client/containerContents/documentDiscovery.js +16 -2
- package/client/containerContents/documentDiscovery.js.map +1 -1
- package/client/containerContents/index.d.ts +1 -1
- package/client/containerContents/index.js +1 -0
- package/client/containerContents/index.js.map +1 -1
- package/client/index.d.ts +1 -1
- package/client/index.js.map +1 -1
- package/client/organizations/index.d.ts +4 -2
- package/client/organizations/index.js +15 -8
- package/client/organizations/index.js.map +1 -1
- package/client/organizations/organizationMemberships.d.ts +41 -0
- package/client/organizations/organizationMemberships.js +166 -0
- package/client/organizations/organizationMemberships.js.map +1 -0
- package/client/organizations/organizationReadModels.d.ts +7 -1
- package/client/organizations/organizationReadModels.js +15 -6
- package/client/organizations/organizationReadModels.js.map +1 -1
- package/client/organizations/organizationsTypes.d.ts +3 -0
- package/client/organizations/organizationsTypes.js.map +1 -1
- package/data/persistence/container-contents/containerStructuralRepair.js +19 -14
- package/data/persistence/container-contents/containerStructuralRepair.js.map +1 -1
- package/data/persistence/container-contents/exclusiveDocumentPlacements.js +60 -0
- package/data/persistence/container-contents/exclusiveDocumentPlacements.js.map +1 -0
- package/data/persistence/organizations/organizationReadModelPersistence.js +14 -0
- package/data/persistence/organizations/organizationReadModelPersistence.js.map +1 -1
- package/data/persistence/organizations/organizationReadModelProjection.js +12 -0
- package/data/persistence/organizations/organizationReadModelProjection.js.map +1 -1
- package/data/sqlite/schema/index.d.ts +90 -0
- package/data/sqlite/schema/index.js +3 -0
- package/data/sqlite/schema/index.js.map +1 -1
- package/data/sqlite/schema/organizationReadModelSchema.js +2 -0
- package/data/sqlite/schema/organizationReadModelSchema.js.map +1 -1
- package/data/sqlite/schema/organizationReadModelTableRegistry.d.ts +19 -0
- package/data/sqlite/schema/organizationSyncPauseSchema.js +32 -0
- package/data/sqlite/schema/organizationSyncPauseSchema.js.map +1 -0
- package/data/sync/organizationSyncPauses.d.ts +59 -0
- package/data/sync/organizationSyncPauses.js +149 -0
- package/data/sync/organizationSyncPauses.js.map +1 -0
- package/index.d.ts +2 -2
- package/index.js.map +1 -1
- package/internal/api-client/ApiClient.d.ts +21 -2
- package/internal/api-client/ApiClient.js +15 -1
- package/internal/api-client/ApiClient.js.map +1 -1
- package/internal/api-client/routes/organizations/memberships.js +36 -0
- package/internal/api-client/routes/organizations/memberships.js.map +1 -0
- package/internal/crypto/index.d.ts +1 -0
- package/internal/crypto/index.js +1 -0
- package/internal/crypto/index.js.map +1 -1
- package/internal/crypto/organizationLeaveRequest.d.ts +13 -0
- package/internal/crypto/organizationLeaveRequest.js +39 -0
- package/internal/crypto/organizationLeaveRequest.js.map +1 -0
- package/internal/validators/operation/index.d.ts +1 -0
- package/internal/validators/operation/index.js +1 -0
- package/internal/validators/operation/index.js.map +1 -1
- package/internal/validators/operation/organizationMemberships.d.ts +503 -0
- package/internal/validators/operation/organizationMemberships.js +93 -0
- package/internal/validators/operation/organizationMemberships.js.map +1 -0
- package/internal/validators/operation/organizations.d.ts +48 -0
- package/internal/validators/operation/registry.d.ts +5 -0
- package/internal/validators/operation/registry.js +5 -0
- package/internal/validators/operation/registry.js.map +1 -1
- package/internal/validators/realtime/index.js +1 -1
- package/internal/validators/realtime/index.js.map +1 -1
- package/internal/validators/realtime/wsInvalidationHints.js +8 -0
- package/internal/validators/realtime/wsInvalidationHints.js.map +1 -1
- package/internal/validators/request/index.d.ts +1 -1
- package/internal/validators/request/index.js +1 -1
- package/internal/validators/request/index.js.map +1 -1
- package/internal/validators/request/organization.d.ts +6 -0
- package/internal/validators/request/organization.js +7 -0
- package/internal/validators/request/organization.js.map +1 -1
- package/internal/validators/response/index.d.ts +1 -0
- package/internal/validators/response/index.js +1 -0
- package/internal/validators/response/index.js.map +1 -1
- package/internal/validators/response/organization.d.ts +82 -0
- package/internal/validators/response/organization.js +6 -0
- package/internal/validators/response/organization.js.map +1 -1
- package/internal/validators/response/organizationMembership.d.ts +66 -0
- package/internal/validators/response/organizationMembership.js +29 -0
- package/internal/validators/response/organizationMembership.js.map +1 -0
- package/internal/validators/response/organizationReadModel.d.ts +60 -0
- package/internal/validators/util/index.d.ts +1 -0
- package/internal/validators/util/index.js +1 -0
- package/internal/validators/util/index.js.map +1 -1
- package/internal/validators/util/organizationLeaveRequest.d.ts +15 -0
- package/internal/validators/util/organizationLeaveRequest.js +22 -0
- package/internal/validators/util/organizationLeaveRequest.js.map +1 -0
- package/package.json +1 -1
- package/stores/container-contents/containerContentsStore.d.ts +1 -1
- package/stores/container-contents/containerContentsStore.js +2 -0
- package/stores/container-contents/containerContentsStore.js.map +1 -1
- package/stores/container-contents/operations/forgetOrganizationContentOperation.js +76 -0
- package/stores/container-contents/operations/forgetOrganizationContentOperation.js.map +1 -0
- package/stores/container-contents/types.d.ts +6 -0
- package/stores/container-contents/types.js.map +1 -1
- package/stores/documents/documentStore/attachments/attachmentRemoval.js +3 -4
- package/stores/documents/documentStore/attachments/attachmentRemoval.js.map +1 -1
- package/stores/documents/documentStore/attachments/index.js +5 -7
- package/stores/documents/documentStore/attachments/index.js.map +1 -1
- package/stores/documents/documentStore/index.js +16 -12
- package/stores/documents/documentStore/index.js.map +1 -1
- package/stores/documents/documentStore/initialization/index.js +9 -0
- package/stores/documents/documentStore/initialization/index.js.map +1 -1
- package/stores/documents/documentStore/mutations.js +5 -7
- package/stores/documents/documentStore/mutations.js.map +1 -1
- package/stores/documents/documentStore/rotation/index.js +2 -1
- package/stores/documents/documentStore/rotation/index.js.map +1 -1
- package/stores/documents/documentStore/rows/index.js +3 -4
- package/stores/documents/documentStore/rows/index.js.map +1 -1
- package/stores/documents/documentStore/state.js +18 -0
- package/stores/documents/documentStore/state.js.map +1 -1
- package/stores/documents/documentStore/sync/index.js +5 -2
- package/stores/documents/documentStore/sync/index.js.map +1 -1
- package/stores/documents/registry.d.ts +32 -0
- package/stores/documents/registry.js +79 -4
- package/stores/documents/registry.js.map +1 -1
- package/stores/documents/removedDocumentStore.js +38 -0
- package/stores/documents/removedDocumentStore.js.map +1 -0
- package/stores/documents/types.d.ts +2 -0
- package/stores/documents/types.js.map +1 -1
- package/sync/reconciliation/service.js +2 -2
- package/sync/reconciliation/service.js.map +1 -1
- package/sync/reconciliation/serviceTypes.d.ts +8 -1
- package/sync/reconciliation/serviceTypes.js.map +1 -1
- package/workflows/container-contents/pendingWrites/index.d.ts +5 -0
- package/workflows/container-contents/pendingWrites/index.js +7 -0
- package/workflows/container-contents/pendingWrites/index.js.map +1 -1
- package/workflows/container-contents/remoteHydration/index.js +7 -1
- package/workflows/container-contents/remoteHydration/index.js.map +1 -1
- package/workflows/container-contents/remoteHydration/remoteContainers/organizationSyncPauseFilter.js +13 -0
- package/workflows/container-contents/remoteHydration/remoteContainers/organizationSyncPauseFilter.js.map +1 -0
- package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js +13 -2
- package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js.map +1 -1
- package/workflows/container-contents/syncPause/discoveryGuard.js +31 -0
- package/workflows/container-contents/syncPause/discoveryGuard.js.map +1 -0
- package/workflows/container-contents/syncPause/documentRemoval.js +154 -0
- package/workflows/container-contents/syncPause/documentRemoval.js.map +1 -0
- package/workflows/organizations/index.d.ts +3 -0
- package/workflows/organizations/index.js +3 -0
- package/workflows/organizations/index.js.map +1 -1
- package/workflows/organizations/memberships/leaveRequest.d.ts +31 -0
- package/workflows/organizations/memberships/leaveRequest.js +20 -0
- package/workflows/organizations/memberships/leaveRequest.js.map +1 -0
- package/workflows/organizations/memberships/leaveRequestVerification.d.ts +15 -0
- package/workflows/organizations/memberships/leaveRequestVerification.js +46 -0
- package/workflows/organizations/memberships/leaveRequestVerification.js.map +1 -0
- package/workflows/organizations/memberships/organizationMemberships.d.ts +84 -0
- package/workflows/organizations/memberships/organizationMemberships.js +95 -0
- package/workflows/organizations/memberships/organizationMemberships.js.map +1 -0
- package/workflows/sync/index.d.ts +1 -0
- package/workflows/sync/index.js +1 -0
- package/workflows/sync/index.js.map +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RequestResult } from "../../../internal/api-client/index.js";
|
|
2
|
+
import { type SigningKeyPair } from "../../../internal/crypto/index.js";
|
|
3
|
+
import type { OrganizationMembershipResponse } from "../../../internal/validators/response/index.js";
|
|
4
|
+
import type { OrganizationLeaveRequest } from "../../../internal/validators/util/index.js";
|
|
5
|
+
import type { OrganizationMembership } from "./organizationMemberships.js";
|
|
6
|
+
export interface OrganizationLeaveRequestApi {
|
|
7
|
+
setOrganizationLeaveRequest(organizationId: string, request: OrganizationLeaveRequest | null): Promise<RequestResult<OrganizationMembershipResponse>>;
|
|
8
|
+
}
|
|
9
|
+
export type OrganizationLeaveRequestResult = {
|
|
10
|
+
readonly status: "updated";
|
|
11
|
+
readonly membership: OrganizationMembership;
|
|
12
|
+
} | {
|
|
13
|
+
readonly status: "failed";
|
|
14
|
+
readonly httpStatus: number | null;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Files or withdraws the user's request to leave one organization. Filing
|
|
19
|
+
* signs the request with the user's identity key, so administrators can tell
|
|
20
|
+
* the member made it; it removes nothing until an administrator approves it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setOrganizationLeaveRequested(input: {
|
|
23
|
+
readonly apiClient: OrganizationLeaveRequestApi;
|
|
24
|
+
readonly organizationId: string;
|
|
25
|
+
readonly requested: boolean;
|
|
26
|
+
readonly signer: {
|
|
27
|
+
readonly signingKeyPair: Pick<SigningKeyPair, "signingPrivateKey">;
|
|
28
|
+
readonly userId: string;
|
|
29
|
+
};
|
|
30
|
+
readonly now?: () => Date;
|
|
31
|
+
}): Promise<OrganizationLeaveRequestResult>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { signOrganizationLeaveRequest, } from "../../../internal/crypto/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Files or withdraws the user's request to leave one organization. Filing
|
|
4
|
+
* signs the request with the user's identity key, so administrators can tell
|
|
5
|
+
* the member made it; it removes nothing until an administrator approves it.
|
|
6
|
+
*/
|
|
7
|
+
export async function setOrganizationLeaveRequested(input) {
|
|
8
|
+
const request = input.requested
|
|
9
|
+
? signOrganizationLeaveRequest({
|
|
10
|
+
organizationId: input.organizationId,
|
|
11
|
+
requestedAt: (input.now?.() ?? new Date()).toISOString(),
|
|
12
|
+
userId: input.signer.userId,
|
|
13
|
+
}, input.signer.signingKeyPair)
|
|
14
|
+
: null;
|
|
15
|
+
const result = await input.apiClient.setOrganizationLeaveRequest(input.organizationId, request);
|
|
16
|
+
return result.ok
|
|
17
|
+
? { membership: result.data, status: "updated" }
|
|
18
|
+
: { httpStatus: result.status, message: result.message, status: "failed" };
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=leaveRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leaveRequest.js","sourceRoot":"","sources":["leaveRequest.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAoB3B;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAAC,KASnD;IACC,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS;QAC7B,CAAC,CAAC,4BAA4B,CAC1B;YACE,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE;YACxD,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM;SAC5B,EACD,KAAK,CAAC,MAAM,CAAC,cAAc,CAC5B;QACH,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,2BAA2B,CAC9D,KAAK,CAAC,cAAc,EACpB,OAAO,CACR,CAAC;IACF,OAAO,MAAM,CAAC,EAAE;QACd,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE;QAChD,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC/E,CAAC","sourcesContent":["import type { RequestResult } from \"@tearleads/api-client\";\nimport {\n type SigningKeyPair,\n signOrganizationLeaveRequest,\n} from \"@tearleads/crypto\";\nimport type { OrganizationMembershipResponse } from \"@tearleads/validators/response\";\nimport type { OrganizationLeaveRequest } from \"@tearleads/validators/util\";\nimport type { OrganizationMembership } from \"./organizationMemberships\";\n\nexport interface OrganizationLeaveRequestApi {\n setOrganizationLeaveRequest(\n organizationId: string,\n request: OrganizationLeaveRequest | null,\n ): Promise<RequestResult<OrganizationMembershipResponse>>;\n}\n\nexport type OrganizationLeaveRequestResult =\n | { readonly status: \"updated\"; readonly membership: OrganizationMembership }\n | {\n readonly status: \"failed\";\n readonly httpStatus: number | null;\n readonly message: string;\n };\n\n/**\n * Files or withdraws the user's request to leave one organization. Filing\n * signs the request with the user's identity key, so administrators can tell\n * the member made it; it removes nothing until an administrator approves it.\n */\nexport async function setOrganizationLeaveRequested(input: {\n readonly apiClient: OrganizationLeaveRequestApi;\n readonly organizationId: string;\n readonly requested: boolean;\n readonly signer: {\n readonly signingKeyPair: Pick<SigningKeyPair, \"signingPrivateKey\">;\n readonly userId: string;\n };\n readonly now?: () => Date;\n}): Promise<OrganizationLeaveRequestResult> {\n const request = input.requested\n ? signOrganizationLeaveRequest(\n {\n organizationId: input.organizationId,\n requestedAt: (input.now?.() ?? new Date()).toISOString(),\n userId: input.signer.userId,\n },\n input.signer.signingKeyPair,\n )\n : null;\n const result = await input.apiClient.setOrganizationLeaveRequest(\n input.organizationId,\n request,\n );\n return result.ok\n ? { membership: result.data, status: \"updated\" }\n : { httpStatus: result.status, message: result.message, status: \"failed\" };\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TrustedUserIdentityResolver } from "../../../data/trustedUserIdentity/index.js";
|
|
2
|
+
import type { OrganizationDirectoryAndGroups, OrganizationDirectoryUser, OrganizationUserDetail } from "../readModel/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* The user as shown, with its leave request kept only when the member signed
|
|
5
|
+
* it. Every directory user a client presents passes through here, whether a
|
|
6
|
+
* read model or a mutation response carried it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function withVerifiedDirectoryUser(organizationId: string, user: OrganizationDirectoryUser, resolveTrustedUserIdentity: TrustedUserIdentityResolver): Promise<OrganizationDirectoryUser>;
|
|
9
|
+
/**
|
|
10
|
+
* Keeps only leave requests their member signed, checked against the member's
|
|
11
|
+
* trusted identity; any other is shown as no request at all.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withVerifiedLeaveRequests<Result extends OrganizationDirectoryAndGroups | null | undefined>(result: Result, resolveTrustedUserIdentity: TrustedUserIdentityResolver): Promise<Result>;
|
|
14
|
+
/** The user detail counterpart of withVerifiedLeaveRequests. */
|
|
15
|
+
export declare function withVerifiedUserLeaveRequest(detail: OrganizationUserDetail | null, organizationId: string, resolveTrustedUserIdentity: TrustedUserIdentityResolver): Promise<OrganizationUserDetail | null>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isValidOrganizationLeaveRequest } from "../../../internal/crypto/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The user as shown, with its leave request kept only when the member signed
|
|
4
|
+
* it. Every directory user a client presents passes through here, whether a
|
|
5
|
+
* read model or a mutation response carried it.
|
|
6
|
+
*/
|
|
7
|
+
export async function withVerifiedDirectoryUser(organizationId, user, resolveTrustedUserIdentity) {
|
|
8
|
+
const request = user.leaveRequest;
|
|
9
|
+
if (!request)
|
|
10
|
+
return user;
|
|
11
|
+
// The member's pinned identity, never the key the directory served beside
|
|
12
|
+
// the request: a server that could swap both could forge any member's ask.
|
|
13
|
+
const identity = await resolveTrustedUserIdentity(user.userId).catch(() => null);
|
|
14
|
+
const verified = identity !== null &&
|
|
15
|
+
request.organizationId === organizationId &&
|
|
16
|
+
request.userId === user.userId &&
|
|
17
|
+
isValidOrganizationLeaveRequest(request, identity.signingPublicKey);
|
|
18
|
+
return verified ? user : { ...user, leaveRequest: null };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Keeps only leave requests their member signed, checked against the member's
|
|
22
|
+
* trusted identity; any other is shown as no request at all.
|
|
23
|
+
*/
|
|
24
|
+
export async function withVerifiedLeaveRequests(result, resolveTrustedUserIdentity) {
|
|
25
|
+
if (!result?.directory.users.some((user) => user.leaveRequest)) {
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
const { organizationId } = result.directory;
|
|
29
|
+
return {
|
|
30
|
+
...result,
|
|
31
|
+
directory: {
|
|
32
|
+
...result.directory,
|
|
33
|
+
users: await Promise.all(result.directory.users.map((user) => withVerifiedDirectoryUser(organizationId, user, resolveTrustedUserIdentity))),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** The user detail counterpart of withVerifiedLeaveRequests. */
|
|
38
|
+
export async function withVerifiedUserLeaveRequest(detail, organizationId, resolveTrustedUserIdentity) {
|
|
39
|
+
if (!detail?.user.leaveRequest)
|
|
40
|
+
return detail;
|
|
41
|
+
return {
|
|
42
|
+
...detail,
|
|
43
|
+
user: await withVerifiedDirectoryUser(organizationId, detail.user, resolveTrustedUserIdentity),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=leaveRequestVerification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leaveRequestVerification.js","sourceRoot":"","sources":["leaveRequestVerification.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;AAQpE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,cAAsB,EACtB,IAA+B,EAC/B,0BAAuD;IAEvD,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC;IAClC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,MAAM,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAClE,GAAG,EAAE,CAAC,IAAI,CACX,CAAC;IACF,MAAM,QAAQ,GACZ,QAAQ,KAAK,IAAI;QACjB,OAAO,CAAC,cAAc,KAAK,cAAc;QACzC,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM;QAC9B,+BAA+B,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACtE,OAAO,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAG7C,MAAc,EACd,0BAAuD;IAEvD,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/D,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC;IAC5C,OAAO;QACL,GAAG,MAAM;QACT,SAAS,EAAE;YACT,GAAG,MAAM,CAAC,SAAS;YACnB,KAAK,EAAE,MAAM,OAAO,CAAC,GAAG,CACtB,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAClC,yBAAyB,CACvB,cAAc,EACd,IAAI,EACJ,0BAA0B,CAC3B,CACF,CACF;SACF;KACF,CAAC;AACJ,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,MAAqC,EACrC,cAAsB,EACtB,0BAAuD;IAEvD,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY;QAAE,OAAO,MAAM,CAAC;IAC9C,OAAO;QACL,GAAG,MAAM;QACT,IAAI,EAAE,MAAM,yBAAyB,CACnC,cAAc,EACd,MAAM,CAAC,IAAI,EACX,0BAA0B,CAC3B;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { isValidOrganizationLeaveRequest } from \"@tearleads/crypto\";\nimport type { TrustedUserIdentityResolver } from \"../../../data/trustedUserIdentity\";\nimport type {\n OrganizationDirectoryAndGroups,\n OrganizationDirectoryUser,\n OrganizationUserDetail,\n} from \"../readModel\";\n\n/**\n * The user as shown, with its leave request kept only when the member signed\n * it. Every directory user a client presents passes through here, whether a\n * read model or a mutation response carried it.\n */\nexport async function withVerifiedDirectoryUser(\n organizationId: string,\n user: OrganizationDirectoryUser,\n resolveTrustedUserIdentity: TrustedUserIdentityResolver,\n): Promise<OrganizationDirectoryUser> {\n const request = user.leaveRequest;\n if (!request) return user;\n // The member's pinned identity, never the key the directory served beside\n // the request: a server that could swap both could forge any member's ask.\n const identity = await resolveTrustedUserIdentity(user.userId).catch(\n () => null,\n );\n const verified =\n identity !== null &&\n request.organizationId === organizationId &&\n request.userId === user.userId &&\n isValidOrganizationLeaveRequest(request, identity.signingPublicKey);\n return verified ? user : { ...user, leaveRequest: null };\n}\n\n/**\n * Keeps only leave requests their member signed, checked against the member's\n * trusted identity; any other is shown as no request at all.\n */\nexport async function withVerifiedLeaveRequests<\n Result extends OrganizationDirectoryAndGroups | null | undefined,\n>(\n result: Result,\n resolveTrustedUserIdentity: TrustedUserIdentityResolver,\n): Promise<Result> {\n if (!result?.directory.users.some((user) => user.leaveRequest)) {\n return result;\n }\n const { organizationId } = result.directory;\n return {\n ...result,\n directory: {\n ...result.directory,\n users: await Promise.all(\n result.directory.users.map((user) =>\n withVerifiedDirectoryUser(\n organizationId,\n user,\n resolveTrustedUserIdentity,\n ),\n ),\n ),\n },\n };\n}\n\n/** The user detail counterpart of withVerifiedLeaveRequests. */\nexport async function withVerifiedUserLeaveRequest(\n detail: OrganizationUserDetail | null,\n organizationId: string,\n resolveTrustedUserIdentity: TrustedUserIdentityResolver,\n): Promise<OrganizationUserDetail | null> {\n if (!detail?.user.leaveRequest) return detail;\n return {\n ...detail,\n user: await withVerifiedDirectoryUser(\n organizationId,\n detail.user,\n resolveTrustedUserIdentity,\n ),\n };\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { RequestResult } from "../../../internal/api-client/index.js";
|
|
2
|
+
import type { UpdateOrganizationMembershipRequest } from "../../../internal/validators/request/index.js";
|
|
3
|
+
import type { OrganizationMembershipResponse, OrganizationMembershipsResponse } from "../../../internal/validators/response/index.js";
|
|
4
|
+
import type { DomainScope } from "../../../data/shared/domainScope.js";
|
|
5
|
+
import type { ExecSql } from "../../../data/sqlite/sqlSchema.js";
|
|
6
|
+
import { type OrganizationSyncPauses } from "../../sync/index.js";
|
|
7
|
+
/** One organization the user belongs to, with their own choices for it. */
|
|
8
|
+
export type OrganizationMembership = OrganizationMembershipResponse;
|
|
9
|
+
export interface OrganizationMembershipApi {
|
|
10
|
+
listOrganizationMemberships(): Promise<OrganizationMembershipsResponse | null>;
|
|
11
|
+
updateOrganizationMembership(organizationId: string, input: UpdateOrganizationMembershipRequest): Promise<RequestResult<OrganizationMembershipResponse>>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What applying a pause needs from the device's content. Wired by the
|
|
15
|
+
* composition root to the container tree and the document sync lanes.
|
|
16
|
+
*/
|
|
17
|
+
export interface OrganizationContentHost {
|
|
18
|
+
/**
|
|
19
|
+
* Removes an organization's content, keeping its metadata root. Refuses,
|
|
20
|
+
* returning false, while any of its writes is unsynced or still queued in
|
|
21
|
+
* an open store.
|
|
22
|
+
*/
|
|
23
|
+
forgetOrganizationContent(organizationId: string): Promise<boolean>;
|
|
24
|
+
/** Whether any local write for the organization has not uploaded yet. */
|
|
25
|
+
hasUnsyncedOrganizationWrites(organizationId: string): Promise<boolean>;
|
|
26
|
+
/** Asks every sync lane to upload now and waits briefly for them to idle. */
|
|
27
|
+
flushWrites(): Promise<void>;
|
|
28
|
+
/** Re-lists the whole tree so resumed organizations download again. */
|
|
29
|
+
refetchContent(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Runs `retry` each time the scope's sync lanes go idle until it resolves
|
|
32
|
+
* true. A later call replaces the pending retry.
|
|
33
|
+
*/
|
|
34
|
+
retryWhenWritesSettle(retry: () => Promise<boolean>): void;
|
|
35
|
+
}
|
|
36
|
+
interface OrganizationMembershipInput {
|
|
37
|
+
readonly apiClient: OrganizationMembershipApi;
|
|
38
|
+
readonly domainScope: DomainScope;
|
|
39
|
+
readonly execSql: ExecSql;
|
|
40
|
+
readonly host: OrganizationContentHost;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the session still uses `domainScope`. A membership list or pause
|
|
43
|
+
* that completes after an identity switch must not touch the new identity's
|
|
44
|
+
* device state.
|
|
45
|
+
*/
|
|
46
|
+
readonly isCurrent: () => boolean;
|
|
47
|
+
}
|
|
48
|
+
export type OrganizationSyncPauseResult = {
|
|
49
|
+
readonly status: "updated";
|
|
50
|
+
readonly membership: OrganizationMembership;
|
|
51
|
+
} | {
|
|
52
|
+
readonly status: "unsynced-writes";
|
|
53
|
+
} | {
|
|
54
|
+
readonly status: "failed";
|
|
55
|
+
readonly httpStatus: number | null;
|
|
56
|
+
readonly message: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Removes the content of every paused organization that has nothing left to
|
|
60
|
+
* upload. An organization with pending or queued writes keeps syncing those
|
|
61
|
+
* out and is retried once the sync lanes go idle, so pausing never discards
|
|
62
|
+
* unsynced work and a removal does not wait for the next membership read.
|
|
63
|
+
*/
|
|
64
|
+
export declare function forgetPausedOrganizationContent(input: {
|
|
65
|
+
readonly host: OrganizationContentHost;
|
|
66
|
+
readonly pauses: OrganizationSyncPauses;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Reads the user's memberships and applies their pause choices, including
|
|
70
|
+
* ones made on another device: paused organizations leave this device, and
|
|
71
|
+
* resumed ones download again. Returns null when the list is unavailable, in
|
|
72
|
+
* which case the persisted pauses stay in force.
|
|
73
|
+
*/
|
|
74
|
+
export declare function refreshOrganizationMemberships(input: OrganizationMembershipInput): Promise<OrganizationMembership[] | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Pauses or resumes one organization for every device of this user. Pausing
|
|
77
|
+
* first uploads this device's pending writes and is refused while any remain,
|
|
78
|
+
* so the local copy it removes holds nothing the server lacks.
|
|
79
|
+
*/
|
|
80
|
+
export declare function setOrganizationSyncPaused(input: OrganizationMembershipInput & {
|
|
81
|
+
readonly organizationId: string;
|
|
82
|
+
readonly paused: boolean;
|
|
83
|
+
}): Promise<OrganizationSyncPauseResult>;
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getOrCreateOrganizationSyncPauses, } from "../../sync/index.js";
|
|
2
|
+
async function forgetEveryPausedOrganization(input) {
|
|
3
|
+
let complete = true;
|
|
4
|
+
for (const organizationId of [...input.pauses.pausedOrganizationIds()]) {
|
|
5
|
+
if (!(await input.host.forgetOrganizationContent(organizationId))) {
|
|
6
|
+
complete = false;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return complete;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Removes the content of every paused organization that has nothing left to
|
|
13
|
+
* upload. An organization with pending or queued writes keeps syncing those
|
|
14
|
+
* out and is retried once the sync lanes go idle, so pausing never discards
|
|
15
|
+
* unsynced work and a removal does not wait for the next membership read.
|
|
16
|
+
*/
|
|
17
|
+
export async function forgetPausedOrganizationContent(input) {
|
|
18
|
+
if (await forgetEveryPausedOrganization(input))
|
|
19
|
+
return;
|
|
20
|
+
input.host.retryWhenWritesSettle(() => input.pauses.runExclusive(() => forgetEveryPausedOrganization(input)));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads the user's memberships and applies their pause choices, including
|
|
24
|
+
* ones made on another device: paused organizations leave this device, and
|
|
25
|
+
* resumed ones download again. Returns null when the list is unavailable, in
|
|
26
|
+
* which case the persisted pauses stay in force.
|
|
27
|
+
*/
|
|
28
|
+
export async function refreshOrganizationMemberships(input) {
|
|
29
|
+
const pauses = getOrCreateOrganizationSyncPauses(input.domainScope);
|
|
30
|
+
return pauses.runExclusive(() => applyMembershipList(input, pauses));
|
|
31
|
+
}
|
|
32
|
+
async function applyMembershipList(input, pauses) {
|
|
33
|
+
const response = await input.apiClient.listOrganizationMemberships();
|
|
34
|
+
if (!response || !input.isCurrent())
|
|
35
|
+
return null;
|
|
36
|
+
const change = await pauses.replace(input.execSql, response.memberships.flatMap((membership) => membership.syncPaused ? [membership.organizationId] : []));
|
|
37
|
+
await forgetPausedOrganizationContent({ host: input.host, pauses });
|
|
38
|
+
if (change.resumed.length > 0)
|
|
39
|
+
await input.host.refetchContent();
|
|
40
|
+
return [...response.memberships];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pauses or resumes one organization for every device of this user. Pausing
|
|
44
|
+
* first uploads this device's pending writes and is refused while any remain,
|
|
45
|
+
* so the local copy it removes holds nothing the server lacks.
|
|
46
|
+
*/
|
|
47
|
+
export async function setOrganizationSyncPaused(input) {
|
|
48
|
+
const pauses = getOrCreateOrganizationSyncPauses(input.domainScope);
|
|
49
|
+
return pauses.runExclusive(() => applySyncPauseChoice(input, pauses));
|
|
50
|
+
}
|
|
51
|
+
async function applySyncPauseChoice(input, pauses) {
|
|
52
|
+
const { host, organizationId, paused } = input;
|
|
53
|
+
const scopeEnded = {
|
|
54
|
+
httpStatus: null,
|
|
55
|
+
message: "The session changed before sync could be changed.",
|
|
56
|
+
status: "failed",
|
|
57
|
+
};
|
|
58
|
+
// Queued behind other membership work, the session may have switched
|
|
59
|
+
// identity; the shared API client would then act for the new identity.
|
|
60
|
+
if (!input.isCurrent())
|
|
61
|
+
return scopeEnded;
|
|
62
|
+
if (paused) {
|
|
63
|
+
await host.flushWrites();
|
|
64
|
+
if (await host.hasUnsyncedOrganizationWrites(organizationId)) {
|
|
65
|
+
return { status: "unsynced-writes" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!input.isCurrent())
|
|
69
|
+
return scopeEnded;
|
|
70
|
+
const result = await input.apiClient.updateOrganizationMembership(organizationId, { syncPaused: paused });
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
return {
|
|
73
|
+
httpStatus: result.status,
|
|
74
|
+
message: result.message,
|
|
75
|
+
status: "failed",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// The choice is recorded; a scope that ended meanwhile applies it on its
|
|
79
|
+
// next membership read instead.
|
|
80
|
+
if (!input.isCurrent())
|
|
81
|
+
return { membership: result.data, status: "updated" };
|
|
82
|
+
await pauses.load(input.execSql);
|
|
83
|
+
const next = new Set(pauses.pausedOrganizationIds());
|
|
84
|
+
if (paused)
|
|
85
|
+
next.add(organizationId);
|
|
86
|
+
else
|
|
87
|
+
next.delete(organizationId);
|
|
88
|
+
await pauses.replace(input.execSql, next);
|
|
89
|
+
if (paused)
|
|
90
|
+
await forgetPausedOrganizationContent({ host, pauses });
|
|
91
|
+
else
|
|
92
|
+
await host.refetchContent();
|
|
93
|
+
return { membership: result.data, status: "updated" };
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=organizationMemberships.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"organizationMemberships.js","sourceRoot":"","sources":["organizationMemberships.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RequestResult } from \"@tearleads/api-client\";\nimport type { UpdateOrganizationMembershipRequest } from \"@tearleads/validators/request\";\nimport type {\n OrganizationMembershipResponse,\n OrganizationMembershipsResponse,\n} from \"@tearleads/validators/response\";\nimport type { DomainScope } from \"../../../data/shared/domainScope\";\nimport type { ExecSql } from \"../../../data/sqlite/sqlSchema\";\nimport {\n getOrCreateOrganizationSyncPauses,\n type OrganizationSyncPauses,\n} from \"../../sync\";\n\n/** One organization the user belongs to, with their own choices for it. */\nexport type OrganizationMembership = OrganizationMembershipResponse;\n\nexport interface OrganizationMembershipApi {\n listOrganizationMemberships(): Promise<OrganizationMembershipsResponse | null>;\n updateOrganizationMembership(\n organizationId: string,\n input: UpdateOrganizationMembershipRequest,\n ): Promise<RequestResult<OrganizationMembershipResponse>>;\n}\n\n/**\n * What applying a pause needs from the device's content. Wired by the\n * composition root to the container tree and the document sync lanes.\n */\nexport interface OrganizationContentHost {\n /**\n * Removes an organization's content, keeping its metadata root. Refuses,\n * returning false, while any of its writes is unsynced or still queued in\n * an open store.\n */\n forgetOrganizationContent(organizationId: string): Promise<boolean>;\n /** Whether any local write for the organization has not uploaded yet. */\n hasUnsyncedOrganizationWrites(organizationId: string): Promise<boolean>;\n /** Asks every sync lane to upload now and waits briefly for them to idle. */\n flushWrites(): Promise<void>;\n /** Re-lists the whole tree so resumed organizations download again. */\n refetchContent(): Promise<void>;\n /**\n * Runs `retry` each time the scope's sync lanes go idle until it resolves\n * true. A later call replaces the pending retry.\n */\n retryWhenWritesSettle(retry: () => Promise<boolean>): void;\n}\n\ninterface OrganizationMembershipInput {\n readonly apiClient: OrganizationMembershipApi;\n readonly domainScope: DomainScope;\n readonly execSql: ExecSql;\n readonly host: OrganizationContentHost;\n /**\n * Whether the session still uses `domainScope`. A membership list or pause\n * that completes after an identity switch must not touch the new identity's\n * device state.\n */\n readonly isCurrent: () => boolean;\n}\n\nexport type OrganizationSyncPauseResult =\n | { readonly status: \"updated\"; readonly membership: OrganizationMembership }\n | { readonly status: \"unsynced-writes\" }\n | {\n readonly status: \"failed\";\n readonly httpStatus: number | null;\n readonly message: string;\n };\n\nasync function forgetEveryPausedOrganization(input: {\n readonly host: OrganizationContentHost;\n readonly pauses: OrganizationSyncPauses;\n}): Promise<boolean> {\n let complete = true;\n for (const organizationId of [...input.pauses.pausedOrganizationIds()]) {\n if (!(await input.host.forgetOrganizationContent(organizationId))) {\n complete = false;\n }\n }\n return complete;\n}\n\n/**\n * Removes the content of every paused organization that has nothing left to\n * upload. An organization with pending or queued writes keeps syncing those\n * out and is retried once the sync lanes go idle, so pausing never discards\n * unsynced work and a removal does not wait for the next membership read.\n */\nexport async function forgetPausedOrganizationContent(input: {\n readonly host: OrganizationContentHost;\n readonly pauses: OrganizationSyncPauses;\n}): Promise<void> {\n if (await forgetEveryPausedOrganization(input)) return;\n input.host.retryWhenWritesSettle(() =>\n input.pauses.runExclusive(() => forgetEveryPausedOrganization(input)),\n );\n}\n\n/**\n * Reads the user's memberships and applies their pause choices, including\n * ones made on another device: paused organizations leave this device, and\n * resumed ones download again. Returns null when the list is unavailable, in\n * which case the persisted pauses stay in force.\n */\nexport async function refreshOrganizationMemberships(\n input: OrganizationMembershipInput,\n): Promise<OrganizationMembership[] | null> {\n const pauses = getOrCreateOrganizationSyncPauses(input.domainScope);\n return pauses.runExclusive(() => applyMembershipList(input, pauses));\n}\n\nasync function applyMembershipList(\n input: OrganizationMembershipInput,\n pauses: OrganizationSyncPauses,\n): Promise<OrganizationMembership[] | null> {\n const response = await input.apiClient.listOrganizationMemberships();\n if (!response || !input.isCurrent()) return null;\n const change = await pauses.replace(\n input.execSql,\n response.memberships.flatMap((membership) =>\n membership.syncPaused ? [membership.organizationId] : [],\n ),\n );\n await forgetPausedOrganizationContent({ host: input.host, pauses });\n if (change.resumed.length > 0) await input.host.refetchContent();\n return [...response.memberships];\n}\n\n/**\n * Pauses or resumes one organization for every device of this user. Pausing\n * first uploads this device's pending writes and is refused while any remain,\n * so the local copy it removes holds nothing the server lacks.\n */\nexport async function setOrganizationSyncPaused(\n input: OrganizationMembershipInput & {\n readonly organizationId: string;\n readonly paused: boolean;\n },\n): Promise<OrganizationSyncPauseResult> {\n const pauses = getOrCreateOrganizationSyncPauses(input.domainScope);\n return pauses.runExclusive(() => applySyncPauseChoice(input, pauses));\n}\n\nasync function applySyncPauseChoice(\n input: OrganizationMembershipInput & {\n readonly organizationId: string;\n readonly paused: boolean;\n },\n pauses: OrganizationSyncPauses,\n): Promise<OrganizationSyncPauseResult> {\n const { host, organizationId, paused } = input;\n const scopeEnded = {\n httpStatus: null,\n message: \"The session changed before sync could be changed.\",\n status: \"failed\",\n } as const;\n // Queued behind other membership work, the session may have switched\n // identity; the shared API client would then act for the new identity.\n if (!input.isCurrent()) return scopeEnded;\n if (paused) {\n await host.flushWrites();\n if (await host.hasUnsyncedOrganizationWrites(organizationId)) {\n return { status: \"unsynced-writes\" };\n }\n }\n if (!input.isCurrent()) return scopeEnded;\n const result = await input.apiClient.updateOrganizationMembership(\n organizationId,\n { syncPaused: paused },\n );\n if (!result.ok) {\n return {\n httpStatus: result.status,\n message: result.message,\n status: \"failed\",\n };\n }\n // The choice is recorded; a scope that ended meanwhile applies it on its\n // next membership read instead.\n if (!input.isCurrent()) return { membership: result.data, status: \"updated\" };\n await pauses.load(input.execSql);\n const next = new Set(pauses.pausedOrganizationIds());\n if (paused) next.add(organizationId);\n else next.delete(organizationId);\n await pauses.replace(input.execSql, next);\n if (paused) await forgetPausedOrganizationContent({ host, pauses });\n else await host.refetchContent();\n return { membership: result.data, status: \"updated\" };\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createDomainScope, type DomainScope, } from "../../data/shared/domainScope.js";
|
|
2
2
|
export { getRuntimePeerSeed } from "../../data/sync/crdtPeerSeed.js";
|
|
3
|
+
export { getOrCreateOrganizationSyncPauses, isPauseRemovedDocument, isRetainedWhileSyncPaused, type OrganizationSyncPauseChange, type OrganizationSyncPauseSubject, type OrganizationSyncPauses, recordPauseRemovedDocument, } from "../../data/sync/organizationSyncPauses.js";
|
|
3
4
|
export { markOriginatedDocuments } from "../../data/sync/originatedDocuments.js";
|
|
4
5
|
export type { DomainSyncCoordinator, DomainSyncSnapshot, SyncIdleOptions, SyncLane, SyncLaneConfig, SyncLaneLastAction, SyncLanePhase, SyncLaneProgress, SyncLaneSnapshot, SyncLaneStatus, UploadSyncLane, UploadSyncLaneOptions, } from "../../data/sync/syncCoordinator.js";
|
|
5
6
|
export { beginDomainSyncUploadLane, getDomainSyncCoordinatorSnapshot, getOrCreateDomainSyncCoordinator, hasDomainSyncCoordinatorPendingWork, requestAllDomainSyncLanes, subscribeToDomainSyncCoordinator, waitForDomainSyncCoordinatorToSettle, } from "../../data/sync/syncCoordinator.js";
|
package/workflows/sync/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createDomainScope, } from "../../data/shared/domainScope.js";
|
|
2
2
|
export { getRuntimePeerSeed } from "../../data/sync/crdtPeerSeed.js";
|
|
3
|
+
export { getOrCreateOrganizationSyncPauses, isPauseRemovedDocument, isRetainedWhileSyncPaused, recordPauseRemovedDocument, } from "../../data/sync/organizationSyncPauses.js";
|
|
3
4
|
export { markOriginatedDocuments } from "../../data/sync/originatedDocuments.js";
|
|
4
5
|
export { beginDomainSyncUploadLane, getDomainSyncCoordinatorSnapshot, getOrCreateDomainSyncCoordinator, hasDomainSyncCoordinatorPendingWork, requestAllDomainSyncLanes, subscribeToDomainSyncCoordinator, waitForDomainSyncCoordinatorToSettle, } from "../../data/sync/syncCoordinator.js";
|
|
5
6
|
export { clearRemoteSyncState, } from "./remoteReset.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,GAElB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAe9E,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,gCAAgC,EAChC,mCAAmC,EACnC,yBAAyB,EACzB,gCAAgC,EAChC,oCAAoC,GACrC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAEL,oBAAoB,GAGrB,MAAM,eAAe,CAAC","sourcesContent":["export {\n createDomainScope,\n type DomainScope,\n} from \"../../data/shared/domainScope\";\nexport { getRuntimePeerSeed } from \"../../data/sync/crdtPeerSeed\";\nexport { markOriginatedDocuments } from \"../../data/sync/originatedDocuments\";\nexport type {\n DomainSyncCoordinator,\n DomainSyncSnapshot,\n SyncIdleOptions,\n SyncLane,\n SyncLaneConfig,\n SyncLaneLastAction,\n SyncLanePhase,\n SyncLaneProgress,\n SyncLaneSnapshot,\n SyncLaneStatus,\n UploadSyncLane,\n UploadSyncLaneOptions,\n} from \"../../data/sync/syncCoordinator\";\nexport {\n beginDomainSyncUploadLane,\n getDomainSyncCoordinatorSnapshot,\n getOrCreateDomainSyncCoordinator,\n hasDomainSyncCoordinatorPendingWork,\n requestAllDomainSyncLanes,\n subscribeToDomainSyncCoordinator,\n waitForDomainSyncCoordinatorToSettle,\n} from \"../../data/sync/syncCoordinator\";\nexport {\n type ClearRemoteSyncStateResult,\n clearRemoteSyncState,\n type RemoteResetInput,\n type RemoteResetReplacement,\n} from \"./remoteReset\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,GAElB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EACL,iCAAiC,EACjC,sBAAsB,EACtB,yBAAyB,EAIzB,0BAA0B,GAC3B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAe9E,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,gCAAgC,EAChC,mCAAmC,EACnC,yBAAyB,EACzB,gCAAgC,EAChC,oCAAoC,GACrC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAEL,oBAAoB,GAGrB,MAAM,eAAe,CAAC","sourcesContent":["export {\n createDomainScope,\n type DomainScope,\n} from \"../../data/shared/domainScope\";\nexport { getRuntimePeerSeed } from \"../../data/sync/crdtPeerSeed\";\nexport {\n getOrCreateOrganizationSyncPauses,\n isPauseRemovedDocument,\n isRetainedWhileSyncPaused,\n type OrganizationSyncPauseChange,\n type OrganizationSyncPauseSubject,\n type OrganizationSyncPauses,\n recordPauseRemovedDocument,\n} from \"../../data/sync/organizationSyncPauses\";\nexport { markOriginatedDocuments } from \"../../data/sync/originatedDocuments\";\nexport type {\n DomainSyncCoordinator,\n DomainSyncSnapshot,\n SyncIdleOptions,\n SyncLane,\n SyncLaneConfig,\n SyncLaneLastAction,\n SyncLanePhase,\n SyncLaneProgress,\n SyncLaneSnapshot,\n SyncLaneStatus,\n UploadSyncLane,\n UploadSyncLaneOptions,\n} from \"../../data/sync/syncCoordinator\";\nexport {\n beginDomainSyncUploadLane,\n getDomainSyncCoordinatorSnapshot,\n getOrCreateDomainSyncCoordinator,\n hasDomainSyncCoordinatorPendingWork,\n requestAllDomainSyncLanes,\n subscribeToDomainSyncCoordinator,\n waitForDomainSyncCoordinatorToSettle,\n} from \"../../data/sync/syncCoordinator\";\nexport {\n type ClearRemoteSyncStateResult,\n clearRemoteSyncState,\n type RemoteResetInput,\n type RemoteResetReplacement,\n} from \"./remoteReset\";\n"]}
|