@tearleads/client-sdk 0.1.114 → 0.1.115
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/data/persistence/container-contents/containerRecoveryPersistence.js +152 -38
- package/data/persistence/container-contents/containerRecoveryPersistence.js.map +1 -1
- package/data/persistence/container-contents/documentContainerProjectionPersistence.js +47 -18
- package/data/persistence/container-contents/documentContainerProjectionPersistence.js.map +1 -1
- package/data/persistence/container-contents/documentPlacement.js +24 -5
- package/data/persistence/container-contents/documentPlacement.js.map +1 -1
- package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js +1 -1
- package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js.map +1 -1
- package/data/persistence/documents/documentDiscoveryEvidencePersistence.js +43 -35
- package/data/persistence/documents/documentDiscoveryEvidencePersistence.js.map +1 -1
- package/data/persistence/documents/documentsPersistence.js +4 -58
- package/data/persistence/documents/documentsPersistence.js.map +1 -1
- package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js +15 -11
- package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js.map +1 -1
- package/data/persistence/documents/internal/containerDocumentTombstones.js +87 -35
- package/data/persistence/documents/internal/containerDocumentTombstones.js.map +1 -1
- package/data/persistence/documents/internal/discoveredDocumentPlacement.js +5 -2
- package/data/persistence/documents/internal/discoveredDocumentPlacement.js.map +1 -1
- package/data/persistence/documents/internal/discoveredDocumentUpsert.js +174 -0
- package/data/persistence/documents/internal/discoveredDocumentUpsert.js.map +1 -0
- package/data/persistence/documents/internal/documentRows.js +96 -1
- package/data/persistence/documents/internal/documentRows.js.map +1 -1
- package/data/persistence/documents/internal/rowQueryPersistence.js +38 -20
- package/data/persistence/documents/internal/rowQueryPersistence.js.map +1 -1
- package/data/persistence/documents/records/documentRecordPersistence.d.ts +5 -0
- package/data/persistence/documents/records/documentRecordPersistence.js +41 -6
- package/data/persistence/documents/records/documentRecordPersistence.js.map +1 -1
- package/data/persistence/documents/records/index.d.ts +1 -1
- package/data/persistence/documents/records/index.js +1 -1
- package/data/persistence/documents/records/index.js.map +1 -1
- package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.d.ts +2 -0
- package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js +17 -1
- package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js.map +1 -1
- package/package.json +1 -1
- package/workflows/container-contents/remoteReset/containerRemoteReset.js +12 -7
- package/workflows/container-contents/remoteReset/containerRemoteReset.js.map +1 -1
- package/workflows/documents/remoteReset/documentRemoteReset.js +31 -18
- package/workflows/documents/remoteReset/documentRemoteReset.js.map +1 -1
|
@@ -8,8 +8,32 @@ import { recordContainerHydrationTombstones } from "./containerHydrationPersiste
|
|
|
8
8
|
import { repairDocumentsForRemovedContainersInTransaction } from "./containerStructuralRepair.js";
|
|
9
9
|
import { discardLinkIntentsForRemovedContainers } from "./documentLinkRemovalRepair.js";
|
|
10
10
|
import { deleteContainerMetadataDocumentRowsInTransaction } from "./dormantContainerMetadata.js";
|
|
11
|
+
const RECOVERY_METADATA_TABLES = [
|
|
12
|
+
[
|
|
13
|
+
"documents",
|
|
14
|
+
"document_id, recovery_document_id, recovery_generation, snapshot_end_version, pending_base_version",
|
|
15
|
+
],
|
|
16
|
+
["document_history_checkpoints", "snapshot, end_version_vector"],
|
|
17
|
+
[
|
|
18
|
+
"document_history_updates",
|
|
19
|
+
"id, update_data, origin, update_id, partial_start_version_vector, partial_end_version_vector, source_version_vector",
|
|
20
|
+
],
|
|
21
|
+
];
|
|
22
|
+
const CREATE_COLUMNS = "id, subject_id, target_container_id, intent_type, remote_container_id, remote_metadata_document_id, remote_metadata_access_state_hash";
|
|
23
|
+
const MOVE_COLUMNS = "id, subject_id, target_container_id, source_container_id, intent_type";
|
|
24
|
+
const LINK_COLUMNS = "id, local_id, subject_id, target_container_id, source_container_id, replace_linked_containers, intent_type";
|
|
25
|
+
const TARGET_COLUMNS = "intent_id, operation, container_id";
|
|
11
26
|
// The token covers all local work the user is agreeing to discard, including
|
|
12
27
|
// shared link intents. A second pane's edit invalidates the confirmation.
|
|
28
|
+
async function summarizeRecoverySnapshot(input) {
|
|
29
|
+
const { creates, dormant, links, metadata, moves, targets } = input;
|
|
30
|
+
const revision = await toFingerprint(new TextEncoder().encode(JSON.stringify([dormant, metadata, creates, moves, links, targets])));
|
|
31
|
+
return {
|
|
32
|
+
revision,
|
|
33
|
+
pendingUpdateCount: (metadata[2] ?? []).filter((row) => Reflect.get(row, "update_id") !== null).length,
|
|
34
|
+
hasStructuralIntent: creates.length + moves.length + links.length > 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
13
37
|
async function recoverySnapshot(execSql, containerId, organizationId) {
|
|
14
38
|
const [dormant] = await execSql(`SELECT container_id, organization_id FROM dormant_container_metadata
|
|
15
39
|
WHERE container_id = ? AND organization_id = ?
|
|
@@ -17,21 +41,11 @@ async function recoverySnapshot(execSql, containerId, organizationId) {
|
|
|
17
41
|
if (!dormant)
|
|
18
42
|
return null;
|
|
19
43
|
const metadata = [];
|
|
20
|
-
for (const [table, columns] of
|
|
21
|
-
[
|
|
22
|
-
"documents",
|
|
23
|
-
"document_id, recovery_document_id, recovery_generation, snapshot_end_version, pending_base_version",
|
|
24
|
-
],
|
|
25
|
-
["document_history_checkpoints", "snapshot, end_version_vector"],
|
|
26
|
-
[
|
|
27
|
-
"document_history_updates",
|
|
28
|
-
"id, update_data, origin, update_id, partial_start_version_vector, partial_end_version_vector, source_version_vector",
|
|
29
|
-
],
|
|
30
|
-
]) {
|
|
44
|
+
for (const [table, columns] of RECOVERY_METADATA_TABLES) {
|
|
31
45
|
metadata.push(await execSql(`SELECT ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id = ? ORDER BY rowid`, [containerId]));
|
|
32
46
|
}
|
|
33
|
-
const creates = await execSql(
|
|
34
|
-
const moves = await execSql(
|
|
47
|
+
const creates = await execSql(`SELECT ${CREATE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id = ? AND sync_status = 'pending' ORDER BY id`, [LOCAL_INTENT_LANES.containerCreate, containerId]);
|
|
48
|
+
const moves = await execSql(`SELECT ${MOVE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id = ? ORDER BY id`, [LOCAL_INTENT_LANES.containerMove, containerId]);
|
|
35
49
|
const placementWhere = `lane = ? AND (target_container_id = ? OR id IN
|
|
36
50
|
(SELECT intent_id FROM document_intent_link_targets WHERE container_id = ?))`;
|
|
37
51
|
const placementBind = [
|
|
@@ -39,40 +53,140 @@ async function recoverySnapshot(execSql, containerId, organizationId) {
|
|
|
39
53
|
containerId,
|
|
40
54
|
containerId,
|
|
41
55
|
];
|
|
42
|
-
const links = await execSql(`SELECT
|
|
43
|
-
const targets = await execSql(`SELECT
|
|
56
|
+
const links = await execSql(`SELECT ${LINK_COLUMNS} FROM local_intents WHERE ${placementWhere} ORDER BY id`, placementBind);
|
|
57
|
+
const targets = await execSql(`SELECT ${TARGET_COLUMNS} FROM document_intent_link_targets WHERE intent_id IN
|
|
44
58
|
(SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`, placementBind);
|
|
45
|
-
|
|
59
|
+
return summarizeRecoverySnapshot({
|
|
60
|
+
creates,
|
|
61
|
+
dormant,
|
|
62
|
+
links,
|
|
63
|
+
metadata,
|
|
64
|
+
moves,
|
|
65
|
+
targets,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Rows grouped by one column's value, dropping that column when asked. */
|
|
69
|
+
function groupRows(rows, column, drop) {
|
|
70
|
+
const groups = new Map();
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const key = String(Reflect.get(row, column));
|
|
73
|
+
const kept = drop
|
|
74
|
+
? Object.fromEntries(Object.entries(row).filter(([name]) => name !== column))
|
|
75
|
+
: row;
|
|
76
|
+
const group = groups.get(key);
|
|
77
|
+
if (group)
|
|
78
|
+
group.push(kept);
|
|
79
|
+
else
|
|
80
|
+
groups.set(key, [kept]);
|
|
81
|
+
}
|
|
82
|
+
return groups;
|
|
83
|
+
}
|
|
84
|
+
/** Keeps SQL `IN` lists, bound twice by the placement reads, under the budget. */
|
|
85
|
+
const RECOVERY_CONTAINER_BATCH_SIZE = 400;
|
|
86
|
+
/** Every row the recovery snapshots of a batch of folders read. */
|
|
87
|
+
async function readRecoveryBatchRows(execSql, ids) {
|
|
88
|
+
const list = ids.map(() => "?").join(", ");
|
|
89
|
+
const metadata = [];
|
|
90
|
+
for (const [table, columns] of RECOVERY_METADATA_TABLES) {
|
|
91
|
+
metadata.push(groupRows(await execSql(`SELECT local_id, ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id IN (${list}) ORDER BY local_id, rowid`, ids), "local_id", true));
|
|
92
|
+
}
|
|
93
|
+
const creates = groupRows(await execSql(`SELECT ${CREATE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id IN (${list}) AND sync_status = 'pending' ORDER BY subject_id, id`, [LOCAL_INTENT_LANES.containerCreate, ...ids]), "subject_id", false);
|
|
94
|
+
const moves = groupRows(await execSql(`SELECT ${MOVE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id IN (${list}) ORDER BY subject_id, id`, [LOCAL_INTENT_LANES.containerMove, ...ids]), "subject_id", false);
|
|
95
|
+
const placementWhere = `lane = ? AND (target_container_id IN (${list}) OR id IN
|
|
96
|
+
(SELECT intent_id FROM document_intent_link_targets WHERE container_id IN (${list})))`;
|
|
97
|
+
const placementBind = [LOCAL_INTENT_LANES.documentPlacement, ...ids, ...ids];
|
|
98
|
+
// Every placement intent any folder in the batch reaches, and ALL link
|
|
99
|
+
// targets of those intents: a folder's share is then the intents that
|
|
100
|
+
// target it or carry a link target in it, as the per-folder read selects.
|
|
101
|
+
const links = await execSql(`SELECT ${LINK_COLUMNS} FROM local_intents WHERE ${placementWhere} ORDER BY id`, placementBind);
|
|
102
|
+
const targets = await execSql(`SELECT ${TARGET_COLUMNS} FROM document_intent_link_targets WHERE intent_id IN
|
|
103
|
+
(SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`, placementBind);
|
|
104
|
+
return { creates, links, metadata, moves, targets };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Each folder's share of a batch's placement rows, indexed in one pass: the
|
|
108
|
+
* intents that target the folder or carry a link target in it (in intent id
|
|
109
|
+
* order), and all link targets of those intents (in the per-folder order).
|
|
110
|
+
*/
|
|
111
|
+
function indexPlacementRows(rows) {
|
|
112
|
+
const targetsByIntent = groupRows(rows.targets, "intent_id", false);
|
|
113
|
+
const linksByFolder = new Map();
|
|
114
|
+
for (const link of rows.links) {
|
|
115
|
+
const intentId = String(Reflect.get(link, "id"));
|
|
116
|
+
const folders = new Set([
|
|
117
|
+
String(Reflect.get(link, "target_container_id")),
|
|
118
|
+
...(targetsByIntent.get(intentId) ?? []).map((target) => String(Reflect.get(target, "container_id"))),
|
|
119
|
+
]);
|
|
120
|
+
for (const folder of folders) {
|
|
121
|
+
const folderLinks = linksByFolder.get(folder);
|
|
122
|
+
if (folderLinks)
|
|
123
|
+
folderLinks.push(link);
|
|
124
|
+
else
|
|
125
|
+
linksByFolder.set(folder, [link]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { linksByFolder, targetsByIntent };
|
|
129
|
+
}
|
|
130
|
+
/** One folder's snapshot from its batch's rows, in the per-folder shape. */
|
|
131
|
+
async function folderRecoverySnapshot(rows, placements, containerId, dormant) {
|
|
132
|
+
const links = placements.linksByFolder.get(containerId) ?? [];
|
|
133
|
+
// Links are in intent id order and targets in (intent id, container,
|
|
134
|
+
// operation) order, so concatenating per link keeps the per-folder order.
|
|
135
|
+
const targets = links.flatMap((link) => placements.targetsByIntent.get(String(Reflect.get(link, "id"))) ?? []);
|
|
136
|
+
const metadata = rows.metadata.map((byFolder) => byFolder.get(containerId) ?? []);
|
|
137
|
+
// The metadata record's updates are its checkpoint snapshot, read above; a
|
|
138
|
+
// folder with no record has none.
|
|
139
|
+
const snapshotRow = (metadata[1] ?? [])[0];
|
|
46
140
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
141
|
+
containerId,
|
|
142
|
+
metadataUpdates: (metadata[0] ?? []).length > 0 && snapshotRow
|
|
143
|
+
? String(Reflect.get(snapshotRow, "snapshot") ?? "")
|
|
144
|
+
: "",
|
|
145
|
+
snapshot: await summarizeRecoverySnapshot({
|
|
146
|
+
creates: rows.creates.get(containerId) ?? [],
|
|
147
|
+
dormant,
|
|
148
|
+
links,
|
|
149
|
+
metadata,
|
|
150
|
+
moves: rows.moves.get(containerId) ?? [],
|
|
151
|
+
targets,
|
|
152
|
+
}),
|
|
50
153
|
};
|
|
51
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* `recoverySnapshot` for every retained folder of an organization, one read
|
|
157
|
+
* per table for each batch of folders. Each folder's rows are grouped in the
|
|
158
|
+
* per-folder order (`local_id, rowid` for metadata; `subject_id, id` for
|
|
159
|
+
* folder intents), so its revision is byte-identical to `recoverySnapshot`'s,
|
|
160
|
+
* which a later discard compares.
|
|
161
|
+
*/
|
|
162
|
+
async function recoverySnapshots(execSql, organizationId) {
|
|
163
|
+
const dormantRows = await execSql(`SELECT container_id, organization_id FROM dormant_container_metadata d
|
|
164
|
+
WHERE organization_id = ?
|
|
165
|
+
AND NOT EXISTS (SELECT 1 FROM containers WHERE id = d.container_id)
|
|
166
|
+
ORDER BY container_id`, [organizationId]);
|
|
167
|
+
const result = [];
|
|
168
|
+
for (let offset = 0; offset < dormantRows.length; offset += RECOVERY_CONTAINER_BATCH_SIZE) {
|
|
169
|
+
const batch = dormantRows.slice(offset, offset + RECOVERY_CONTAINER_BATCH_SIZE);
|
|
170
|
+
const ids = batch.map((row) => String(Reflect.get(row, "container_id")));
|
|
171
|
+
const rows = await readRecoveryBatchRows(execSql, ids);
|
|
172
|
+
const placements = indexPlacementRows(rows);
|
|
173
|
+
for (const [index, dormant] of batch.entries()) {
|
|
174
|
+
result.push(await folderRecoverySnapshot(rows, placements, ids[index] ?? "", dormant));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
52
179
|
export async function listRetainedContainerMetadata(execSql, organizationId) {
|
|
53
180
|
if (!organizationId)
|
|
54
181
|
return [];
|
|
55
182
|
await sqlContainerContentsPersistence.ensureSchema(execSql);
|
|
56
183
|
await ensureSqlTables(execSql, documentPlacementIntentTables);
|
|
57
|
-
return runSerializedSqlMutation(execSql, async (locked) => getClientSQLitePersistenceRuntime(locked).transaction(async () => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const snapshot = await recoverySnapshot(locked, containerId, organizationId);
|
|
64
|
-
if (!snapshot)
|
|
65
|
-
continue;
|
|
66
|
-
const record = await sqlContainerContentsPersistence.loadContainerMetadataRecord(locked, containerId);
|
|
67
|
-
result.push({
|
|
68
|
-
containerId,
|
|
69
|
-
organizationId,
|
|
70
|
-
...snapshot,
|
|
71
|
-
metadataUpdates: record?.metadataUpdates ?? "",
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
return result;
|
|
75
|
-
}));
|
|
184
|
+
return runSerializedSqlMutation(execSql, async (locked) => getClientSQLitePersistenceRuntime(locked).transaction(async () => (await recoverySnapshots(locked, organizationId)).map(({ containerId, metadataUpdates, snapshot }) => ({
|
|
185
|
+
containerId,
|
|
186
|
+
organizationId,
|
|
187
|
+
...snapshot,
|
|
188
|
+
metadataUpdates,
|
|
189
|
+
}))));
|
|
76
190
|
}
|
|
77
191
|
/** Explicit local discard, never called by remote discovery. */
|
|
78
192
|
export async function discardRetainedContainerMetadata(execSql, input) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"containerRecoveryPersistence.js","sourceRoot":"","sources":["containerRecoveryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { toFingerprint } from \"@tearleads/crypto\";\nimport { and, eq } from \"drizzle-orm\";\nimport {\n documentPlacementIntentTables,\n inContainerIntentLanes,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport { sqlContainerContentsPersistence } from \"./containerContentsPersistence\";\nimport { recordContainerHydrationTombstones } from \"./containerHydrationPersistence\";\nimport { repairDocumentsForRemovedContainersInTransaction } from \"./containerStructuralRepair\";\nimport { discardLinkIntentsForRemovedContainers } from \"./documentLinkRemovalRepair\";\nimport { deleteContainerMetadataDocumentRowsInTransaction } from \"./dormantContainerMetadata\";\n\n// The token covers all local work the user is agreeing to discard, including\n// shared link intents. A second pane's edit invalidates the confirmation.\nasync function recoverySnapshot(\n execSql: ExecSql,\n containerId: string,\n organizationId: string,\n) {\n const [dormant] = await execSql(\n `SELECT container_id, organization_id FROM dormant_container_metadata\n WHERE container_id = ? AND organization_id = ?\n AND NOT EXISTS (SELECT 1 FROM containers WHERE id = ?)`,\n [containerId, organizationId, containerId],\n );\n if (!dormant) return null;\n const metadata = [];\n for (const [table, columns] of [\n [\n \"documents\",\n \"document_id, recovery_document_id, recovery_generation, snapshot_end_version, pending_base_version\",\n ],\n [\"document_history_checkpoints\", \"snapshot, end_version_vector\"],\n [\n \"document_history_updates\",\n \"id, update_data, origin, update_id, partial_start_version_vector, partial_end_version_vector, source_version_vector\",\n ],\n ]) {\n metadata.push(\n await execSql(\n `SELECT ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id = ? ORDER BY rowid`,\n [containerId],\n ),\n );\n }\n const creates = await execSql(\n \"SELECT id, subject_id, target_container_id, intent_type, remote_container_id, remote_metadata_document_id, remote_metadata_access_state_hash FROM local_intents WHERE lane = ? AND subject_id = ? AND sync_status = 'pending' ORDER BY id\",\n [LOCAL_INTENT_LANES.containerCreate, containerId],\n );\n const moves = await execSql(\n \"SELECT id, subject_id, target_container_id, source_container_id, intent_type FROM local_intents WHERE lane = ? AND subject_id = ? ORDER BY id\",\n [LOCAL_INTENT_LANES.containerMove, containerId],\n );\n const placementWhere = `lane = ? AND (target_container_id = ? OR id IN\n (SELECT intent_id FROM document_intent_link_targets WHERE container_id = ?))`;\n const placementBind = [\n LOCAL_INTENT_LANES.documentPlacement,\n containerId,\n containerId,\n ];\n const links = await execSql(\n `SELECT id, local_id, subject_id, target_container_id, source_container_id, replace_linked_containers, intent_type FROM local_intents WHERE ${placementWhere} ORDER BY id`,\n placementBind,\n );\n const targets = await execSql(\n `SELECT intent_id, operation, container_id FROM document_intent_link_targets WHERE intent_id IN\n (SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`,\n placementBind,\n );\n const revision = await toFingerprint(\n new TextEncoder().encode(\n JSON.stringify([dormant, metadata, creates, moves, links, targets]),\n ),\n );\n return {\n revision,\n pendingUpdateCount: (metadata[2] ?? []).filter(\n (row) => Reflect.get(row, \"update_id\") !== null,\n ).length,\n hasStructuralIntent: creates.length + moves.length + links.length > 0,\n };\n}\n\nexport async function listRetainedContainerMetadata(\n execSql: ExecSql,\n organizationId: string | null,\n) {\n if (!organizationId) return [];\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n return runSerializedSqlMutation(execSql, async (locked) =>\n getClientSQLitePersistenceRuntime(locked).transaction(async () => {\n const rows = await locked(\n `SELECT container_id FROM dormant_container_metadata\n WHERE organization_id = ? ORDER BY container_id`,\n [organizationId],\n );\n const result = [];\n for (const row of rows) {\n const containerId = String(Reflect.get(row, \"container_id\"));\n const snapshot = await recoverySnapshot(\n locked,\n containerId,\n organizationId,\n );\n if (!snapshot) continue;\n const record =\n await sqlContainerContentsPersistence.loadContainerMetadataRecord(\n locked,\n containerId,\n );\n result.push({\n containerId,\n organizationId,\n ...snapshot,\n metadataUpdates: record?.metadataUpdates ?? \"\",\n });\n }\n return result;\n }),\n );\n}\n\n/** Explicit local discard, never called by remote discovery. */\nexport async function discardRetainedContainerMetadata(\n execSql: ExecSql,\n input: {\n containerId: string;\n organizationId: string;\n revision: string;\n },\n): Promise<boolean> {\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n return runSerializedSqlMutation(execSql, async (locked) =>\n getClientSQLitePersistenceRuntime(locked).transaction(\n async (tx) => {\n const current = await recoverySnapshot(\n locked,\n input.containerId,\n input.organizationId,\n );\n if (!current || current.revision !== input.revision) return false;\n await recordContainerHydrationTombstones({\n tx,\n removals: [\n {\n containerId: input.containerId,\n reason: \"deleted\",\n updatedAt: new Date().toISOString(),\n },\n ],\n });\n await repairDocumentsForRemovedContainersInTransaction({\n tx,\n removals: [\n {\n containerId: input.containerId,\n reason: \"deleted\",\n updatedAt: new Date().toISOString(),\n },\n ],\n });\n await discardLinkIntentsForRemovedContainers({\n tx,\n containerIds: [input.containerId],\n });\n await tx\n .delete(localIntents)\n .where(\n and(\n inContainerIntentLanes(),\n eq(localIntents.subjectId, input.containerId),\n ),\n )\n .run();\n await deleteContainerMetadataDocumentRowsInTransaction(tx, [\n input.containerId,\n ]);\n return true;\n },\n { behavior: \"immediate\" },\n ),\n );\n}\n\n/** Retained metadata of an organization's absent folders; binds the org. */\nexport const RETAINED_CONTAINER_METADATA_SQL = `SELECT 1 FROM dormant_container_metadata d\n WHERE d.organization_id = ? AND NOT EXISTS (SELECT 1 FROM containers c WHERE c.id = d.container_id)`;\n\n/**\n * An organization's local folders whose queued move points at an unavailable\n * parent; binds the move lane, then the organization.\n */\nexport const RECOVERY_FOLDER_MOVES_SQL = `SELECT c.id FROM containers c JOIN local_intents i\n ON i.lane = ? AND i.subject_id = c.id AND i.target_container_id = c.parent_id\n WHERE c.organization_id = ? AND i.sync_status IN ('pending', 'blocked')\n AND NOT EXISTS (SELECT 1 FROM containers p WHERE p.id = c.parent_id)`;\n\n/** Existing local folders whose queued move points at an unavailable parent. */\nexport async function listRecoveryFolderMoveIds(\n execSql: ExecSql,\n organizationId: string | null,\n): Promise<string[]> {\n if (!organizationId) return [];\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n const rows = await execSql(`${RECOVERY_FOLDER_MOVES_SQL} ORDER BY c.id`, [\n LOCAL_INTENT_LANES.containerMove,\n organizationId,\n ]);\n return rows.map((row) => String(Reflect.get(row, \"id\")));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"containerRecoveryPersistence.js","sourceRoot":"","sources":["containerRecoveryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { toFingerprint } from \"@tearleads/crypto\";\nimport { and, eq } from \"drizzle-orm\";\nimport {\n documentPlacementIntentTables,\n inContainerIntentLanes,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport { sqlContainerContentsPersistence } from \"./containerContentsPersistence\";\nimport { recordContainerHydrationTombstones } from \"./containerHydrationPersistence\";\nimport { repairDocumentsForRemovedContainersInTransaction } from \"./containerStructuralRepair\";\nimport { discardLinkIntentsForRemovedContainers } from \"./documentLinkRemovalRepair\";\nimport { deleteContainerMetadataDocumentRowsInTransaction } from \"./dormantContainerMetadata\";\n\nconst RECOVERY_METADATA_TABLES = [\n [\n \"documents\",\n \"document_id, recovery_document_id, recovery_generation, snapshot_end_version, pending_base_version\",\n ],\n [\"document_history_checkpoints\", \"snapshot, end_version_vector\"],\n [\n \"document_history_updates\",\n \"id, update_data, origin, update_id, partial_start_version_vector, partial_end_version_vector, source_version_vector\",\n ],\n] as const;\nconst CREATE_COLUMNS =\n \"id, subject_id, target_container_id, intent_type, remote_container_id, remote_metadata_document_id, remote_metadata_access_state_hash\";\nconst MOVE_COLUMNS =\n \"id, subject_id, target_container_id, source_container_id, intent_type\";\nconst LINK_COLUMNS =\n \"id, local_id, subject_id, target_container_id, source_container_id, replace_linked_containers, intent_type\";\nconst TARGET_COLUMNS = \"intent_id, operation, container_id\";\n\ntype RecoveryRows = Awaited<ReturnType<ExecSql>>;\n\n// The token covers all local work the user is agreeing to discard, including\n// shared link intents. A second pane's edit invalidates the confirmation.\nasync function summarizeRecoverySnapshot(input: {\n creates: RecoveryRows;\n dormant: RecoveryRows[number];\n links: RecoveryRows;\n metadata: RecoveryRows[];\n moves: RecoveryRows;\n targets: RecoveryRows;\n}) {\n const { creates, dormant, links, metadata, moves, targets } = input;\n const revision = await toFingerprint(\n new TextEncoder().encode(\n JSON.stringify([dormant, metadata, creates, moves, links, targets]),\n ),\n );\n return {\n revision,\n pendingUpdateCount: (metadata[2] ?? []).filter(\n (row) => Reflect.get(row, \"update_id\") !== null,\n ).length,\n hasStructuralIntent: creates.length + moves.length + links.length > 0,\n };\n}\n\nasync function recoverySnapshot(\n execSql: ExecSql,\n containerId: string,\n organizationId: string,\n) {\n const [dormant] = await execSql(\n `SELECT container_id, organization_id FROM dormant_container_metadata\n WHERE container_id = ? AND organization_id = ?\n AND NOT EXISTS (SELECT 1 FROM containers WHERE id = ?)`,\n [containerId, organizationId, containerId],\n );\n if (!dormant) return null;\n const metadata = [];\n for (const [table, columns] of RECOVERY_METADATA_TABLES) {\n metadata.push(\n await execSql(\n `SELECT ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id = ? ORDER BY rowid`,\n [containerId],\n ),\n );\n }\n const creates = await execSql(\n `SELECT ${CREATE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id = ? AND sync_status = 'pending' ORDER BY id`,\n [LOCAL_INTENT_LANES.containerCreate, containerId],\n );\n const moves = await execSql(\n `SELECT ${MOVE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id = ? ORDER BY id`,\n [LOCAL_INTENT_LANES.containerMove, containerId],\n );\n const placementWhere = `lane = ? AND (target_container_id = ? OR id IN\n (SELECT intent_id FROM document_intent_link_targets WHERE container_id = ?))`;\n const placementBind = [\n LOCAL_INTENT_LANES.documentPlacement,\n containerId,\n containerId,\n ];\n const links = await execSql(\n `SELECT ${LINK_COLUMNS} FROM local_intents WHERE ${placementWhere} ORDER BY id`,\n placementBind,\n );\n const targets = await execSql(\n `SELECT ${TARGET_COLUMNS} FROM document_intent_link_targets WHERE intent_id IN\n (SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`,\n placementBind,\n );\n return summarizeRecoverySnapshot({\n creates,\n dormant,\n links,\n metadata,\n moves,\n targets,\n });\n}\n\n/** Rows grouped by one column's value, dropping that column when asked. */\nfunction groupRows(\n rows: RecoveryRows,\n column: string,\n drop: boolean,\n): Map<string, RecoveryRows> {\n const groups = new Map<string, RecoveryRows>();\n for (const row of rows) {\n const key = String(Reflect.get(row, column));\n const kept = drop\n ? Object.fromEntries(\n Object.entries(row).filter(([name]) => name !== column),\n )\n : row;\n const group = groups.get(key);\n if (group) group.push(kept);\n else groups.set(key, [kept]);\n }\n return groups;\n}\n\n/** Keeps SQL `IN` lists, bound twice by the placement reads, under the budget. */\nconst RECOVERY_CONTAINER_BATCH_SIZE = 400;\n\n/** Every row the recovery snapshots of a batch of folders read. */\nasync function readRecoveryBatchRows(execSql: ExecSql, ids: string[]) {\n const list = ids.map(() => \"?\").join(\", \");\n const metadata = [];\n for (const [table, columns] of RECOVERY_METADATA_TABLES) {\n metadata.push(\n groupRows(\n await execSql(\n `SELECT local_id, ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id IN (${list}) ORDER BY local_id, rowid`,\n ids,\n ),\n \"local_id\",\n true,\n ),\n );\n }\n const creates = groupRows(\n await execSql(\n `SELECT ${CREATE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id IN (${list}) AND sync_status = 'pending' ORDER BY subject_id, id`,\n [LOCAL_INTENT_LANES.containerCreate, ...ids],\n ),\n \"subject_id\",\n false,\n );\n const moves = groupRows(\n await execSql(\n `SELECT ${MOVE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id IN (${list}) ORDER BY subject_id, id`,\n [LOCAL_INTENT_LANES.containerMove, ...ids],\n ),\n \"subject_id\",\n false,\n );\n const placementWhere = `lane = ? AND (target_container_id IN (${list}) OR id IN\n (SELECT intent_id FROM document_intent_link_targets WHERE container_id IN (${list})))`;\n const placementBind = [LOCAL_INTENT_LANES.documentPlacement, ...ids, ...ids];\n // Every placement intent any folder in the batch reaches, and ALL link\n // targets of those intents: a folder's share is then the intents that\n // target it or carry a link target in it, as the per-folder read selects.\n const links = await execSql(\n `SELECT ${LINK_COLUMNS} FROM local_intents WHERE ${placementWhere} ORDER BY id`,\n placementBind,\n );\n const targets = await execSql(\n `SELECT ${TARGET_COLUMNS} FROM document_intent_link_targets WHERE intent_id IN\n (SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`,\n placementBind,\n );\n return { creates, links, metadata, moves, targets };\n}\n\n/**\n * Each folder's share of a batch's placement rows, indexed in one pass: the\n * intents that target the folder or carry a link target in it (in intent id\n * order), and all link targets of those intents (in the per-folder order).\n */\nfunction indexPlacementRows(\n rows: Awaited<ReturnType<typeof readRecoveryBatchRows>>,\n) {\n const targetsByIntent = groupRows(rows.targets, \"intent_id\", false);\n const linksByFolder = new Map<string, RecoveryRows>();\n for (const link of rows.links) {\n const intentId = String(Reflect.get(link, \"id\"));\n const folders = new Set([\n String(Reflect.get(link, \"target_container_id\")),\n ...(targetsByIntent.get(intentId) ?? []).map((target) =>\n String(Reflect.get(target, \"container_id\")),\n ),\n ]);\n for (const folder of folders) {\n const folderLinks = linksByFolder.get(folder);\n if (folderLinks) folderLinks.push(link);\n else linksByFolder.set(folder, [link]);\n }\n }\n return { linksByFolder, targetsByIntent };\n}\n\n/** One folder's snapshot from its batch's rows, in the per-folder shape. */\nasync function folderRecoverySnapshot(\n rows: Awaited<ReturnType<typeof readRecoveryBatchRows>>,\n placements: ReturnType<typeof indexPlacementRows>,\n containerId: string,\n dormant: RecoveryRows[number],\n) {\n const links = placements.linksByFolder.get(containerId) ?? [];\n // Links are in intent id order and targets in (intent id, container,\n // operation) order, so concatenating per link keeps the per-folder order.\n const targets = links.flatMap(\n (link) =>\n placements.targetsByIntent.get(String(Reflect.get(link, \"id\"))) ?? [],\n );\n const metadata = rows.metadata.map(\n (byFolder) => byFolder.get(containerId) ?? [],\n );\n // The metadata record's updates are its checkpoint snapshot, read above; a\n // folder with no record has none.\n const snapshotRow = (metadata[1] ?? [])[0];\n return {\n containerId,\n metadataUpdates:\n (metadata[0] ?? []).length > 0 && snapshotRow\n ? String(Reflect.get(snapshotRow, \"snapshot\") ?? \"\")\n : \"\",\n snapshot: await summarizeRecoverySnapshot({\n creates: rows.creates.get(containerId) ?? [],\n dormant,\n links,\n metadata,\n moves: rows.moves.get(containerId) ?? [],\n targets,\n }),\n };\n}\n\n/**\n * `recoverySnapshot` for every retained folder of an organization, one read\n * per table for each batch of folders. Each folder's rows are grouped in the\n * per-folder order (`local_id, rowid` for metadata; `subject_id, id` for\n * folder intents), so its revision is byte-identical to `recoverySnapshot`'s,\n * which a later discard compares.\n */\nasync function recoverySnapshots(execSql: ExecSql, organizationId: string) {\n const dormantRows = await execSql(\n `SELECT container_id, organization_id FROM dormant_container_metadata d\n WHERE organization_id = ?\n AND NOT EXISTS (SELECT 1 FROM containers WHERE id = d.container_id)\n ORDER BY container_id`,\n [organizationId],\n );\n const result = [];\n for (\n let offset = 0;\n offset < dormantRows.length;\n offset += RECOVERY_CONTAINER_BATCH_SIZE\n ) {\n const batch = dormantRows.slice(\n offset,\n offset + RECOVERY_CONTAINER_BATCH_SIZE,\n );\n const ids = batch.map((row) => String(Reflect.get(row, \"container_id\")));\n const rows = await readRecoveryBatchRows(execSql, ids);\n const placements = indexPlacementRows(rows);\n for (const [index, dormant] of batch.entries()) {\n result.push(\n await folderRecoverySnapshot(\n rows,\n placements,\n ids[index] ?? \"\",\n dormant,\n ),\n );\n }\n }\n return result;\n}\n\nexport async function listRetainedContainerMetadata(\n execSql: ExecSql,\n organizationId: string | null,\n) {\n if (!organizationId) return [];\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n return runSerializedSqlMutation(execSql, async (locked) =>\n getClientSQLitePersistenceRuntime(locked).transaction(async () =>\n (await recoverySnapshots(locked, organizationId)).map(\n ({ containerId, metadataUpdates, snapshot }) => ({\n containerId,\n organizationId,\n ...snapshot,\n metadataUpdates,\n }),\n ),\n ),\n );\n}\n\n/** Explicit local discard, never called by remote discovery. */\nexport async function discardRetainedContainerMetadata(\n execSql: ExecSql,\n input: {\n containerId: string;\n organizationId: string;\n revision: string;\n },\n): Promise<boolean> {\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, documentPlacementIntentTables);\n return runSerializedSqlMutation(execSql, async (locked) =>\n getClientSQLitePersistenceRuntime(locked).transaction(\n async (tx) => {\n const current = await recoverySnapshot(\n locked,\n input.containerId,\n input.organizationId,\n );\n if (!current || current.revision !== input.revision) return false;\n await recordContainerHydrationTombstones({\n tx,\n removals: [\n {\n containerId: input.containerId,\n reason: \"deleted\",\n updatedAt: new Date().toISOString(),\n },\n ],\n });\n await repairDocumentsForRemovedContainersInTransaction({\n tx,\n removals: [\n {\n containerId: input.containerId,\n reason: \"deleted\",\n updatedAt: new Date().toISOString(),\n },\n ],\n });\n await discardLinkIntentsForRemovedContainers({\n tx,\n containerIds: [input.containerId],\n });\n await tx\n .delete(localIntents)\n .where(\n and(\n inContainerIntentLanes(),\n eq(localIntents.subjectId, input.containerId),\n ),\n )\n .run();\n await deleteContainerMetadataDocumentRowsInTransaction(tx, [\n input.containerId,\n ]);\n return true;\n },\n { behavior: \"immediate\" },\n ),\n );\n}\n\n/** Retained metadata of an organization's absent folders; binds the org. */\nexport const RETAINED_CONTAINER_METADATA_SQL = `SELECT 1 FROM dormant_container_metadata d\n WHERE d.organization_id = ? AND NOT EXISTS (SELECT 1 FROM containers c WHERE c.id = d.container_id)`;\n\n/**\n * An organization's local folders whose queued move points at an unavailable\n * parent; binds the move lane, then the organization.\n */\nexport const RECOVERY_FOLDER_MOVES_SQL = `SELECT c.id FROM containers c JOIN local_intents i\n ON i.lane = ? AND i.subject_id = c.id AND i.target_container_id = c.parent_id\n WHERE c.organization_id = ? AND i.sync_status IN ('pending', 'blocked')\n AND NOT EXISTS (SELECT 1 FROM containers p WHERE p.id = c.parent_id)`;\n\n/** Existing local folders whose queued move points at an unavailable parent. */\nexport async function listRecoveryFolderMoveIds(\n execSql: ExecSql,\n organizationId: string | null,\n): Promise<string[]> {\n if (!organizationId) return [];\n await sqlContainerContentsPersistence.ensureSchema(execSql);\n const rows = await execSql(`${RECOVERY_FOLDER_MOVES_SQL} ORDER BY c.id`, [\n LOCAL_INTENT_LANES.containerMove,\n organizationId,\n ]);\n return rows.map((row) => String(Reflect.get(row, \"id\")));\n}\n"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { and, asc, eq, inArray, notInArray } from "drizzle-orm";
|
|
1
|
+
import { and, asc, eq, inArray, notInArray, or } from "drizzle-orm";
|
|
2
2
|
import { documentContainerProjection, documentContainerProjectionTables, documentPlacementIntentTables, documentTables, inLocalIntentLane, LOCAL_INTENT_LANES, localIntents, } from "../../sqlite/schema/index.js";
|
|
3
3
|
import { documentIntentLinkTargets } from "../../sqlite/schema/documentPlacementIntentSchema.js";
|
|
4
|
-
import { sqliteInsertBatchSize } from "../../sqlite/sqliteBindBudget.js";
|
|
4
|
+
import { REMOTE_RESET_SQL_BATCH_SIZE, remoteResetBatches, SQLITE_BOUND_VARIABLE_BUDGET, sqliteInsertBatchSize, } from "../../sqlite/sqliteBindBudget.js";
|
|
5
5
|
import { getClientSQLitePersistenceRuntime, } from "../../sqlite/sqlitePersistenceRuntime.js";
|
|
6
6
|
import { ensureSqlTables } from "../../sqlite/sqlSchema.js";
|
|
7
|
-
import { deleteContainerDocumentTombstoneHoldRowsForLinks, deleteContainerDocumentTombstoneHoldsForContainers, deleteContainerDocumentTombstoneHoldsForDocuments, } from "../documents/containerDocumentTombstoneHoldsPersistence.js";
|
|
7
|
+
import { deleteContainerDocumentTombstoneHoldRowsForLinks, deleteContainerDocumentTombstoneHoldsForContainers, deleteContainerDocumentTombstoneHoldsForDocuments, placementPairsMatch, } from "../documents/containerDocumentTombstoneHoldsPersistence.js";
|
|
8
8
|
import { filterWritableDocumentPlacements, } from "./documentPlacement.js";
|
|
9
9
|
const LINK_INSERT_BATCH_SIZE = sqliteInsertBatchSize(documentContainerProjection);
|
|
10
10
|
/** Link rows and their tombstone holds leave together on a remote reset. */
|
|
@@ -22,23 +22,52 @@ export async function deleteDocumentPlacementRowsByDocumentIds(tx, documentIds)
|
|
|
22
22
|
.run();
|
|
23
23
|
await deleteContainerDocumentTombstoneHoldsForDocuments(tx, documentIds);
|
|
24
24
|
}
|
|
25
|
-
/** Removes
|
|
26
|
-
export async function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/** Removes the listed (document, container) placements, 50 pairs per statement. */
|
|
26
|
+
export async function deleteDocumentPlacementRowsInTransaction(tx, placements) {
|
|
27
|
+
for (const batch of remoteResetBatches(placements)) {
|
|
28
|
+
await tx
|
|
29
|
+
.delete(documentContainerProjection)
|
|
30
|
+
.where(placementPairsMatch(batch, documentContainerProjection.documentId, documentContainerProjection.containerId))
|
|
31
|
+
.run();
|
|
32
|
+
}
|
|
31
33
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Removes each document's placements outside the containers its head links,
|
|
36
|
+
* batching documents so one statement stays inside SQLite's bind budget.
|
|
37
|
+
*/
|
|
38
|
+
export async function deleteUnlinkedDocumentPlacementRowsInTransaction(tx, linkedContainerIdsByDocumentId) {
|
|
39
|
+
let batch = [];
|
|
40
|
+
let binds = 0;
|
|
41
|
+
const flush = async () => {
|
|
42
|
+
if (batch.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
await tx
|
|
45
|
+
.delete(documentContainerProjection)
|
|
46
|
+
.where(or(...batch))
|
|
47
|
+
.run();
|
|
48
|
+
batch = [];
|
|
49
|
+
binds = 0;
|
|
50
|
+
};
|
|
51
|
+
for (const [documentId, linked] of linkedContainerIdsByDocumentId) {
|
|
52
|
+
// The document id plus one bind per linked container.
|
|
53
|
+
const documentBinds = 1 + linked.size;
|
|
54
|
+
if (batch.length >= REMOTE_RESET_SQL_BATCH_SIZE ||
|
|
55
|
+
binds + documentBinds > SQLITE_BOUND_VARIABLE_BUDGET) {
|
|
56
|
+
await flush();
|
|
57
|
+
}
|
|
58
|
+
batch.push(unlinkedDocumentPlacementRows(documentId, linked));
|
|
59
|
+
binds += documentBinds;
|
|
60
|
+
}
|
|
61
|
+
await flush();
|
|
62
|
+
}
|
|
63
|
+
/** A document's placements outside the containers its head links. */
|
|
64
|
+
function unlinkedDocumentPlacementRows(documentId, linkedContainerIds) {
|
|
65
|
+
const ofDocument = eq(documentContainerProjection.documentId, documentId);
|
|
66
|
+
return linkedContainerIds.size === 0
|
|
67
|
+
? ofDocument
|
|
68
|
+
: (and(ofDocument, notInArray(documentContainerProjection.containerId, [
|
|
39
69
|
...linkedContainerIds,
|
|
40
|
-
])))
|
|
41
|
-
.run();
|
|
70
|
+
])) ?? ofDocument);
|
|
42
71
|
}
|
|
43
72
|
/** Removes a deleted document's queued placement intents and link targets. */
|
|
44
73
|
export async function deleteDocumentPlacementIntentsInTransaction(tx, documentId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentContainerProjectionPersistence.js","sourceRoot":"","sources":["documentContainerProjectionPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray, notInArray } from \"drizzle-orm\";\nimport {\n documentContainerProjection,\n documentContainerProjectionTables,\n documentPlacementIntentTables,\n documentTables,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { documentIntentLinkTargets } from \"../../sqlite/schema/documentPlacementIntentSchema\";\nimport { sqliteInsertBatchSize } from \"../../sqlite/sqliteBindBudget\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\nimport {\n deleteContainerDocumentTombstoneHoldRowsForLinks,\n deleteContainerDocumentTombstoneHoldsForContainers,\n deleteContainerDocumentTombstoneHoldsForDocuments,\n} from \"../documents/containerDocumentTombstoneHoldsPersistence\";\n\nimport {\n type DocumentPlacementInput,\n type DocumentPlacementWriteOptions,\n filterWritableDocumentPlacements,\n} from \"./documentPlacement\";\n\nconst LINK_INSERT_BATCH_SIZE = sqliteInsertBatchSize(\n documentContainerProjection,\n);\n\ninterface DocumentContainerProjectionPersistence {\n ensureSchema: (execSql: ExecSql) => Promise<void>;\n listLinkedContainerIds: (\n execSql: ExecSql,\n documentId: string,\n ) => Promise<ReadonlyArray<string>>;\n listLinkedContainerIdsByDocumentIds: (\n execSql: ExecSql,\n documentIds: ReadonlyArray<string>,\n ) => Promise<ReadonlyMap<string, ReadonlyArray<string>>>;\n listDocumentIdsByContainerIds: (\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ) => Promise<ReadonlyArray<string>>;\n replaceDocumentLinks: (\n execSql: ExecSql,\n documentId: string,\n containerIds: ReadonlyArray<string>,\n options?: DocumentPlacementWriteOptions,\n ) => Promise<void>;\n replaceDocumentLinksBatch: (\n execSql: ExecSql,\n inputs: ReadonlyArray<DocumentPlacementInput>,\n options?: DocumentPlacementWriteOptions,\n ) => Promise<void>;\n}\n\n/** Link rows and their tombstone holds leave together on a remote reset. */\nexport async function deleteDocumentPlacementRowsByContainerIds(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.containerId, containerIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForContainers(tx, containerIds);\n}\n\nexport async function deleteDocumentPlacementRowsByDocumentIds(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, documentIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForDocuments(tx, documentIds);\n}\n\n/** Removes one placement a verified tombstone retired. */\nexport async function deleteDocumentPlacementRowInTransaction(\n tx: ClientSQLiteTransactionScope,\n placement: { readonly containerId: string; readonly documentId: string },\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(\n and(\n eq(documentContainerProjection.documentId, placement.documentId),\n eq(documentContainerProjection.containerId, placement.containerId),\n ),\n )\n .run();\n}\n\n/** Removes a document's placements outside the containers its head links. */\nexport async function deleteUnlinkedDocumentPlacementRowsInTransaction(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n linkedContainerIds: ReadonlySet<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(\n linkedContainerIds.size === 0\n ? eq(documentContainerProjection.documentId, documentId)\n : and(\n eq(documentContainerProjection.documentId, documentId),\n notInArray(documentContainerProjection.containerId, [\n ...linkedContainerIds,\n ]),\n ),\n )\n .run();\n}\n\n/** Removes a deleted document's queued placement intents and link targets. */\nexport async function deleteDocumentPlacementIntentsInTransaction(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n): Promise<void> {\n const placementIntent = and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n eq(localIntents.subjectId, documentId),\n );\n await tx\n .delete(documentIntentLinkTargets)\n .where(\n inArray(\n documentIntentLinkTargets.intentId,\n tx\n .select({ id: localIntents.id })\n .from(localIntents)\n .where(placementIntent),\n ),\n )\n .run();\n await tx.delete(localIntents).where(placementIntent).run();\n}\n\nexport const sqlDocumentContainerProjectionPersistence: DocumentContainerProjectionPersistence =\n {\n async ensureSchema(execSql) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n },\n async listLinkedContainerIds(execSql, documentId) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({ containerId: documentContainerProjection.containerId })\n .from(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, documentId))\n .orderBy(asc(documentContainerProjection.containerId));\n\n return rows.map((row) => row.containerId);\n },\n async listLinkedContainerIdsByDocumentIds(execSql, documentIds) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const uniqueDocumentIds = Array.from(new Set(documentIds));\n if (uniqueDocumentIds.length === 0) {\n return new Map();\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n documentId: documentContainerProjection.documentId,\n containerId: documentContainerProjection.containerId,\n })\n .from(documentContainerProjection)\n .where(\n inArray(documentContainerProjection.documentId, uniqueDocumentIds),\n )\n .orderBy(\n asc(documentContainerProjection.documentId),\n asc(documentContainerProjection.containerId),\n );\n\n const linkedContainerIdsByDocumentId = new Map<string, string[]>();\n for (const documentId of uniqueDocumentIds) {\n linkedContainerIdsByDocumentId.set(documentId, []);\n }\n\n for (const row of rows) {\n const linkedContainerIds =\n linkedContainerIdsByDocumentId.get(row.documentId) ?? [];\n linkedContainerIds.push(row.containerId);\n linkedContainerIdsByDocumentId.set(row.documentId, linkedContainerIds);\n }\n\n return linkedContainerIdsByDocumentId;\n },\n async listDocumentIdsByContainerIds(execSql, containerIds) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const uniqueContainerIds = Array.from(new Set(containerIds));\n if (uniqueContainerIds.length === 0) {\n return [];\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .selectDistinct({ documentId: documentContainerProjection.documentId })\n .from(documentContainerProjection)\n .where(\n inArray(documentContainerProjection.containerId, uniqueContainerIds),\n )\n .orderBy(asc(documentContainerProjection.documentId));\n\n return rows.map((row) => row.documentId);\n },\n async replaceDocumentLinks(execSql, documentId, containerIds, options) {\n await sqlDocumentContainerProjectionPersistence.replaceDocumentLinksBatch(\n execSql,\n [{ documentId, containerIds }],\n options,\n );\n },\n async replaceDocumentLinksBatch(execSql, inputs, options) {\n await ensureSqlTables(execSql, [\n ...documentContainerProjectionTables,\n ...documentPlacementIntentTables,\n ...documentTables,\n ]);\n const latest = new Map(inputs.map((input) => [input.documentId, input]));\n if (latest.size === 0) return;\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n const replace = async (tx: ClientSQLiteTransactionScope) => {\n const writable = await filterWritableDocumentPlacements(\n tx,\n [...latest.values()],\n options,\n );\n if (writable.length === 0) return;\n // A placement the writer re-asserts is no longer tombstoned: a\n // listing that links it again, or a local move that owns it, releases\n // the hold so the document is not hidden until the next retry.\n if (options?.moveIntentId) {\n await deleteContainerDocumentTombstoneHoldsForDocuments(\n tx,\n writable.map((input) => input.documentId),\n );\n } else {\n await deleteContainerDocumentTombstoneHoldRowsForLinks(tx, writable);\n }\n await tx\n .delete(documentContainerProjection)\n .where(\n inArray(\n documentContainerProjection.documentId,\n writable.map((input) => input.documentId),\n ),\n )\n .run();\n const updatedAt = new Date().toISOString();\n const rows = writable.flatMap((input) =>\n Array.from(new Set(input.containerIds))\n .sort()\n .map((containerId) => ({\n documentId: input.documentId,\n containerId,\n updatedAt,\n })),\n );\n // A lane page of multi-link documents can carry more link rows than\n // one statement may bind.\n for (\n let index = 0;\n index < rows.length;\n index += LINK_INSERT_BATCH_SIZE\n ) {\n await tx\n .insert(documentContainerProjection)\n .values(rows.slice(index, index + LINK_INSERT_BATCH_SIZE))\n .run();\n }\n };\n if (options?.stillCurrent) {\n await runtime.guardedTransaction(replace, options.stillCurrent);\n return;\n }\n await runtime.transaction(replace);\n },\n };\n"]}
|
|
1
|
+
{"version":3,"file":"documentContainerProjectionPersistence.js","sourceRoot":"","sources":["documentContainerProjectionPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray, notInArray, or, type SQL } from \"drizzle-orm\";\nimport {\n documentContainerProjection,\n documentContainerProjectionTables,\n documentPlacementIntentTables,\n documentTables,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { documentIntentLinkTargets } from \"../../sqlite/schema/documentPlacementIntentSchema\";\nimport {\n REMOTE_RESET_SQL_BATCH_SIZE,\n remoteResetBatches,\n SQLITE_BOUND_VARIABLE_BUDGET,\n sqliteInsertBatchSize,\n} from \"../../sqlite/sqliteBindBudget\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\nimport {\n deleteContainerDocumentTombstoneHoldRowsForLinks,\n deleteContainerDocumentTombstoneHoldsForContainers,\n deleteContainerDocumentTombstoneHoldsForDocuments,\n placementPairsMatch,\n} from \"../documents/containerDocumentTombstoneHoldsPersistence\";\n\nimport {\n type DocumentPlacementInput,\n type DocumentPlacementWriteOptions,\n filterWritableDocumentPlacements,\n} from \"./documentPlacement\";\n\nconst LINK_INSERT_BATCH_SIZE = sqliteInsertBatchSize(\n documentContainerProjection,\n);\n\ninterface DocumentContainerProjectionPersistence {\n ensureSchema: (execSql: ExecSql) => Promise<void>;\n listLinkedContainerIds: (\n execSql: ExecSql,\n documentId: string,\n ) => Promise<ReadonlyArray<string>>;\n listLinkedContainerIdsByDocumentIds: (\n execSql: ExecSql,\n documentIds: ReadonlyArray<string>,\n ) => Promise<ReadonlyMap<string, ReadonlyArray<string>>>;\n listDocumentIdsByContainerIds: (\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ) => Promise<ReadonlyArray<string>>;\n replaceDocumentLinks: (\n execSql: ExecSql,\n documentId: string,\n containerIds: ReadonlyArray<string>,\n options?: DocumentPlacementWriteOptions,\n ) => Promise<void>;\n replaceDocumentLinksBatch: (\n execSql: ExecSql,\n inputs: ReadonlyArray<DocumentPlacementInput>,\n options?: DocumentPlacementWriteOptions,\n ) => Promise<void>;\n}\n\n/** Link rows and their tombstone holds leave together on a remote reset. */\nexport async function deleteDocumentPlacementRowsByContainerIds(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.containerId, containerIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForContainers(tx, containerIds);\n}\n\nexport async function deleteDocumentPlacementRowsByDocumentIds(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<void> {\n await tx\n .delete(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, documentIds))\n .run();\n await deleteContainerDocumentTombstoneHoldsForDocuments(tx, documentIds);\n}\n\n/** Removes the listed (document, container) placements, 50 pairs per statement. */\nexport async function deleteDocumentPlacementRowsInTransaction(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<{\n readonly containerId: string;\n readonly documentId: string;\n }>,\n): Promise<void> {\n for (const batch of remoteResetBatches(placements)) {\n await tx\n .delete(documentContainerProjection)\n .where(\n placementPairsMatch(\n batch,\n documentContainerProjection.documentId,\n documentContainerProjection.containerId,\n ),\n )\n .run();\n }\n}\n\n/**\n * Removes each document's placements outside the containers its head links,\n * batching documents so one statement stays inside SQLite's bind budget.\n */\nexport async function deleteUnlinkedDocumentPlacementRowsInTransaction(\n tx: ClientSQLiteTransactionScope,\n linkedContainerIdsByDocumentId: ReadonlyMap<string, ReadonlySet<string>>,\n): Promise<void> {\n let batch: SQL[] = [];\n let binds = 0;\n const flush = async () => {\n if (batch.length === 0) return;\n await tx\n .delete(documentContainerProjection)\n .where(or(...batch))\n .run();\n batch = [];\n binds = 0;\n };\n for (const [documentId, linked] of linkedContainerIdsByDocumentId) {\n // The document id plus one bind per linked container.\n const documentBinds = 1 + linked.size;\n if (\n batch.length >= REMOTE_RESET_SQL_BATCH_SIZE ||\n binds + documentBinds > SQLITE_BOUND_VARIABLE_BUDGET\n ) {\n await flush();\n }\n batch.push(unlinkedDocumentPlacementRows(documentId, linked));\n binds += documentBinds;\n }\n await flush();\n}\n\n/** A document's placements outside the containers its head links. */\nfunction unlinkedDocumentPlacementRows(\n documentId: string,\n linkedContainerIds: ReadonlySet<string>,\n): SQL {\n const ofDocument = eq(documentContainerProjection.documentId, documentId);\n return linkedContainerIds.size === 0\n ? ofDocument\n : (and(\n ofDocument,\n notInArray(documentContainerProjection.containerId, [\n ...linkedContainerIds,\n ]),\n ) ?? ofDocument);\n}\n\n/** Removes a deleted document's queued placement intents and link targets. */\nexport async function deleteDocumentPlacementIntentsInTransaction(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n): Promise<void> {\n const placementIntent = and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n eq(localIntents.subjectId, documentId),\n );\n await tx\n .delete(documentIntentLinkTargets)\n .where(\n inArray(\n documentIntentLinkTargets.intentId,\n tx\n .select({ id: localIntents.id })\n .from(localIntents)\n .where(placementIntent),\n ),\n )\n .run();\n await tx.delete(localIntents).where(placementIntent).run();\n}\n\nexport const sqlDocumentContainerProjectionPersistence: DocumentContainerProjectionPersistence =\n {\n async ensureSchema(execSql) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n },\n async listLinkedContainerIds(execSql, documentId) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({ containerId: documentContainerProjection.containerId })\n .from(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, documentId))\n .orderBy(asc(documentContainerProjection.containerId));\n\n return rows.map((row) => row.containerId);\n },\n async listLinkedContainerIdsByDocumentIds(execSql, documentIds) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const uniqueDocumentIds = Array.from(new Set(documentIds));\n if (uniqueDocumentIds.length === 0) {\n return new Map();\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n documentId: documentContainerProjection.documentId,\n containerId: documentContainerProjection.containerId,\n })\n .from(documentContainerProjection)\n .where(\n inArray(documentContainerProjection.documentId, uniqueDocumentIds),\n )\n .orderBy(\n asc(documentContainerProjection.documentId),\n asc(documentContainerProjection.containerId),\n );\n\n const linkedContainerIdsByDocumentId = new Map<string, string[]>();\n for (const documentId of uniqueDocumentIds) {\n linkedContainerIdsByDocumentId.set(documentId, []);\n }\n\n for (const row of rows) {\n const linkedContainerIds =\n linkedContainerIdsByDocumentId.get(row.documentId) ?? [];\n linkedContainerIds.push(row.containerId);\n linkedContainerIdsByDocumentId.set(row.documentId, linkedContainerIds);\n }\n\n return linkedContainerIdsByDocumentId;\n },\n async listDocumentIdsByContainerIds(execSql, containerIds) {\n await ensureSqlTables(execSql, documentContainerProjectionTables);\n const uniqueContainerIds = Array.from(new Set(containerIds));\n if (uniqueContainerIds.length === 0) {\n return [];\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .selectDistinct({ documentId: documentContainerProjection.documentId })\n .from(documentContainerProjection)\n .where(\n inArray(documentContainerProjection.containerId, uniqueContainerIds),\n )\n .orderBy(asc(documentContainerProjection.documentId));\n\n return rows.map((row) => row.documentId);\n },\n async replaceDocumentLinks(execSql, documentId, containerIds, options) {\n await sqlDocumentContainerProjectionPersistence.replaceDocumentLinksBatch(\n execSql,\n [{ documentId, containerIds }],\n options,\n );\n },\n async replaceDocumentLinksBatch(execSql, inputs, options) {\n await ensureSqlTables(execSql, [\n ...documentContainerProjectionTables,\n ...documentPlacementIntentTables,\n ...documentTables,\n ]);\n const latest = new Map(inputs.map((input) => [input.documentId, input]));\n if (latest.size === 0) return;\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n const replace = async (tx: ClientSQLiteTransactionScope) => {\n const writable = await filterWritableDocumentPlacements(\n tx,\n [...latest.values()],\n options,\n );\n if (writable.length === 0) return;\n // A placement the writer re-asserts is no longer tombstoned: a\n // listing that links it again, or a local move that owns it, releases\n // the hold so the document is not hidden until the next retry.\n if (options?.moveIntentId) {\n await deleteContainerDocumentTombstoneHoldsForDocuments(\n tx,\n writable.map((input) => input.documentId),\n );\n } else {\n await deleteContainerDocumentTombstoneHoldRowsForLinks(tx, writable);\n }\n await tx\n .delete(documentContainerProjection)\n .where(\n inArray(\n documentContainerProjection.documentId,\n writable.map((input) => input.documentId),\n ),\n )\n .run();\n const updatedAt = new Date().toISOString();\n const rows = writable.flatMap((input) =>\n Array.from(new Set(input.containerIds))\n .sort()\n .map((containerId) => ({\n documentId: input.documentId,\n containerId,\n updatedAt,\n })),\n );\n // A lane page of multi-link documents can carry more link rows than\n // one statement may bind.\n for (\n let index = 0;\n index < rows.length;\n index += LINK_INSERT_BATCH_SIZE\n ) {\n await tx\n .insert(documentContainerProjection)\n .values(rows.slice(index, index + LINK_INSERT_BATCH_SIZE))\n .run();\n }\n };\n if (options?.stillCurrent) {\n await runtime.guardedTransaction(replace, options.stillCurrent);\n return;\n }\n await runtime.transaction(replace);\n },\n };\n"]}
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { and, eq, inArray } from "drizzle-orm";
|
|
2
2
|
import { DOCUMENTS_APP_KIND } from "../../documents/documentConstants.js";
|
|
3
3
|
import { documents, inLocalIntentLane, LOCAL_INTENT_LANES, localIntents, } from "../../sqlite/schema/index.js";
|
|
4
|
+
import { sqliteRowsPerStatement } from "../../sqlite/sqliteBindBudget.js";
|
|
5
|
+
const movePlacementSelection = {
|
|
6
|
+
id: localIntents.id,
|
|
7
|
+
intentType: localIntents.intentType,
|
|
8
|
+
targetContainerId: localIntents.targetContainerId,
|
|
9
|
+
};
|
|
4
10
|
export async function loadDocumentMovePlacement(tx, documentId) {
|
|
5
11
|
const [intent] = await tx
|
|
6
|
-
.select(
|
|
7
|
-
id: localIntents.id,
|
|
8
|
-
intentType: localIntents.intentType,
|
|
9
|
-
targetContainerId: localIntents.targetContainerId,
|
|
10
|
-
})
|
|
12
|
+
.select(movePlacementSelection)
|
|
11
13
|
.from(localIntents)
|
|
12
14
|
.where(and(inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement), eq(localIntents.subjectId, documentId)))
|
|
13
15
|
.limit(1);
|
|
14
16
|
return intent;
|
|
15
17
|
}
|
|
18
|
+
/** `loadDocumentMovePlacement` for many documents, keyed by document id. */
|
|
19
|
+
export async function loadDocumentMovePlacements(tx, documentIds) {
|
|
20
|
+
const placements = new Map();
|
|
21
|
+
const unique = Array.from(new Set(documentIds));
|
|
22
|
+
// The lane predicate binds one value; the rest carry document ids.
|
|
23
|
+
const chunkSize = sqliteRowsPerStatement(1, 1);
|
|
24
|
+
for (let offset = 0; offset < unique.length; offset += chunkSize) {
|
|
25
|
+
const rows = await tx
|
|
26
|
+
.select({ subjectId: localIntents.subjectId, ...movePlacementSelection })
|
|
27
|
+
.from(localIntents)
|
|
28
|
+
.where(and(inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement), inArray(localIntents.subjectId, unique.slice(offset, offset + chunkSize))));
|
|
29
|
+
for (const { subjectId, ...placement } of rows) {
|
|
30
|
+
placements.set(subjectId, placement);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return placements;
|
|
34
|
+
}
|
|
16
35
|
/** Check at commit time: neither intermediate replay nor discovery owns a pending move. */
|
|
17
36
|
export async function filterWritableDocumentPlacements(tx, inputs, options) {
|
|
18
37
|
const ids = inputs.map((input) => input.documentId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentPlacement.js","sourceRoot":"","sources":["documentPlacement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,GACb,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"documentPlacement.js","sourceRoot":"","sources":["documentPlacement.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, inArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../documents/documentConstants\";\nimport {\n documents,\n inLocalIntentLane,\n LOCAL_INTENT_LANES,\n localIntents,\n} from \"../../sqlite/schema\";\nimport { sqliteRowsPerStatement } from \"../../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\nexport interface DocumentPlacementInput {\n documentId: string;\n containerIds: ReadonlyArray<string>;\n accessEpoch?: number | undefined;\n}\n\nexport interface DocumentPlacementWriteOptions {\n stillCurrent?: (() => boolean) | undefined;\n moveIntentId?: string | undefined;\n}\n\nconst movePlacementSelection = {\n id: localIntents.id,\n intentType: localIntents.intentType,\n targetContainerId: localIntents.targetContainerId,\n};\n\nexport interface DocumentMovePlacement {\n readonly id: string | null;\n readonly intentType: string;\n readonly targetContainerId: string;\n}\n\nexport async function loadDocumentMovePlacement(\n tx: ClientSQLiteTransactionScope,\n documentId: string,\n): Promise<DocumentMovePlacement | undefined> {\n const [intent] = await tx\n .select(movePlacementSelection)\n .from(localIntents)\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n eq(localIntents.subjectId, documentId),\n ),\n )\n .limit(1);\n return intent;\n}\n\n/** `loadDocumentMovePlacement` for many documents, keyed by document id. */\nexport async function loadDocumentMovePlacements(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<Map<string, DocumentMovePlacement>> {\n const placements = new Map<string, DocumentMovePlacement>();\n const unique = Array.from(new Set(documentIds));\n // The lane predicate binds one value; the rest carry document ids.\n const chunkSize = sqliteRowsPerStatement(1, 1);\n for (let offset = 0; offset < unique.length; offset += chunkSize) {\n const rows = await tx\n .select({ subjectId: localIntents.subjectId, ...movePlacementSelection })\n .from(localIntents)\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n inArray(\n localIntents.subjectId,\n unique.slice(offset, offset + chunkSize),\n ),\n ),\n );\n for (const { subjectId, ...placement } of rows) {\n placements.set(subjectId, placement);\n }\n }\n return placements;\n}\n\n/** Check at commit time: neither intermediate replay nor discovery owns a pending move. */\nexport async function filterWritableDocumentPlacements(\n tx: ClientSQLiteTransactionScope,\n inputs: readonly DocumentPlacementInput[],\n options?: DocumentPlacementWriteOptions,\n): Promise<readonly DocumentPlacementInput[]> {\n const ids = inputs.map((input) => input.documentId);\n const intents = new Map(\n (\n await tx\n .select({\n documentId: localIntents.subjectId,\n id: localIntents.id,\n })\n .from(localIntents)\n .where(\n and(\n inLocalIntentLane(LOCAL_INTENT_LANES.documentPlacement),\n inArray(localIntents.subjectId, ids),\n ),\n )\n ).map((row) => [row.documentId, row.id]),\n );\n const versionedIds = inputs\n .filter((input) => input.accessEpoch !== undefined)\n .map((input) => input.documentId);\n const epochs = new Map<string, number>();\n if (versionedIds.length > 0) {\n const rows = await tx\n .select({\n documentId: documents.documentId,\n accessEpoch: documents.accessEpoch,\n })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n inArray(documents.documentId, versionedIds),\n ),\n );\n for (const row of rows)\n if (row.documentId)\n epochs.set(\n row.documentId,\n Math.max(epochs.get(row.documentId) ?? 0, row.accessEpoch),\n );\n }\n return inputs.filter((input) => {\n if (options?.moveIntentId)\n return intents.get(input.documentId) === options.moveIntentId;\n if (intents.has(input.documentId)) return false;\n return (\n input.accessEpoch === undefined ||\n input.accessEpoch >= (epochs.get(input.documentId) ?? 0)\n );\n });\n}\n"]}
|
|
@@ -4,7 +4,7 @@ import { containerDocumentTombstoneHolds, documentContainerProjectionTables, doc
|
|
|
4
4
|
import { getClientSQLitePersistenceRuntime } from "../../sqlite/sqlitePersistenceRuntime.js";
|
|
5
5
|
import { ensureSqlTables, runSerializedSqlMutation, } from "../../sqlite/sqlSchema.js";
|
|
6
6
|
import { deleteContainerDocumentTombstoneHoldRows, holdContainerDocumentTombstonesInTransaction, listContainerDocumentTombstoneHoldsForDocumentsInTransaction, listContainerDocumentTombstoneHoldsInTransaction, listKnownContainerDocumentPlacementsInTransaction, partitionContainerDocumentTombstoneHolds, } from "./internal/containerDocumentTombstoneHolds.js";
|
|
7
|
-
export { containerDocumentPlacementKey, deleteContainerDocumentTombstoneHoldRowsForLinks, deleteContainerDocumentTombstoneHoldsForContainers, deleteContainerDocumentTombstoneHoldsForDocuments, } from "./internal/containerDocumentTombstoneHolds.js";
|
|
7
|
+
export { containerDocumentPlacementKey, deleteContainerDocumentTombstoneHoldRowsForLinks, deleteContainerDocumentTombstoneHoldsForContainers, deleteContainerDocumentTombstoneHoldsForDocuments, placementPairsMatch, } from "./internal/containerDocumentTombstoneHolds.js";
|
|
8
8
|
/**
|
|
9
9
|
* Retry backoff for a held tombstone: fifteen minutes after the first failed
|
|
10
10
|
* verification, doubling on every further failure up to 32 hours. An
|