@tearleads/client-sdk 0.1.87 → 0.1.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/Tearleads.js +1 -1
  2. package/Tearleads.js.map +1 -1
  3. package/client/containerContents/documentDiscovery.js +16 -2
  4. package/client/containerContents/documentDiscovery.js.map +1 -1
  5. package/client/containerContents/index.d.ts +1 -1
  6. package/client/containerContents/index.js +1 -0
  7. package/client/containerContents/index.js.map +1 -1
  8. package/client/index.d.ts +1 -1
  9. package/client/index.js.map +1 -1
  10. package/client/organizations/index.d.ts +4 -2
  11. package/client/organizations/index.js +6 -3
  12. package/client/organizations/index.js.map +1 -1
  13. package/client/organizations/organizationMemberships.d.ts +30 -0
  14. package/client/organizations/organizationMemberships.js +132 -0
  15. package/client/organizations/organizationMemberships.js.map +1 -0
  16. package/client/organizations/organizationsTypes.d.ts +3 -0
  17. package/client/organizations/organizationsTypes.js.map +1 -1
  18. package/data/persistence/container-contents/containerStructuralRepair.js +19 -14
  19. package/data/persistence/container-contents/containerStructuralRepair.js.map +1 -1
  20. package/data/persistence/container-contents/exclusiveDocumentPlacements.js +60 -0
  21. package/data/persistence/container-contents/exclusiveDocumentPlacements.js.map +1 -0
  22. package/data/sqlite/schema/index.d.ts +71 -0
  23. package/data/sqlite/schema/index.js +3 -0
  24. package/data/sqlite/schema/index.js.map +1 -1
  25. package/data/sqlite/schema/organizationSyncPauseSchema.js +32 -0
  26. package/data/sqlite/schema/organizationSyncPauseSchema.js.map +1 -0
  27. package/data/sync/organizationSyncPauses.d.ts +59 -0
  28. package/data/sync/organizationSyncPauses.js +149 -0
  29. package/data/sync/organizationSyncPauses.js.map +1 -0
  30. package/index.d.ts +2 -2
  31. package/index.js.map +1 -1
  32. package/internal/api-client/ApiClient.d.ts +11 -2
  33. package/internal/api-client/ApiClient.js +9 -1
  34. package/internal/api-client/ApiClient.js.map +1 -1
  35. package/internal/api-client/routes/organizations/memberships.js +17 -0
  36. package/internal/api-client/routes/organizations/memberships.js.map +1 -0
  37. package/internal/validators/operation/index.d.ts +1 -0
  38. package/internal/validators/operation/index.js +1 -0
  39. package/internal/validators/operation/index.js.map +1 -1
  40. package/internal/validators/operation/organizationMemberships.d.ts +240 -0
  41. package/internal/validators/operation/organizationMemberships.js +51 -0
  42. package/internal/validators/operation/organizationMemberships.js.map +1 -0
  43. package/internal/validators/operation/registry.d.ts +3 -0
  44. package/internal/validators/operation/registry.js +3 -0
  45. package/internal/validators/operation/registry.js.map +1 -1
  46. package/internal/validators/realtime/index.js +1 -1
  47. package/internal/validators/realtime/index.js.map +1 -1
  48. package/internal/validators/realtime/wsInvalidationHints.js +8 -0
  49. package/internal/validators/realtime/wsInvalidationHints.js.map +1 -1
  50. package/internal/validators/request/index.d.ts +1 -1
  51. package/internal/validators/request/index.js +1 -1
  52. package/internal/validators/request/index.js.map +1 -1
  53. package/internal/validators/request/organization.d.ts +6 -0
  54. package/internal/validators/request/organization.js +7 -0
  55. package/internal/validators/request/organization.js.map +1 -1
  56. package/internal/validators/response/index.d.ts +1 -0
  57. package/internal/validators/response/index.js +1 -0
  58. package/internal/validators/response/index.js.map +1 -1
  59. package/internal/validators/response/organizationMembership.d.ts +56 -0
  60. package/internal/validators/response/organizationMembership.js +26 -0
  61. package/internal/validators/response/organizationMembership.js.map +1 -0
  62. package/package.json +1 -1
  63. package/stores/container-contents/containerContentsStore.d.ts +1 -1
  64. package/stores/container-contents/containerContentsStore.js +2 -0
  65. package/stores/container-contents/containerContentsStore.js.map +1 -1
  66. package/stores/container-contents/operations/forgetOrganizationContentOperation.js +76 -0
  67. package/stores/container-contents/operations/forgetOrganizationContentOperation.js.map +1 -0
  68. package/stores/container-contents/types.d.ts +6 -0
  69. package/stores/container-contents/types.js.map +1 -1
  70. package/stores/documents/documentStore/attachments/attachmentRemoval.js +3 -4
  71. package/stores/documents/documentStore/attachments/attachmentRemoval.js.map +1 -1
  72. package/stores/documents/documentStore/attachments/index.js +5 -7
  73. package/stores/documents/documentStore/attachments/index.js.map +1 -1
  74. package/stores/documents/documentStore/index.js +16 -12
  75. package/stores/documents/documentStore/index.js.map +1 -1
  76. package/stores/documents/documentStore/initialization/index.js +9 -0
  77. package/stores/documents/documentStore/initialization/index.js.map +1 -1
  78. package/stores/documents/documentStore/mutations.js +5 -7
  79. package/stores/documents/documentStore/mutations.js.map +1 -1
  80. package/stores/documents/documentStore/rotation/index.js +2 -1
  81. package/stores/documents/documentStore/rotation/index.js.map +1 -1
  82. package/stores/documents/documentStore/rows/index.js +3 -4
  83. package/stores/documents/documentStore/rows/index.js.map +1 -1
  84. package/stores/documents/documentStore/state.js +18 -0
  85. package/stores/documents/documentStore/state.js.map +1 -1
  86. package/stores/documents/documentStore/sync/index.js +5 -2
  87. package/stores/documents/documentStore/sync/index.js.map +1 -1
  88. package/stores/documents/registry.d.ts +32 -0
  89. package/stores/documents/registry.js +79 -4
  90. package/stores/documents/registry.js.map +1 -1
  91. package/stores/documents/removedDocumentStore.js +38 -0
  92. package/stores/documents/removedDocumentStore.js.map +1 -0
  93. package/stores/documents/types.d.ts +2 -0
  94. package/stores/documents/types.js.map +1 -1
  95. package/sync/reconciliation/service.js +2 -2
  96. package/sync/reconciliation/service.js.map +1 -1
  97. package/sync/reconciliation/serviceTypes.d.ts +8 -1
  98. package/sync/reconciliation/serviceTypes.js.map +1 -1
  99. package/workflows/container-contents/pendingWrites/index.d.ts +5 -0
  100. package/workflows/container-contents/pendingWrites/index.js +7 -0
  101. package/workflows/container-contents/pendingWrites/index.js.map +1 -1
  102. package/workflows/container-contents/remoteHydration/index.js +7 -1
  103. package/workflows/container-contents/remoteHydration/index.js.map +1 -1
  104. package/workflows/container-contents/remoteHydration/remoteContainers/organizationSyncPauseFilter.js +13 -0
  105. package/workflows/container-contents/remoteHydration/remoteContainers/organizationSyncPauseFilter.js.map +1 -0
  106. package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js +13 -2
  107. package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js.map +1 -1
  108. package/workflows/container-contents/syncPause/discoveryGuard.js +31 -0
  109. package/workflows/container-contents/syncPause/discoveryGuard.js.map +1 -0
  110. package/workflows/container-contents/syncPause/documentRemoval.js +154 -0
  111. package/workflows/container-contents/syncPause/documentRemoval.js.map +1 -0
  112. package/workflows/organizations/index.d.ts +1 -0
  113. package/workflows/organizations/index.js +1 -0
  114. package/workflows/organizations/index.js.map +1 -1
  115. package/workflows/organizations/memberships/organizationMemberships.d.ts +84 -0
  116. package/workflows/organizations/memberships/organizationMemberships.js +95 -0
  117. package/workflows/organizations/memberships/organizationMemberships.js.map +1 -0
  118. package/workflows/sync/index.d.ts +1 -0
  119. package/workflows/sync/index.js +1 -0
  120. package/workflows/sync/index.js.map +1 -1
@@ -0,0 +1,30 @@
1
+ import { type OrganizationMembership, type OrganizationSyncPauseResult } from "../../workflows/organizations/index.js";
2
+ import { type DomainScope } from "../../workflows/sync/index.js";
3
+ import { type ContainerContents } from "../containerContents/index.js";
4
+ import type { InternalRuntime } from "../runtime/workflowRuntime.js";
5
+ /** The user's own conduit to the organizations they belong to. */
6
+ export interface OrganizationMemberships {
7
+ /**
8
+ * Reads the user's memberships and applies their pause choices on this
9
+ * device. Null when offline or the list could not be read.
10
+ */
11
+ refresh(): Promise<OrganizationMembership[] | null>;
12
+ /** Pauses or resumes one organization for all of the user's devices. */
13
+ setSyncPaused(organizationId: string, paused: boolean): Promise<OrganizationSyncPauseResult>;
14
+ /** The organizations paused on this device, read from local storage. */
15
+ loadPausedOrganizationIds(): Promise<ReadonlySet<string>>;
16
+ /** Notifies when the paused set for the current scope changes. */
17
+ subscribeToSyncPauses(listener: () => void): () => void;
18
+ }
19
+ /**
20
+ * The scope's document reconciler. Containers it reconciled before a pause
21
+ * stay marked as listed, so a resume has to forget that before reconciling.
22
+ */
23
+ export interface OrganizationContentReconciler {
24
+ resetDiscovered(): void;
25
+ reconcileNow(options: {
26
+ readonly freshListing: true;
27
+ }): Promise<void>;
28
+ }
29
+ export declare function retryWhenWritesSettle(domainScope: DomainScope, retry: () => Promise<boolean>): void;
30
+ export declare function createOrganizationMemberships(runtimeService: InternalRuntime, containerContents: ContainerContents, openReconciler: () => OrganizationContentReconciler): OrganizationMemberships;
@@ -0,0 +1,132 @@
1
+ import { refreshOrganizationMemberships, setOrganizationSyncPaused, } from "../../workflows/organizations/index.js";
2
+ import { getOrCreateOrganizationSyncPauses, hasDomainSyncCoordinatorPendingWork, requestAllDomainSyncLanes, subscribeToDomainSyncCoordinator, waitForDomainSyncCoordinatorToSettle, } from "../../workflows/sync/index.js";
3
+ import { carriesLocalWrites, } from "../containerContents/index.js";
4
+ // Long enough for a typical upload burst, short enough to answer a tap.
5
+ const PAUSE_FLUSH_TIMEOUT_MS = 10_000;
6
+ const settleRetriesByScope = new WeakMap();
7
+ // One retry per scope: it runs whenever the scope's sync lanes go idle and
8
+ // stops once it reports the work done.
9
+ export function retryWhenWritesSettle(domainScope, retry) {
10
+ const existing = settleRetriesByScope.get(domainScope);
11
+ if (existing) {
12
+ existing.retry = retry;
13
+ return;
14
+ }
15
+ const entry = { retry, running: false, unsubscribe: () => { } };
16
+ settleRetriesByScope.set(domainScope, entry);
17
+ const attempt = () => {
18
+ if (entry.running || hasDomainSyncCoordinatorPendingWork(domainScope)) {
19
+ return;
20
+ }
21
+ entry.running = true;
22
+ void entry
23
+ .retry()
24
+ .then((done) => {
25
+ if (!done)
26
+ return;
27
+ entry.unsubscribe();
28
+ settleRetriesByScope.delete(domainScope);
29
+ }, () => undefined)
30
+ .finally(() => {
31
+ entry.running = false;
32
+ });
33
+ };
34
+ entry.unsubscribe = subscribeToDomainSyncCoordinator(domainScope, attempt);
35
+ // The lanes may already have gone idle before the subscription existed.
36
+ attempt();
37
+ }
38
+ // Bound to the scope the operation started in: the tree is opened once, and
39
+ // every action re-checks the scope so stale work never reaches another
40
+ // identity's device state.
41
+ function createOrganizationContentHost(runtime, containerContents, openReconciler, isCurrent) {
42
+ const { domainScope } = runtime.state;
43
+ const tree = containerContents.openTree();
44
+ return {
45
+ async forgetOrganizationContent(organizationId) {
46
+ // A scope that ended has nothing left to retry.
47
+ if (!isCurrent())
48
+ return true;
49
+ return tree.forgetOrganizationContent(organizationId);
50
+ },
51
+ async hasUnsyncedOrganizationWrites(organizationId) {
52
+ if (!isCurrent())
53
+ return true;
54
+ const pending = await containerContents
55
+ .documentQueries()
56
+ .listPendingWrites();
57
+ return pending.some((item) => item.organizationId === organizationId && carriesLocalWrites(item));
58
+ },
59
+ async flushWrites() {
60
+ requestAllDomainSyncLanes(domainScope);
61
+ await waitForDomainSyncCoordinatorToSettle(domainScope, {
62
+ timeoutMs: PAUSE_FLUSH_TIMEOUT_MS,
63
+ });
64
+ },
65
+ async refetchContent() {
66
+ if (!isCurrent())
67
+ return;
68
+ // Re-lists the whole tree and every container's documents. The resumed
69
+ // organization's folders were listed before the pause, and a hydration
70
+ // may already have restored them, so they must be forgotten explicitly.
71
+ // A sweep that started listing while paused would miss them, so this
72
+ // one queues behind it rather than joining it.
73
+ const reconciler = openReconciler();
74
+ reconciler.resetDiscovered();
75
+ await reconciler.reconcileNow({ freshListing: true });
76
+ },
77
+ retryWhenWritesSettle: (retry) => retryWhenWritesSettle(domainScope, retry),
78
+ };
79
+ }
80
+ function canReachServer(runtime) {
81
+ return (runtime.infra.dbStatus === "ready" &&
82
+ runtime.auth.isAuthenticated &&
83
+ runtime.state.online);
84
+ }
85
+ export function createOrganizationMemberships(runtimeService, containerContents, openReconciler) {
86
+ const membershipInput = (runtime) => {
87
+ const { domainScope } = runtime.state;
88
+ const isCurrent = () => runtimeService.workflowInput().state.domainScope === domainScope;
89
+ return {
90
+ apiClient: runtime.apiClient,
91
+ domainScope,
92
+ execSql: runtime.infra.execSql,
93
+ host: createOrganizationContentHost(runtime, containerContents, openReconciler, isCurrent),
94
+ isCurrent,
95
+ };
96
+ };
97
+ return {
98
+ async refresh() {
99
+ const runtime = runtimeService.workflowInput();
100
+ if (!canReachServer(runtime))
101
+ return null;
102
+ return refreshOrganizationMemberships(membershipInput(runtime));
103
+ },
104
+ async setSyncPaused(organizationId, paused) {
105
+ const runtime = runtimeService.workflowInput();
106
+ if (!canReachServer(runtime)) {
107
+ return {
108
+ httpStatus: null,
109
+ message: "Connect to change sync for an organization.",
110
+ status: "failed",
111
+ };
112
+ }
113
+ return setOrganizationSyncPaused({
114
+ ...membershipInput(runtime),
115
+ organizationId,
116
+ paused,
117
+ });
118
+ },
119
+ async loadPausedOrganizationIds() {
120
+ const runtime = runtimeService.workflowInput();
121
+ if (runtime.infra.dbStatus !== "ready")
122
+ return new Set();
123
+ const pauses = getOrCreateOrganizationSyncPauses(runtime.state.domainScope);
124
+ await pauses.load(runtime.infra.execSql);
125
+ return pauses.pausedOrganizationIds();
126
+ },
127
+ subscribeToSyncPauses(listener) {
128
+ return getOrCreateOrganizationSyncPauses(runtimeService.workflowInput().state.domainScope).subscribe(listener);
129
+ },
130
+ };
131
+ }
132
+ //# 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 {\n type OrganizationContentHost,\n type OrganizationMembership,\n type OrganizationSyncPauseResult,\n refreshOrganizationMemberships,\n setOrganizationSyncPaused,\n} from \"../../workflows/organizations\";\nimport {\n type DomainScope,\n getOrCreateOrganizationSyncPauses,\n hasDomainSyncCoordinatorPendingWork,\n requestAllDomainSyncLanes,\n subscribeToDomainSyncCoordinator,\n waitForDomainSyncCoordinatorToSettle,\n} from \"../../workflows/sync\";\nimport {\n type ContainerContents,\n carriesLocalWrites,\n} from \"../containerContents\";\nimport type {\n InternalRuntime,\n InternalWorkflowRuntimeInput,\n} from \"../runtime/workflowRuntime\";\n\n// Long enough for a typical upload burst, short enough to answer a tap.\nconst PAUSE_FLUSH_TIMEOUT_MS = 10_000;\n\n/** The user's own conduit to the organizations they belong to. */\nexport interface OrganizationMemberships {\n /**\n * Reads the user's memberships and applies their pause choices on this\n * device. Null when offline or the list could not be read.\n */\n refresh(): Promise<OrganizationMembership[] | null>;\n /** Pauses or resumes one organization for all of the user's devices. */\n setSyncPaused(\n organizationId: string,\n paused: boolean,\n ): Promise<OrganizationSyncPauseResult>;\n /** The organizations paused on this device, read from local storage. */\n loadPausedOrganizationIds(): Promise<ReadonlySet<string>>;\n /** Notifies when the paused set for the current scope changes. */\n subscribeToSyncPauses(listener: () => void): () => void;\n}\n\n/**\n * The scope's document reconciler. Containers it reconciled before a pause\n * stay marked as listed, so a resume has to forget that before reconciling.\n */\nexport interface OrganizationContentReconciler {\n resetDiscovered(): void;\n reconcileNow(options: { readonly freshListing: true }): Promise<void>;\n}\n\ninterface SettleRetry {\n retry: () => Promise<boolean>;\n running: boolean;\n unsubscribe: () => void;\n}\n\nconst settleRetriesByScope = new WeakMap<DomainScope, SettleRetry>();\n\n// One retry per scope: it runs whenever the scope's sync lanes go idle and\n// stops once it reports the work done.\nexport function retryWhenWritesSettle(\n domainScope: DomainScope,\n retry: () => Promise<boolean>,\n): void {\n const existing = settleRetriesByScope.get(domainScope);\n if (existing) {\n existing.retry = retry;\n return;\n }\n const entry: SettleRetry = { retry, running: false, unsubscribe: () => {} };\n settleRetriesByScope.set(domainScope, entry);\n const attempt = () => {\n if (entry.running || hasDomainSyncCoordinatorPendingWork(domainScope)) {\n return;\n }\n entry.running = true;\n void entry\n .retry()\n .then(\n (done) => {\n if (!done) return;\n entry.unsubscribe();\n settleRetriesByScope.delete(domainScope);\n },\n () => undefined,\n )\n .finally(() => {\n entry.running = false;\n });\n };\n entry.unsubscribe = subscribeToDomainSyncCoordinator(domainScope, attempt);\n // The lanes may already have gone idle before the subscription existed.\n attempt();\n}\n\n// Bound to the scope the operation started in: the tree is opened once, and\n// every action re-checks the scope so stale work never reaches another\n// identity's device state.\nfunction createOrganizationContentHost(\n runtime: InternalWorkflowRuntimeInput,\n containerContents: ContainerContents,\n openReconciler: () => OrganizationContentReconciler,\n isCurrent: () => boolean,\n): OrganizationContentHost {\n const { domainScope } = runtime.state;\n const tree = containerContents.openTree();\n return {\n async forgetOrganizationContent(organizationId) {\n // A scope that ended has nothing left to retry.\n if (!isCurrent()) return true;\n return tree.forgetOrganizationContent(organizationId);\n },\n async hasUnsyncedOrganizationWrites(organizationId) {\n if (!isCurrent()) return true;\n const pending = await containerContents\n .documentQueries()\n .listPendingWrites();\n return pending.some(\n (item) =>\n item.organizationId === organizationId && carriesLocalWrites(item),\n );\n },\n async flushWrites() {\n requestAllDomainSyncLanes(domainScope);\n await waitForDomainSyncCoordinatorToSettle(domainScope, {\n timeoutMs: PAUSE_FLUSH_TIMEOUT_MS,\n });\n },\n async refetchContent() {\n if (!isCurrent()) return;\n // Re-lists the whole tree and every container's documents. The resumed\n // organization's folders were listed before the pause, and a hydration\n // may already have restored them, so they must be forgotten explicitly.\n // A sweep that started listing while paused would miss them, so this\n // one queues behind it rather than joining it.\n const reconciler = openReconciler();\n reconciler.resetDiscovered();\n await reconciler.reconcileNow({ freshListing: true });\n },\n retryWhenWritesSettle: (retry) => retryWhenWritesSettle(domainScope, retry),\n };\n}\n\nfunction canReachServer(runtime: InternalWorkflowRuntimeInput): boolean {\n return (\n runtime.infra.dbStatus === \"ready\" &&\n runtime.auth.isAuthenticated &&\n runtime.state.online\n );\n}\n\nexport function createOrganizationMemberships(\n runtimeService: InternalRuntime,\n containerContents: ContainerContents,\n openReconciler: () => OrganizationContentReconciler,\n): OrganizationMemberships {\n const membershipInput = (runtime: InternalWorkflowRuntimeInput) => {\n const { domainScope } = runtime.state;\n const isCurrent = () =>\n runtimeService.workflowInput().state.domainScope === domainScope;\n return {\n apiClient: runtime.apiClient,\n domainScope,\n execSql: runtime.infra.execSql,\n host: createOrganizationContentHost(\n runtime,\n containerContents,\n openReconciler,\n isCurrent,\n ),\n isCurrent,\n };\n };\n return {\n async refresh() {\n const runtime = runtimeService.workflowInput();\n if (!canReachServer(runtime)) return null;\n return refreshOrganizationMemberships(membershipInput(runtime));\n },\n async setSyncPaused(organizationId, paused) {\n const runtime = runtimeService.workflowInput();\n if (!canReachServer(runtime)) {\n return {\n httpStatus: null,\n message: \"Connect to change sync for an organization.\",\n status: \"failed\",\n };\n }\n return setOrganizationSyncPaused({\n ...membershipInput(runtime),\n organizationId,\n paused,\n });\n },\n async loadPausedOrganizationIds() {\n const runtime = runtimeService.workflowInput();\n if (runtime.infra.dbStatus !== \"ready\") return new Set<string>();\n const pauses = getOrCreateOrganizationSyncPauses(\n runtime.state.domainScope,\n );\n await pauses.load(runtime.infra.execSql);\n return pauses.pausedOrganizationIds();\n },\n subscribeToSyncPauses(listener) {\n return getOrCreateOrganizationSyncPauses(\n runtimeService.workflowInput().state.domainScope,\n ).subscribe(listener);\n },\n };\n}\n"]}
@@ -4,6 +4,7 @@ import type { cancelStripeSubscription, checkNativePurchaseEligibility, claimNat
4
4
  import type { InternalWorkflowRuntimeInput } from "../runtime/workflowRuntime.js";
5
5
  import type { OrganizationDataUsageCoordinator } from "./organizationDataUsage.js";
6
6
  import type { loadOrganizationGroupPresentationDetails } from "./organizationGroupPresentation.js";
7
+ import type { OrganizationMemberships } from "./organizationMemberships.js";
7
8
  import type { OrganizationReadModelCoordinator } from "./organizationReadModels.js";
8
9
  import type { AbandonOrganizationPolicyMutationInput, DiscardUnreadableOrganizationPolicyMutationInput, readPendingOrganizationPolicyMutation } from "./principalMutationRecovery.js";
9
10
  import type { AddOrganizationGroupUserInput, deleteGroupForOrganization, OrganizationGrantRef, RemoveOrganizationGroupUserInput } from "./principalMutations.js";
@@ -40,6 +41,8 @@ export interface Organizations {
40
41
  loadGroupContainers: (groupId: string) => ReturnType<OrganizationReadModelCoordinator["loadLocalGroupContainers"]>;
41
42
  loadGrants: () => ReturnType<OrganizationReadModelCoordinator["loadLocalGrants"]>;
42
43
  listLocalOrganizations: () => Promise<LocalOrganizationSummary[]>;
44
+ /** The user's own memberships and per-organization sync choices. */
45
+ readonly memberships: OrganizationMemberships;
43
46
  loadPolicyHistory: (beforeVersion?: number) => ReturnType<OrganizationReadModelCoordinator["loadOrganizationPolicyHistory"]>;
44
47
  loadUserDetail: (userId: string) => ReturnType<OrganizationReadModelCoordinator["loadLocalUserDetail"]>;
45
48
  updateRosterEntry: (userId: string, profileDocumentId: string | null) => ReturnType<typeof updateOrganizationRosterEntry>;
@@ -1 +1 @@
1
- {"version":3,"file":"organizationsTypes.js","sourceRoot":"","sources":["organizationsTypes.ts"],"names":[],"mappings":"","sourcesContent":["import type { NativeSubscriptionStore } from \"@tearleads/validators/billing\";\nimport type { PrincipalPolicyMutationResponse } from \"@tearleads/validators/response\";\nimport type {\n cancelStripeSubscription,\n checkNativePurchaseEligibility,\n claimNativeOrganizationSubscription,\n createOrganizationGroup,\n createStripeCheckout,\n createStripeCheckoutSession,\n importOrganizationUser,\n LocalOrganizationSummary,\n loadOrganizationBilling,\n loadOrganizationBillingHistory,\n loadOrganizationBillingManagementUrl,\n loadStripeCheckoutOptions,\n revokeOrganizationContainerGrant,\n startOrganizationTrial,\n updateOrganizationProfile,\n updateOrganizationRosterEntry,\n} from \"../../workflows/organizations\";\nimport type { InternalWorkflowRuntimeInput } from \"../runtime/workflowRuntime\";\nimport type { OrganizationDataUsageCoordinator } from \"./organizationDataUsage\";\nimport type { loadOrganizationGroupPresentationDetails } from \"./organizationGroupPresentation\";\nimport type { OrganizationReadModelCoordinator } from \"./organizationReadModels\";\nimport type {\n AbandonOrganizationPolicyMutationInput,\n DiscardUnreadableOrganizationPolicyMutationInput,\n readPendingOrganizationPolicyMutation,\n} from \"./principalMutationRecovery\";\nimport type {\n AddOrganizationGroupUserInput,\n deleteGroupForOrganization,\n OrganizationGrantRef,\n RemoveOrganizationGroupUserInput,\n} from \"./principalMutations\";\n\n/** Exact current artifacts and container receipts; no historical prefix. */\nexport type OrganizationGroupMutationReceipt = PrincipalPolicyMutationResponse;\n\nexport interface Organizations {\n discardUnreadablePolicyMutation: (\n input: DiscardUnreadableOrganizationPolicyMutationInput,\n ) => Promise<boolean>;\n readPendingPolicyMutation: (\n organizationId: string,\n ) => ReturnType<typeof readPendingOrganizationPolicyMutation>;\n retryPendingPolicyMutation: (organizationId: string) => Promise<void>;\n abandonPendingPolicyMutation: (\n input: AbandonOrganizationPolicyMutationInput,\n ) => Promise<boolean>;\n addUserToGroup: (\n input: AddOrganizationGroupUserInput,\n ) => Promise<OrganizationGroupMutationReceipt>;\n createGroup: (name: string) => ReturnType<typeof createOrganizationGroup>;\n deleteGroup: (\n groupId: string,\n ) => ReturnType<typeof deleteGroupForOrganization>;\n importUserById: (userId: string) => ReturnType<typeof importOrganizationUser>;\n loadBilling: () => ReturnType<typeof loadOrganizationBilling>;\n /** Billing for an organization the session is not currently switched to. */\n loadBillingForOrganization: (\n organizationId: string,\n ) => ReturnType<typeof loadOrganizationBilling>;\n loadBillingHistory: () => ReturnType<typeof loadOrganizationBillingHistory>;\n loadBillingManagementUrl: () => ReturnType<\n typeof loadOrganizationBillingManagementUrl\n >;\n /** Direct Stripe checkout (issue #1654): options, start, and cancel. */\n loadStripeCheckoutOptions: (\n organizationId?: string,\n ) => ReturnType<typeof loadStripeCheckoutOptions>;\n createStripeCheckout: (\n organizationId?: string,\n ) => ReturnType<typeof createStripeCheckout>;\n createStripeCheckoutSession: (\n returnUrl: string,\n organizationId?: string,\n ) => ReturnType<typeof createStripeCheckoutSession>;\n cancelStripeSubscription: () => ReturnType<typeof cancelStripeSubscription>;\n claimNativeSubscription: (\n organizationId: string,\n store: NativeSubscriptionStore,\n ) => ReturnType<typeof claimNativeOrganizationSubscription>;\n checkNativePurchaseEligibility: (\n organizationId: string,\n store: NativeSubscriptionStore,\n ) => ReturnType<typeof checkNativePurchaseEligibility>;\n loadDataUsage: () => ReturnType<\n OrganizationDataUsageCoordinator[\"reconcile\"]\n >;\n loadLocalDataUsage: () => ReturnType<\n OrganizationDataUsageCoordinator[\"loadLocal\"]\n >;\n loadDirectoryAndGroups: () => ReturnType<\n OrganizationReadModelCoordinator[\"reconcile\"]\n >;\n loadDirectoryAndGroupsAfterMutation: () => ReturnType<\n OrganizationReadModelCoordinator[\"reconcileAfterMutation\"]\n >;\n loadLocalDirectoryAndGroups: () => ReturnType<\n OrganizationReadModelCoordinator[\"loadLocal\"]\n >;\n loadGroupMembers: (\n groupId: string,\n ) => ReturnType<\n InternalWorkflowRuntimeInput[\"apiClient\"][\"listOrganizationGroupMembers\"]\n >;\n loadGroupPresentationDetails(\n groupId: string,\n beforeVersion?: number,\n ): ReturnType<typeof loadOrganizationGroupPresentationDetails>;\n loadGroupContainers: (\n groupId: string,\n ) => ReturnType<OrganizationReadModelCoordinator[\"loadLocalGroupContainers\"]>;\n loadGrants: () => ReturnType<\n OrganizationReadModelCoordinator[\"loadLocalGrants\"]\n >;\n listLocalOrganizations: () => Promise<LocalOrganizationSummary[]>;\n loadPolicyHistory: (\n beforeVersion?: number,\n ) => ReturnType<\n OrganizationReadModelCoordinator[\"loadOrganizationPolicyHistory\"]\n >;\n loadUserDetail: (\n userId: string,\n ) => ReturnType<OrganizationReadModelCoordinator[\"loadLocalUserDetail\"]>;\n updateRosterEntry: (\n userId: string,\n profileDocumentId: string | null,\n ) => ReturnType<typeof updateOrganizationRosterEntry>;\n updateProfile: (\n profileDocumentId: string | null,\n ) => ReturnType<typeof updateOrganizationProfile>;\n removeUserFromGroup: (\n input: RemoveOrganizationGroupUserInput,\n ) => Promise<OrganizationGroupMutationReceipt>;\n revokeGrant: (\n grant: OrganizationGrantRef,\n ) => Promise<\n | Awaited<ReturnType<typeof revokeOrganizationContainerGrant>>\n | OrganizationGroupMutationReceipt\n >;\n startTrial: (\n organizationId?: string,\n ) => ReturnType<typeof startOrganizationTrial>;\n}\n"]}
1
+ {"version":3,"file":"organizationsTypes.js","sourceRoot":"","sources":["organizationsTypes.ts"],"names":[],"mappings":"","sourcesContent":["import type { NativeSubscriptionStore } from \"@tearleads/validators/billing\";\nimport type { PrincipalPolicyMutationResponse } from \"@tearleads/validators/response\";\nimport type {\n cancelStripeSubscription,\n checkNativePurchaseEligibility,\n claimNativeOrganizationSubscription,\n createOrganizationGroup,\n createStripeCheckout,\n createStripeCheckoutSession,\n importOrganizationUser,\n LocalOrganizationSummary,\n loadOrganizationBilling,\n loadOrganizationBillingHistory,\n loadOrganizationBillingManagementUrl,\n loadStripeCheckoutOptions,\n revokeOrganizationContainerGrant,\n startOrganizationTrial,\n updateOrganizationProfile,\n updateOrganizationRosterEntry,\n} from \"../../workflows/organizations\";\nimport type { InternalWorkflowRuntimeInput } from \"../runtime/workflowRuntime\";\nimport type { OrganizationDataUsageCoordinator } from \"./organizationDataUsage\";\nimport type { loadOrganizationGroupPresentationDetails } from \"./organizationGroupPresentation\";\nimport type { OrganizationMemberships } from \"./organizationMemberships\";\nimport type { OrganizationReadModelCoordinator } from \"./organizationReadModels\";\nimport type {\n AbandonOrganizationPolicyMutationInput,\n DiscardUnreadableOrganizationPolicyMutationInput,\n readPendingOrganizationPolicyMutation,\n} from \"./principalMutationRecovery\";\nimport type {\n AddOrganizationGroupUserInput,\n deleteGroupForOrganization,\n OrganizationGrantRef,\n RemoveOrganizationGroupUserInput,\n} from \"./principalMutations\";\n\n/** Exact current artifacts and container receipts; no historical prefix. */\nexport type OrganizationGroupMutationReceipt = PrincipalPolicyMutationResponse;\n\nexport interface Organizations {\n discardUnreadablePolicyMutation: (\n input: DiscardUnreadableOrganizationPolicyMutationInput,\n ) => Promise<boolean>;\n readPendingPolicyMutation: (\n organizationId: string,\n ) => ReturnType<typeof readPendingOrganizationPolicyMutation>;\n retryPendingPolicyMutation: (organizationId: string) => Promise<void>;\n abandonPendingPolicyMutation: (\n input: AbandonOrganizationPolicyMutationInput,\n ) => Promise<boolean>;\n addUserToGroup: (\n input: AddOrganizationGroupUserInput,\n ) => Promise<OrganizationGroupMutationReceipt>;\n createGroup: (name: string) => ReturnType<typeof createOrganizationGroup>;\n deleteGroup: (\n groupId: string,\n ) => ReturnType<typeof deleteGroupForOrganization>;\n importUserById: (userId: string) => ReturnType<typeof importOrganizationUser>;\n loadBilling: () => ReturnType<typeof loadOrganizationBilling>;\n /** Billing for an organization the session is not currently switched to. */\n loadBillingForOrganization: (\n organizationId: string,\n ) => ReturnType<typeof loadOrganizationBilling>;\n loadBillingHistory: () => ReturnType<typeof loadOrganizationBillingHistory>;\n loadBillingManagementUrl: () => ReturnType<\n typeof loadOrganizationBillingManagementUrl\n >;\n /** Direct Stripe checkout (issue #1654): options, start, and cancel. */\n loadStripeCheckoutOptions: (\n organizationId?: string,\n ) => ReturnType<typeof loadStripeCheckoutOptions>;\n createStripeCheckout: (\n organizationId?: string,\n ) => ReturnType<typeof createStripeCheckout>;\n createStripeCheckoutSession: (\n returnUrl: string,\n organizationId?: string,\n ) => ReturnType<typeof createStripeCheckoutSession>;\n cancelStripeSubscription: () => ReturnType<typeof cancelStripeSubscription>;\n claimNativeSubscription: (\n organizationId: string,\n store: NativeSubscriptionStore,\n ) => ReturnType<typeof claimNativeOrganizationSubscription>;\n checkNativePurchaseEligibility: (\n organizationId: string,\n store: NativeSubscriptionStore,\n ) => ReturnType<typeof checkNativePurchaseEligibility>;\n loadDataUsage: () => ReturnType<\n OrganizationDataUsageCoordinator[\"reconcile\"]\n >;\n loadLocalDataUsage: () => ReturnType<\n OrganizationDataUsageCoordinator[\"loadLocal\"]\n >;\n loadDirectoryAndGroups: () => ReturnType<\n OrganizationReadModelCoordinator[\"reconcile\"]\n >;\n loadDirectoryAndGroupsAfterMutation: () => ReturnType<\n OrganizationReadModelCoordinator[\"reconcileAfterMutation\"]\n >;\n loadLocalDirectoryAndGroups: () => ReturnType<\n OrganizationReadModelCoordinator[\"loadLocal\"]\n >;\n loadGroupMembers: (\n groupId: string,\n ) => ReturnType<\n InternalWorkflowRuntimeInput[\"apiClient\"][\"listOrganizationGroupMembers\"]\n >;\n loadGroupPresentationDetails(\n groupId: string,\n beforeVersion?: number,\n ): ReturnType<typeof loadOrganizationGroupPresentationDetails>;\n loadGroupContainers: (\n groupId: string,\n ) => ReturnType<OrganizationReadModelCoordinator[\"loadLocalGroupContainers\"]>;\n loadGrants: () => ReturnType<\n OrganizationReadModelCoordinator[\"loadLocalGrants\"]\n >;\n listLocalOrganizations: () => Promise<LocalOrganizationSummary[]>;\n /** The user's own memberships and per-organization sync choices. */\n readonly memberships: OrganizationMemberships;\n loadPolicyHistory: (\n beforeVersion?: number,\n ) => ReturnType<\n OrganizationReadModelCoordinator[\"loadOrganizationPolicyHistory\"]\n >;\n loadUserDetail: (\n userId: string,\n ) => ReturnType<OrganizationReadModelCoordinator[\"loadLocalUserDetail\"]>;\n updateRosterEntry: (\n userId: string,\n profileDocumentId: string | null,\n ) => ReturnType<typeof updateOrganizationRosterEntry>;\n updateProfile: (\n profileDocumentId: string | null,\n ) => ReturnType<typeof updateOrganizationProfile>;\n removeUserFromGroup: (\n input: RemoveOrganizationGroupUserInput,\n ) => Promise<OrganizationGroupMutationReceipt>;\n revokeGrant: (\n grant: OrganizationGrantRef,\n ) => Promise<\n | Awaited<ReturnType<typeof revokeOrganizationContainerGrant>>\n | OrganizationGroupMutationReceipt\n >;\n startTrial: (\n organizationId?: string,\n ) => ReturnType<typeof startOrganizationTrial>;\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { and, asc, eq, inArray } from "drizzle-orm";
2
2
  import { containers, documentContainerProjection, documentProjection, inContainerIntentLanes, inLocalIntentLane, LOCAL_INTENT_LANES, localIntents, } from "../../sqlite/schema/index.js";
3
+ import { sqliteRowsPerStatement } from "../../sqlite/sqliteBindBudget.js";
3
4
  import { deleteContainerDocumentTombstoneHoldsForContainers } from "../documents/containerDocumentTombstoneHoldsPersistence.js";
4
5
  import { setDocumentProjectionContainerInTransaction } from "../documents/documentProjectionContainers.js";
5
6
  import { getLatestTimestamp } from "../latestTimestamp.js";
@@ -26,25 +27,29 @@ async function loadOrganizationIdsForContainers(tx, containerIds) {
26
27
  }
27
28
  return organizationIdByContainerId;
28
29
  }
30
+ // One removed container can hold any number of documents, so their ids are
31
+ // read back in batches that fit SQLite's bind budget.
32
+ const DOCUMENT_ID_BATCH_SIZE = sqliteRowsPerStatement(1);
29
33
  // A document unlinked from a removed container may keep other live links; its
30
34
  // projection then re-homes to the first remaining link (deterministic order)
31
35
  // instead of dropping to null.
32
36
  async function loadFirstRemainingContainerIdByDocumentId(tx, documentIds) {
33
37
  const firstRemainingContainerIdByDocumentId = new Map();
34
- if (documentIds.length === 0) {
35
- return firstRemainingContainerIdByDocumentId;
36
- }
37
- const remainingLinks = await tx
38
- .select({
39
- containerId: documentContainerProjection.containerId,
40
- documentId: documentContainerProjection.documentId,
41
- })
42
- .from(documentContainerProjection)
43
- .where(inArray(documentContainerProjection.documentId, documentIds))
44
- .orderBy(asc(documentContainerProjection.documentId), asc(documentContainerProjection.containerId));
45
- for (const link of remainingLinks) {
46
- if (!firstRemainingContainerIdByDocumentId.has(link.documentId)) {
47
- firstRemainingContainerIdByDocumentId.set(link.documentId, link.containerId);
38
+ for (let index = 0; index < documentIds.length; index += DOCUMENT_ID_BATCH_SIZE) {
39
+ // A document's links all fall in its own batch, so the first per
40
+ // document is still the lowest container id.
41
+ const remainingLinks = await tx
42
+ .select({
43
+ containerId: documentContainerProjection.containerId,
44
+ documentId: documentContainerProjection.documentId,
45
+ })
46
+ .from(documentContainerProjection)
47
+ .where(inArray(documentContainerProjection.documentId, documentIds.slice(index, index + DOCUMENT_ID_BATCH_SIZE)))
48
+ .orderBy(asc(documentContainerProjection.documentId), asc(documentContainerProjection.containerId));
49
+ for (const link of remainingLinks) {
50
+ if (!firstRemainingContainerIdByDocumentId.has(link.documentId)) {
51
+ firstRemainingContainerIdByDocumentId.set(link.documentId, link.containerId);
52
+ }
48
53
  }
49
54
  }
50
55
  return firstRemainingContainerIdByDocumentId;
@@ -1 +1 @@
1
- {"version":3,"file":"containerStructuralRepair.js","sourceRoot":"","sources":["containerStructuralRepair.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray } from \"drizzle-orm\";\nimport {\n containers,\n documentContainerProjection,\n documentProjection,\n inContainerIntentLanes,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { deleteContainerDocumentTombstoneHoldsForContainers } from \"../documents/containerDocumentTombstoneHoldsPersistence\";\nimport { setDocumentProjectionContainerInTransaction } from \"../documents/documentProjectionContainers\";\nimport { getLatestTimestamp } from \"../latestTimestamp\";\nimport type {\n ContainerRemoval,\n LocalRootDescendantReparentInput,\n} from \"./containerContentsPersistenceTypes\";\nimport { saveContainerCreateIntent } from \"./containerIntentPersistence\";\nimport { deleteContainerWatermarksInTransaction } from \"./containerSyncWatermarkPersistence\";\nimport { deleteContainerMetadataDocumentRowsInTransaction } from \"./dormantContainerMetadata\";\n\n// The containers being removed still exist when repair runs (deletion follows\n// it). Capture their org attribution so a detached document's pending writes\n// keep a resolvable organization id after the join source is gone — e.g. when\n// access to a shared org is revoked mid-sync.\nasync function loadOrganizationIdsForContainers(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<Map<string, string>> {\n const rows = await tx\n .select({\n id: containers.id,\n organizationId: containers.organizationId,\n })\n .from(containers)\n .where(inArray(containers.id, containerIds));\n const organizationIdByContainerId = new Map<string, string>();\n for (const container of rows) {\n if (container.id && container.organizationId) {\n organizationIdByContainerId.set(container.id, container.organizationId);\n }\n }\n return organizationIdByContainerId;\n}\n\n// A document unlinked from a removed container may keep other live links; its\n// projection then re-homes to the first remaining link (deterministic order)\n// instead of dropping to null.\nasync function loadFirstRemainingContainerIdByDocumentId(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<Map<string, string>> {\n const firstRemainingContainerIdByDocumentId = new Map<string, string>();\n if (documentIds.length === 0) {\n return firstRemainingContainerIdByDocumentId;\n }\n const remainingLinks = await tx\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, documentIds))\n .orderBy(\n asc(documentContainerProjection.documentId),\n asc(documentContainerProjection.containerId),\n );\n for (const link of remainingLinks) {\n if (!firstRemainingContainerIdByDocumentId.has(link.documentId)) {\n firstRemainingContainerIdByDocumentId.set(\n link.documentId,\n link.containerId,\n );\n }\n }\n return firstRemainingContainerIdByDocumentId;\n}\n\nexport async function repairDocumentsForRemovedContainersInTransaction(input: {\n removals: ReadonlyArray<ContainerRemoval>;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { tx } = input;\n const updatedAtByContainerId = new Map(\n input.removals.map((removal) => [removal.containerId, removal.updatedAt]),\n );\n const containerIds = Array.from(updatedAtByContainerId.keys());\n if (containerIds.length === 0) {\n return;\n }\n\n {\n const selectedRows = await tx\n .select({\n containerId: documentProjection.containerId,\n documentId: documentProjection.documentId,\n localId: documentProjection.localId,\n updatedAt: documentProjection.updatedAt,\n })\n .from(documentProjection)\n .where(inArray(documentProjection.containerId, containerIds));\n\n const organizationIdByContainerId = await loadOrganizationIdsForContainers(\n tx,\n containerIds,\n );\n\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.containerId, containerIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForContainers(tx, containerIds);\n\n const documentIds = Array.from(\n new Set(\n selectedRows.flatMap((row) => (row.documentId ? [row.documentId] : [])),\n ),\n );\n const firstRemainingContainerIdByDocumentId =\n await loadFirstRemainingContainerIdByDocumentId(tx, documentIds);\n\n for (const row of selectedRows) {\n if (!row.localId) {\n continue;\n }\n\n const removedOrganizationId = row.containerId\n ? organizationIdByContainerId.get(row.containerId)\n : undefined;\n await setDocumentProjectionContainerInTransaction(tx, row.localId, {\n containerId: row.documentId\n ? (firstRemainingContainerIdByDocumentId.get(row.documentId) ?? null)\n : null,\n organizationId: removedOrganizationId,\n updatedAt: getLatestTimestamp(\n row.updatedAt,\n row.containerId\n ? (updatedAtByContainerId.get(row.containerId) ?? row.updatedAt)\n : row.updatedAt,\n ),\n });\n }\n }\n}\n\nexport async function updateReparentedDescendantContainers(input: {\n descendantReparents: ReadonlyArray<LocalRootDescendantReparentInput>;\n remoteOrganizationId: string;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<void> {\n const { descendantReparents, remoteOrganizationId, tx, updatedAt } = input;\n const descendantIds = Array.from(\n new Set(descendantReparents.map((reparent) => reparent.containerId)),\n );\n if (descendantIds.length === 0) {\n return;\n }\n\n await tx\n .update(containers)\n .set({\n organizationId: remoteOrganizationId,\n localUpdatedAt: updatedAt,\n })\n .where(inArray(containers.id, descendantIds))\n .run();\n\n for (const reparent of descendantReparents) {\n await tx\n .update(containers)\n .set({ parentId: reparent.parentContainerId })\n .where(eq(containers.id, reparent.containerId))\n .run();\n\n if (reparent.updateCreateIntent && reparent.parentContainerId) {\n await saveContainerCreateIntent({\n containerId: reparent.containerId,\n createIntent: { parentContainerId: reparent.parentContainerId },\n tx,\n updatedAt,\n });\n }\n\n if (reparent.parentContainerId) {\n await tx\n .update(localIntents)\n .set({\n id: crypto.randomUUID(),\n targetContainerId: reparent.parentContainerId,\n syncStatus: \"pending\",\n updatedAt,\n })\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.containerMove),\n eq(localIntents.subjectId, reparent.containerId),\n ),\n )\n .run();\n }\n }\n}\n\nexport async function reparentLocalContainerChildren(input: {\n fromContainerId: string;\n remoteOrganizationId: string;\n toContainerId: string;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<void> {\n const {\n fromContainerId,\n remoteOrganizationId,\n toContainerId,\n tx,\n updatedAt,\n } = input;\n await tx\n .update(containers)\n .set({\n organizationId: remoteOrganizationId,\n parentId: toContainerId,\n localUpdatedAt: updatedAt,\n })\n .where(eq(containers.parentId, fromContainerId))\n .run();\n const createLane = inLocalIntentLane(LOCAL_INTENT_LANES.containerCreate);\n const createIntentRows = await tx\n .select({ containerId: localIntents.subjectId })\n .from(localIntents)\n .where(and(createLane, eq(localIntents.targetContainerId, fromContainerId)))\n .all();\n for (const intent of createIntentRows) {\n await tx\n .update(localIntents)\n .set({\n id: crypto.randomUUID(),\n targetContainerId: toContainerId,\n updatedAt,\n })\n .where(and(createLane, eq(localIntents.subjectId, intent.containerId)))\n .run();\n }\n const moveLane = inLocalIntentLane(LOCAL_INTENT_LANES.containerMove);\n const moveIntentRows = await tx\n .select({ containerId: localIntents.subjectId })\n .from(localIntents)\n .where(and(moveLane, eq(localIntents.targetContainerId, fromContainerId)))\n .all();\n for (const intent of moveIntentRows) {\n await tx\n .update(localIntents)\n .set({\n id: crypto.randomUUID(),\n targetContainerId: toContainerId,\n syncStatus: \"pending\",\n updatedAt,\n })\n .where(and(moveLane, eq(localIntents.subjectId, intent.containerId)))\n .run();\n }\n}\n\nexport async function deleteLocalContainerRows(input: {\n containerId: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { containerId, tx } = input;\n await tx\n .delete(localIntents)\n .where(\n and(inContainerIntentLanes(), eq(localIntents.subjectId, containerId)),\n )\n .run();\n await tx.delete(containers).where(eq(containers.id, containerId)).run();\n await deleteContainerMetadataDocumentRowsInTransaction(tx, [containerId]);\n await deleteContainerWatermarksInTransaction(tx, [containerId]);\n}\n"]}
1
+ {"version":3,"file":"containerStructuralRepair.js","sourceRoot":"","sources":["containerStructuralRepair.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,UAAU,EACV,2BAA2B,EAC3B,kBAAkB,EAClB,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,GACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEvE,OAAO,EAAE,kDAAkD,EAAE,MAAM,yDAAyD,CAAC;AAC7H,OAAO,EAAE,2CAA2C,EAAE,MAAM,2CAA2C,CAAC;AACxG,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAKxD,OAAO,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AACzE,OAAO,EAAE,sCAAsC,EAAE,MAAM,qCAAqC,CAAC;AAC7F,OAAO,EAAE,gDAAgD,EAAE,MAAM,4BAA4B,CAAC;AAE9F,8EAA8E;AAC9E,6EAA6E;AAC7E,8EAA8E;AAC9E,8CAA8C;AAC9C,KAAK,UAAU,gCAAgC,CAC7C,EAAgC,EAChC,YAAmC;IAEnC,MAAM,IAAI,GAAG,MAAM,EAAE;SAClB,MAAM,CAAC;QACN,EAAE,EAAE,UAAU,CAAC,EAAE;QACjB,cAAc,EAAE,UAAU,CAAC,cAAc;KAC1C,CAAC;SACD,IAAI,CAAC,UAAU,CAAC;SAChB,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC;IAC/C,MAAM,2BAA2B,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9D,KAAK,MAAM,SAAS,IAAI,IAAI,EAAE,CAAC;QAC7B,IAAI,SAAS,CAAC,EAAE,IAAI,SAAS,CAAC,cAAc,EAAE,CAAC;YAC7C,2BAA2B,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IACD,OAAO,2BAA2B,CAAC;AACrC,CAAC;AAED,2EAA2E;AAC3E,sDAAsD;AACtD,MAAM,sBAAsB,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAAC;AAEzD,8EAA8E;AAC9E,6EAA6E;AAC7E,+BAA+B;AAC/B,KAAK,UAAU,yCAAyC,CACtD,EAAgC,EAChC,WAAkC;IAElC,MAAM,qCAAqC,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxE,KACE,IAAI,KAAK,GAAG,CAAC,EACb,KAAK,GAAG,WAAW,CAAC,MAAM,EAC1B,KAAK,IAAI,sBAAsB,EAC/B,CAAC;QACD,iEAAiE;QACjE,6CAA6C;QAC7C,MAAM,cAAc,GAAG,MAAM,EAAE;aAC5B,MAAM,CAAC;YACN,WAAW,EAAE,2BAA2B,CAAC,WAAW;YACpD,UAAU,EAAE,2BAA2B,CAAC,UAAU;SACnD,CAAC;aACD,IAAI,CAAC,2BAA2B,CAAC;aACjC,KAAK,CACJ,OAAO,CACL,2BAA2B,CAAC,UAAU,EACtC,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,sBAAsB,CAAC,CACzD,CACF;aACA,OAAO,CACN,GAAG,CAAC,2BAA2B,CAAC,UAAU,CAAC,EAC3C,GAAG,CAAC,2BAA2B,CAAC,WAAW,CAAC,CAC7C,CAAC;QACJ,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;YAClC,IAAI,CAAC,qCAAqC,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChE,qCAAqC,CAAC,GAAG,CACvC,IAAI,CAAC,UAAU,EACf,IAAI,CAAC,WAAW,CACjB,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,qCAAqC,CAAC;AAC/C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gDAAgD,CAAC,KAGtE;IACC,MAAM,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC;IACrB,MAAM,sBAAsB,GAAG,IAAI,GAAG,CACpC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAC1E,CAAC;IACF,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/D,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO;IACT,CAAC;IAED,CAAC;QACC,MAAM,YAAY,GAAG,MAAM,EAAE;aAC1B,MAAM,CAAC;YACN,WAAW,EAAE,kBAAkB,CAAC,WAAW;YAC3C,UAAU,EAAE,kBAAkB,CAAC,UAAU;YACzC,OAAO,EAAE,kBAAkB,CAAC,OAAO;YACnC,SAAS,EAAE,kBAAkB,CAAC,SAAS;SACxC,CAAC;aACD,IAAI,CAAC,kBAAkB,CAAC;aACxB,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;QAEhE,MAAM,2BAA2B,GAAG,MAAM,gCAAgC,CACxE,EAAE,EACF,YAAY,CACb,CAAC;QAEF,MAAM,EAAE;aACL,MAAM,CAAC,2BAA2B,CAAC;aACnC,KAAK,CAAC,OAAO,CAAC,2BAA2B,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;aACrE,GAAG,EAAE,CAAC;QACT,MAAM,kDAAkD,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;QAE3E,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAC5B,IAAI,GAAG,CACL,YAAY,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CACxE,CACF,CAAC;QACF,MAAM,qCAAqC,GACzC,MAAM,yCAAyC,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;QAEnE,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAC/B,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;gBACjB,SAAS;YACX,CAAC;YAED,MAAM,qBAAqB,GAAG,GAAG,CAAC,WAAW;gBAC3C,CAAC,CAAC,2BAA2B,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC;gBAClD,CAAC,CAAC,SAAS,CAAC;YACd,MAAM,2CAA2C,CAAC,EAAE,EAAE,GAAG,CAAC,OAAO,EAAE;gBACjE,WAAW,EAAE,GAAG,CAAC,UAAU;oBACzB,CAAC,CAAC,CAAC,qCAAqC,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC;oBACrE,CAAC,CAAC,IAAI;gBACR,cAAc,EAAE,qBAAqB;gBACrC,SAAS,EAAE,kBAAkB,CAC3B,GAAG,CAAC,SAAS,EACb,GAAG,CAAC,WAAW;oBACb,CAAC,CAAC,CAAC,sBAAsB,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC;oBAChE,CAAC,CAAC,GAAG,CAAC,SAAS,CAClB;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oCAAoC,CAAC,KAK1D;IACC,MAAM,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,EAAE,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAC3E,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAC9B,IAAI,GAAG,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CACrE,CAAC;IACF,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO;IACT,CAAC;IAED,MAAM,EAAE;SACL,MAAM,CAAC,UAAU,CAAC;SAClB,GAAG,CAAC;QACH,cAAc,EAAE,oBAAoB;QACpC,cAAc,EAAE,SAAS;KAC1B,CAAC;SACD,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC;SAC5C,GAAG,EAAE,CAAC;IAET,KAAK,MAAM,QAAQ,IAAI,mBAAmB,EAAE,CAAC;QAC3C,MAAM,EAAE;aACL,MAAM,CAAC,UAAU,CAAC;aAClB,GAAG,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,iBAAiB,EAAE,CAAC;aAC7C,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;aAC9C,GAAG,EAAE,CAAC;QAET,IAAI,QAAQ,CAAC,kBAAkB,IAAI,QAAQ,CAAC,iBAAiB,EAAE,CAAC;YAC9D,MAAM,yBAAyB,CAAC;gBAC9B,WAAW,EAAE,QAAQ,CAAC,WAAW;gBACjC,YAAY,EAAE,EAAE,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB,EAAE;gBAC/D,EAAE;gBACF,SAAS;aACV,CAAC,CAAC;QACL,CAAC;QAED,IAAI,QAAQ,CAAC,iBAAiB,EAAE,CAAC;YAC/B,MAAM,EAAE;iBACL,MAAM,CAAC,YAAY,CAAC;iBACpB,GAAG,CAAC;gBACH,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;gBACvB,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;gBAC7C,UAAU,EAAE,SAAS;gBACrB,SAAS;aACV,CAAC;iBACD,KAAK,CACJ,GAAG,CACD,iBAAiB,CAAC,kBAAkB,CAAC,aAAa,CAAC,EACnD,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CACjD,CACF;iBACA,GAAG,EAAE,CAAC;QACX,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAAC,KAMpD;IACC,MAAM,EACJ,eAAe,EACf,oBAAoB,EACpB,aAAa,EACb,EAAE,EACF,SAAS,GACV,GAAG,KAAK,CAAC;IACV,MAAM,EAAE;SACL,MAAM,CAAC,UAAU,CAAC;SAClB,GAAG,CAAC;QACH,cAAc,EAAE,oBAAoB;QACpC,QAAQ,EAAE,aAAa;QACvB,cAAc,EAAE,SAAS;KAC1B,CAAC;SACD,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;SAC/C,GAAG,EAAE,CAAC;IACT,MAAM,UAAU,GAAG,iBAAiB,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACzE,MAAM,gBAAgB,GAAG,MAAM,EAAE;SAC9B,MAAM,CAAC,EAAE,WAAW,EAAE,YAAY,CAAC,SAAS,EAAE,CAAC;SAC/C,IAAI,CAAC,YAAY,CAAC;SAClB,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,YAAY,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC,CAAC;SAC3E,GAAG,EAAE,CAAC;IACT,KAAK,MAAM,MAAM,IAAI,gBAAgB,EAAE,CAAC;QACtC,MAAM,EAAE;aACL,MAAM,CAAC,YAAY,CAAC;aACpB,GAAG,CAAC;YACH,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;YACvB,iBAAiB,EAAE,aAAa;YAChC,SAAS;SACV,CAAC;aACD,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;aACtE,GAAG,EAAE,CAAC;IACX,CAAC;IACD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IACrE,MAAM,cAAc,GAAG,MAAM,EAAE;SAC5B,MAAM,CAAC,EAAE,WAAW,EAAE,YAAY,CAAC,SAAS,EAAE,CAAC;SAC/C,IAAI,CAAC,YAAY,CAAC;SAClB,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC,CAAC;SACzE,GAAG,EAAE,CAAC;IACT,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;QACpC,MAAM,EAAE;aACL,MAAM,CAAC,YAAY,CAAC;aACpB,GAAG,CAAC;YACH,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;YACvB,iBAAiB,EAAE,aAAa;YAChC,UAAU,EAAE,SAAS;YACrB,SAAS;SACV,CAAC;aACD,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;aACpE,GAAG,EAAE,CAAC;IACX,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,KAG9C;IACC,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC;IAClC,MAAM,EAAE;SACL,MAAM,CAAC,YAAY,CAAC;SACpB,KAAK,CACJ,GAAG,CAAC,sBAAsB,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CACvE;SACA,GAAG,EAAE,CAAC;IACT,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IACxE,MAAM,gDAAgD,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1E,MAAM,sCAAsC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;AAClE,CAAC","sourcesContent":["import { and, asc, eq, inArray } from \"drizzle-orm\";\nimport {\n containers,\n documentContainerProjection,\n documentProjection,\n inContainerIntentLanes,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { sqliteRowsPerStatement } from \"../../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { deleteContainerDocumentTombstoneHoldsForContainers } from \"../documents/containerDocumentTombstoneHoldsPersistence\";\nimport { setDocumentProjectionContainerInTransaction } from \"../documents/documentProjectionContainers\";\nimport { getLatestTimestamp } from \"../latestTimestamp\";\nimport type {\n ContainerRemoval,\n LocalRootDescendantReparentInput,\n} from \"./containerContentsPersistenceTypes\";\nimport { saveContainerCreateIntent } from \"./containerIntentPersistence\";\nimport { deleteContainerWatermarksInTransaction } from \"./containerSyncWatermarkPersistence\";\nimport { deleteContainerMetadataDocumentRowsInTransaction } from \"./dormantContainerMetadata\";\n\n// The containers being removed still exist when repair runs (deletion follows\n// it). Capture their org attribution so a detached document's pending writes\n// keep a resolvable organization id after the join source is gone — e.g. when\n// access to a shared org is revoked mid-sync.\nasync function loadOrganizationIdsForContainers(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<Map<string, string>> {\n const rows = await tx\n .select({\n id: containers.id,\n organizationId: containers.organizationId,\n })\n .from(containers)\n .where(inArray(containers.id, containerIds));\n const organizationIdByContainerId = new Map<string, string>();\n for (const container of rows) {\n if (container.id && container.organizationId) {\n organizationIdByContainerId.set(container.id, container.organizationId);\n }\n }\n return organizationIdByContainerId;\n}\n\n// One removed container can hold any number of documents, so their ids are\n// read back in batches that fit SQLite's bind budget.\nconst DOCUMENT_ID_BATCH_SIZE = sqliteRowsPerStatement(1);\n\n// A document unlinked from a removed container may keep other live links; its\n// projection then re-homes to the first remaining link (deterministic order)\n// instead of dropping to null.\nasync function loadFirstRemainingContainerIdByDocumentId(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<Map<string, string>> {\n const firstRemainingContainerIdByDocumentId = new Map<string, string>();\n for (\n let index = 0;\n index < documentIds.length;\n index += DOCUMENT_ID_BATCH_SIZE\n ) {\n // A document's links all fall in its own batch, so the first per\n // document is still the lowest container id.\n const remainingLinks = await tx\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(\n inArray(\n documentContainerProjection.documentId,\n documentIds.slice(index, index + DOCUMENT_ID_BATCH_SIZE),\n ),\n )\n .orderBy(\n asc(documentContainerProjection.documentId),\n asc(documentContainerProjection.containerId),\n );\n for (const link of remainingLinks) {\n if (!firstRemainingContainerIdByDocumentId.has(link.documentId)) {\n firstRemainingContainerIdByDocumentId.set(\n link.documentId,\n link.containerId,\n );\n }\n }\n }\n return firstRemainingContainerIdByDocumentId;\n}\n\nexport async function repairDocumentsForRemovedContainersInTransaction(input: {\n removals: ReadonlyArray<ContainerRemoval>;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { tx } = input;\n const updatedAtByContainerId = new Map(\n input.removals.map((removal) => [removal.containerId, removal.updatedAt]),\n );\n const containerIds = Array.from(updatedAtByContainerId.keys());\n if (containerIds.length === 0) {\n return;\n }\n\n {\n const selectedRows = await tx\n .select({\n containerId: documentProjection.containerId,\n documentId: documentProjection.documentId,\n localId: documentProjection.localId,\n updatedAt: documentProjection.updatedAt,\n })\n .from(documentProjection)\n .where(inArray(documentProjection.containerId, containerIds));\n\n const organizationIdByContainerId = await loadOrganizationIdsForContainers(\n tx,\n containerIds,\n );\n\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.containerId, containerIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForContainers(tx, containerIds);\n\n const documentIds = Array.from(\n new Set(\n selectedRows.flatMap((row) => (row.documentId ? [row.documentId] : [])),\n ),\n );\n const firstRemainingContainerIdByDocumentId =\n await loadFirstRemainingContainerIdByDocumentId(tx, documentIds);\n\n for (const row of selectedRows) {\n if (!row.localId) {\n continue;\n }\n\n const removedOrganizationId = row.containerId\n ? organizationIdByContainerId.get(row.containerId)\n : undefined;\n await setDocumentProjectionContainerInTransaction(tx, row.localId, {\n containerId: row.documentId\n ? (firstRemainingContainerIdByDocumentId.get(row.documentId) ?? null)\n : null,\n organizationId: removedOrganizationId,\n updatedAt: getLatestTimestamp(\n row.updatedAt,\n row.containerId\n ? (updatedAtByContainerId.get(row.containerId) ?? row.updatedAt)\n : row.updatedAt,\n ),\n });\n }\n }\n}\n\nexport async function updateReparentedDescendantContainers(input: {\n descendantReparents: ReadonlyArray<LocalRootDescendantReparentInput>;\n remoteOrganizationId: string;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<void> {\n const { descendantReparents, remoteOrganizationId, tx, updatedAt } = input;\n const descendantIds = Array.from(\n new Set(descendantReparents.map((reparent) => reparent.containerId)),\n );\n if (descendantIds.length === 0) {\n return;\n }\n\n await tx\n .update(containers)\n .set({\n organizationId: remoteOrganizationId,\n localUpdatedAt: updatedAt,\n })\n .where(inArray(containers.id, descendantIds))\n .run();\n\n for (const reparent of descendantReparents) {\n await tx\n .update(containers)\n .set({ parentId: reparent.parentContainerId })\n .where(eq(containers.id, reparent.containerId))\n .run();\n\n if (reparent.updateCreateIntent && reparent.parentContainerId) {\n await saveContainerCreateIntent({\n containerId: reparent.containerId,\n createIntent: { parentContainerId: reparent.parentContainerId },\n tx,\n updatedAt,\n });\n }\n\n if (reparent.parentContainerId) {\n await tx\n .update(localIntents)\n .set({\n id: crypto.randomUUID(),\n targetContainerId: reparent.parentContainerId,\n syncStatus: \"pending\",\n updatedAt,\n })\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.containerMove),\n eq(localIntents.subjectId, reparent.containerId),\n ),\n )\n .run();\n }\n }\n}\n\nexport async function reparentLocalContainerChildren(input: {\n fromContainerId: string;\n remoteOrganizationId: string;\n toContainerId: string;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<void> {\n const {\n fromContainerId,\n remoteOrganizationId,\n toContainerId,\n tx,\n updatedAt,\n } = input;\n await tx\n .update(containers)\n .set({\n organizationId: remoteOrganizationId,\n parentId: toContainerId,\n localUpdatedAt: updatedAt,\n })\n .where(eq(containers.parentId, fromContainerId))\n .run();\n const createLane = inLocalIntentLane(LOCAL_INTENT_LANES.containerCreate);\n const createIntentRows = await tx\n .select({ containerId: localIntents.subjectId })\n .from(localIntents)\n .where(and(createLane, eq(localIntents.targetContainerId, fromContainerId)))\n .all();\n for (const intent of createIntentRows) {\n await tx\n .update(localIntents)\n .set({\n id: crypto.randomUUID(),\n targetContainerId: toContainerId,\n updatedAt,\n })\n .where(and(createLane, eq(localIntents.subjectId, intent.containerId)))\n .run();\n }\n const moveLane = inLocalIntentLane(LOCAL_INTENT_LANES.containerMove);\n const moveIntentRows = await tx\n .select({ containerId: localIntents.subjectId })\n .from(localIntents)\n .where(and(moveLane, eq(localIntents.targetContainerId, fromContainerId)))\n .all();\n for (const intent of moveIntentRows) {\n await tx\n .update(localIntents)\n .set({\n id: crypto.randomUUID(),\n targetContainerId: toContainerId,\n syncStatus: \"pending\",\n updatedAt,\n })\n .where(and(moveLane, eq(localIntents.subjectId, intent.containerId)))\n .run();\n }\n}\n\nexport async function deleteLocalContainerRows(input: {\n containerId: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { containerId, tx } = input;\n await tx\n .delete(localIntents)\n .where(\n and(inContainerIntentLanes(), eq(localIntents.subjectId, containerId)),\n )\n .run();\n await tx.delete(containers).where(eq(containers.id, containerId)).run();\n await deleteContainerMetadataDocumentRowsInTransaction(tx, [containerId]);\n await deleteContainerWatermarksInTransaction(tx, [containerId]);\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import { and, eq, inArray } from "drizzle-orm";
2
+ import { DOCUMENTS_APP_KIND } from "../../documents/documentConstants.js";
3
+ import { documentContainerProjection, documents } from "../../sqlite/schema/index.js";
4
+ import { SQLITE_BOUND_VARIABLE_BUDGET } from "../../sqlite/sqliteBindBudget.js";
5
+ import { getClientSQLitePersistenceRuntime } from "../../sqlite/sqlitePersistenceRuntime.js";
6
+ // Leave room for the fixed binds each statement adds beside its id list.
7
+ const ID_BATCH_SIZE = SQLITE_BOUND_VARIABLE_BUDGET - 8;
8
+ function batches(values) {
9
+ const result = [];
10
+ for (let index = 0; index < values.length; index += ID_BATCH_SIZE) {
11
+ result.push(values.slice(index, index + ID_BATCH_SIZE));
12
+ }
13
+ return result;
14
+ }
15
+ /**
16
+ * Local ids of the documents whose every placement is one of `containerIds`.
17
+ * A document also placed in a container outside the set stays: removing these
18
+ * containers must not take a document another folder still shows.
19
+ */
20
+ export async function listDocumentLocalIdsPlacedOnlyIn(execSql, containerIds) {
21
+ const targets = new Set(containerIds);
22
+ if (targets.size === 0)
23
+ return [];
24
+ const { db } = getClientSQLitePersistenceRuntime(execSql);
25
+ const placedDocumentIds = new Set();
26
+ for (const batch of batches([...targets])) {
27
+ const rows = await db
28
+ .select({ documentId: documentContainerProjection.documentId })
29
+ .from(documentContainerProjection)
30
+ .where(inArray(documentContainerProjection.containerId, batch));
31
+ for (const row of rows)
32
+ placedDocumentIds.add(row.documentId);
33
+ }
34
+ const exclusiveDocumentIds = new Set(placedDocumentIds);
35
+ for (const batch of batches([...placedDocumentIds])) {
36
+ const rows = await db
37
+ .select({
38
+ containerId: documentContainerProjection.containerId,
39
+ documentId: documentContainerProjection.documentId,
40
+ })
41
+ .from(documentContainerProjection)
42
+ .where(inArray(documentContainerProjection.documentId, batch));
43
+ for (const row of rows) {
44
+ if (!targets.has(row.containerId)) {
45
+ exclusiveDocumentIds.delete(row.documentId);
46
+ }
47
+ }
48
+ }
49
+ const localIds = [];
50
+ for (const batch of batches([...exclusiveDocumentIds])) {
51
+ const rows = await db
52
+ .select({ localId: documents.localId })
53
+ .from(documents)
54
+ .where(and(eq(documents.appKind, DOCUMENTS_APP_KIND), inArray(documents.documentId, batch)));
55
+ for (const row of rows)
56
+ localIds.push(row.localId);
57
+ }
58
+ return localIds.sort();
59
+ }
60
+ //# sourceMappingURL=exclusiveDocumentPlacements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exclusiveDocumentPlacements.js","sourceRoot":"","sources":["exclusiveDocumentPlacements.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, inArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../documents/documentConstants\";\nimport { documentContainerProjection, documents } from \"../../sqlite/schema\";\nimport { SQLITE_BOUND_VARIABLE_BUDGET } from \"../../sqlite/sqliteBindBudget\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../sqlite/sqlSchema\";\n\n// Leave room for the fixed binds each statement adds beside its id list.\nconst ID_BATCH_SIZE = SQLITE_BOUND_VARIABLE_BUDGET - 8;\n\nfunction batches<T>(values: readonly T[]): T[][] {\n const result: T[][] = [];\n for (let index = 0; index < values.length; index += ID_BATCH_SIZE) {\n result.push(values.slice(index, index + ID_BATCH_SIZE));\n }\n return result;\n}\n\n/**\n * Local ids of the documents whose every placement is one of `containerIds`.\n * A document also placed in a container outside the set stays: removing these\n * containers must not take a document another folder still shows.\n */\nexport async function listDocumentLocalIdsPlacedOnlyIn(\n execSql: ExecSql,\n containerIds: readonly string[],\n): Promise<string[]> {\n const targets = new Set(containerIds);\n if (targets.size === 0) return [];\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const placedDocumentIds = new Set<string>();\n for (const batch of batches([...targets])) {\n const rows = await db\n .select({ documentId: documentContainerProjection.documentId })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.containerId, batch));\n for (const row of rows) placedDocumentIds.add(row.documentId);\n }\n\n const exclusiveDocumentIds = new Set(placedDocumentIds);\n for (const batch of batches([...placedDocumentIds])) {\n const rows = await db\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, batch));\n for (const row of rows) {\n if (!targets.has(row.containerId)) {\n exclusiveDocumentIds.delete(row.documentId);\n }\n }\n }\n\n const localIds: string[] = [];\n for (const batch of batches([...exclusiveDocumentIds])) {\n const rows = await db\n .select({ localId: documents.localId })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n inArray(documents.documentId, batch),\n ),\n );\n for (const row of rows) localIds.push(row.localId);\n }\n return localIds.sort();\n}\n"]}
@@ -1733,6 +1733,77 @@ export declare const documentProjectionTables: ReadonlyArray<SqlTableSchema>;
1733
1733
  export declare const containerSyncWatermarkTables: ReadonlyArray<SqlTableSchema>;
1734
1734
  export declare const clientSqlTables: ReadonlyArray<SqlTableSchema>;
1735
1735
  export declare const clientSQLiteSchema: {
1736
+ organizationSyncPauseRemovals: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
1737
+ name: "organization_sync_pause_removals";
1738
+ schema: undefined;
1739
+ columns: {
1740
+ localId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
1741
+ name: "local_id";
1742
+ tableName: "organization_sync_pause_removals";
1743
+ dataType: "string";
1744
+ columnType: "SQLiteText";
1745
+ data: string;
1746
+ driverParam: string;
1747
+ notNull: true;
1748
+ hasDefault: false;
1749
+ isPrimaryKey: true;
1750
+ isAutoincrement: false;
1751
+ hasRuntimeDefault: false;
1752
+ enumValues: [string, ...string[]];
1753
+ baseColumn: never;
1754
+ identity: undefined;
1755
+ generated: undefined;
1756
+ }, {}, {
1757
+ length: number | undefined;
1758
+ }>;
1759
+ organizationId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
1760
+ name: "organization_id";
1761
+ tableName: "organization_sync_pause_removals";
1762
+ dataType: "string";
1763
+ columnType: "SQLiteText";
1764
+ data: string;
1765
+ driverParam: string;
1766
+ notNull: true;
1767
+ hasDefault: false;
1768
+ isPrimaryKey: false;
1769
+ isAutoincrement: false;
1770
+ hasRuntimeDefault: false;
1771
+ enumValues: [string, ...string[]];
1772
+ baseColumn: never;
1773
+ identity: undefined;
1774
+ generated: undefined;
1775
+ }, {}, {
1776
+ length: number | undefined;
1777
+ }>;
1778
+ };
1779
+ dialect: "sqlite";
1780
+ }>;
1781
+ organizationSyncPauses: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
1782
+ name: "organization_sync_pauses";
1783
+ schema: undefined;
1784
+ columns: {
1785
+ organizationId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
1786
+ name: "organization_id";
1787
+ tableName: "organization_sync_pauses";
1788
+ dataType: "string";
1789
+ columnType: "SQLiteText";
1790
+ data: string;
1791
+ driverParam: string;
1792
+ notNull: true;
1793
+ hasDefault: false;
1794
+ isPrimaryKey: true;
1795
+ isAutoincrement: false;
1796
+ hasRuntimeDefault: false;
1797
+ enumValues: [string, ...string[]];
1798
+ baseColumn: never;
1799
+ identity: undefined;
1800
+ generated: undefined;
1801
+ }, {}, {
1802
+ length: number | undefined;
1803
+ }>;
1804
+ };
1805
+ dialect: "sqlite";
1806
+ }>;
1736
1807
  organizationProvisioningAttempts: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
1737
1808
  name: "organization_provisioning_attempts";
1738
1809
  schema: undefined;
@@ -12,6 +12,7 @@ import * as organizationDataUsage from "./organizationDataUsageSchema.js";
12
12
  import { organizationFounders } from "./organizationFounderSchema.js";
13
13
  import * as provisioningAttempt from "./organizationProvisioningAttemptSchema.js";
14
14
  import { organizationReadModelSQLiteSchema, organizationReadModelTables, } from "./organizationReadModelTableRegistry.js";
15
+ import * as organizationSyncPause from "./organizationSyncPauseSchema.js";
15
16
  import { principalGrantRetirements } from "./principalGrantRetirementSchema.js";
16
17
  import { principalHistoryEntries, principalHistoryEvidenceTables, principalHistoryNodes, principalHistoryPrefixes, } from "./principalHistoryEvidenceSchema.js";
17
18
  import { principalHistoryNodeReferences, principalHistoryRootOwners, } from "./principalHistoryNodeRetentionSchema.js";
@@ -486,6 +487,7 @@ export const clientSqlTables = [
486
487
  ...organizationReadModelTables,
487
488
  ...organizationDataUsage.organizationDataUsageTables,
488
489
  ...provisioningAttempt.organizationProvisioningAttemptTables,
490
+ ...organizationSyncPause.organizationSyncPauseTables,
489
491
  ];
490
492
  export const clientSQLiteSchema = {
491
493
  principalHistoryNodeReferences,
@@ -521,5 +523,6 @@ export const clientSQLiteSchema = {
521
523
  ...organizationReadModelSQLiteSchema,
522
524
  ...organizationDataUsage.organizationDataUsageSQLiteSchema,
523
525
  ...provisioningAttempt.organizationProvisioningAttemptSQLiteSchema,
526
+ ...organizationSyncPause.organizationSyncPauseSQLiteSchema,
524
527
  };
525
528
  //# sourceMappingURL=index.js.map