@tearleads/client-sdk 0.1.113 → 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/containerContentsPersistence.js +3 -8
- package/data/persistence/container-contents/containerContentsPersistence.js.map +1 -1
- package/data/persistence/container-contents/containerHydrationPersistence.js +15 -5
- package/data/persistence/container-contents/containerHydrationPersistence.js.map +1 -1
- package/data/persistence/container-contents/containerMetadataMutationPersistence.js +8 -2
- package/data/persistence/container-contents/containerMetadataMutationPersistence.js.map +1 -1
- package/data/persistence/container-contents/containerMetadataRows.js +34 -2
- package/data/persistence/container-contents/containerMetadataRows.js.map +1 -1
- package/data/persistence/container-contents/containerPersistence.d.ts +231 -0
- package/data/persistence/container-contents/containerPersistence.js +20 -16
- package/data/persistence/container-contents/containerPersistence.js.map +1 -1
- package/data/persistence/container-contents/containerRecoveryPersistence.js +167 -51
- package/data/persistence/container-contents/containerRecoveryPersistence.js.map +1 -1
- package/data/persistence/container-contents/documentContainerProjectionPersistence.js +56 -19
- package/data/persistence/container-contents/documentContainerProjectionPersistence.js.map +1 -1
- package/data/persistence/container-contents/documentMoveIntentPersistence.js +29 -5
- package/data/persistence/container-contents/documentMoveIntentPersistence.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/container-contents/documentPlacementIntentEnqueue.js +1 -1
- package/data/persistence/container-contents/documentPlacementIntentEnqueue.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/documentMutationPersistence.js +15 -15
- package/data/persistence/documents/internal/documentMutationPersistence.js.map +1 -1
- package/data/persistence/documents/internal/documentRows.js +122 -11
- package/data/persistence/documents/internal/documentRows.js.map +1 -1
- package/data/persistence/documents/internal/rowQueryPersistence.js +70 -27
- package/data/persistence/documents/internal/rowQueryPersistence.js.map +1 -1
- package/data/persistence/documents/records/documentHistoryPersistence.js +17 -11
- package/data/persistence/documents/records/documentHistoryPersistence.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/documentQueries/index.js +21 -6
- package/workflows/container-contents/documentQueries/index.js.map +1 -1
- package/workflows/container-contents/remoteReset/containerRemoteReset.js +12 -7
- package/workflows/container-contents/remoteReset/containerRemoteReset.js.map +1 -1
- package/workflows/documents/documentMutationCommit.js +11 -12
- package/workflows/documents/documentMutationCommit.js.map +1 -1
- package/workflows/documents/documentMutationPreparation.d.ts +1 -0
- package/workflows/documents/documentMutationPreparation.js +1 -0
- package/workflows/documents/documentMutationPreparation.js.map +1 -1
- package/workflows/documents/documentPersistGuards.js +24 -0
- package/workflows/documents/documentPersistGuards.js.map +1 -1
- package/workflows/documents/remoteReset/documentRemoteReset.js +31 -18
- package/workflows/documents/remoteReset/documentRemoteReset.js.map +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
1
|
+
import { and, asc, eq, inArray, or, sql } from "drizzle-orm";
|
|
2
2
|
import { containerDocumentTombstoneHolds, documentContainerProjection, documentProjection, } from "../../../sqlite/schema/index.js";
|
|
3
|
+
import { remoteResetBatches } from "../../../sqlite/sqliteBindBudget.js";
|
|
3
4
|
import { filterWritableDocumentPlacements } from "../../container-contents/documentPlacement.js";
|
|
4
5
|
/** Keep `IN (...)` lists well under SQLite's bound-parameter limit. */
|
|
5
6
|
const HOLD_SQL_ID_BATCH_SIZE = 400;
|
|
@@ -15,11 +16,18 @@ export function containerDocumentPlacementKey(placement) {
|
|
|
15
16
|
return `${placement.documentId}\u0000${placement.containerId}`;
|
|
16
17
|
}
|
|
17
18
|
const placementKey = containerDocumentPlacementKey;
|
|
19
|
+
/** Matches the listed (document, container) placements by primary key. */
|
|
20
|
+
export function placementPairsMatch(placements, documentIdColumn, containerIdColumn) {
|
|
21
|
+
return or(...placements.map((placement) => and(eq(documentIdColumn, placement.documentId), eq(containerIdColumn, placement.containerId))));
|
|
22
|
+
}
|
|
23
|
+
function uniquePlacements(placements) {
|
|
24
|
+
return Array.from(new Map(placements.map((placement) => [placementKey(placement), placement])).values());
|
|
25
|
+
}
|
|
18
26
|
export async function deleteContainerDocumentTombstoneHoldRows(tx, placements) {
|
|
19
|
-
for (const
|
|
27
|
+
for (const batch of remoteResetBatches(uniquePlacements(placements))) {
|
|
20
28
|
await tx
|
|
21
29
|
.delete(containerDocumentTombstoneHolds)
|
|
22
|
-
.where(
|
|
30
|
+
.where(placementPairsMatch(batch, containerDocumentTombstoneHolds.documentId, containerDocumentTombstoneHolds.containerId))
|
|
23
31
|
.run();
|
|
24
32
|
}
|
|
25
33
|
}
|
|
@@ -126,14 +134,10 @@ export async function holdContainerDocumentTombstonesInTransaction(tx, tombstone
|
|
|
126
134
|
}
|
|
127
135
|
/** Release the holds on placements a listing has re-asserted as linked. */
|
|
128
136
|
export async function deleteContainerDocumentTombstoneHoldRowsForLinks(tx, links) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.where(and(eq(containerDocumentTombstoneHolds.documentId, link.documentId), inArray(containerDocumentTombstoneHolds.containerId, batch)))
|
|
134
|
-
.run();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
+
await deleteContainerDocumentTombstoneHoldRows(tx, links.flatMap((link) => link.containerIds.map((containerId) => ({
|
|
138
|
+
containerId,
|
|
139
|
+
documentId: link.documentId,
|
|
140
|
+
}))));
|
|
137
141
|
}
|
|
138
142
|
const holdRowSelection = {
|
|
139
143
|
attempts: containerDocumentTombstoneHolds.attempts,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"containerDocumentTombstoneHolds.js","sourceRoot":"","sources":["containerDocumentTombstoneHolds.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray, sql } from \"drizzle-orm\";\nimport {\n containerDocumentTombstoneHolds,\n documentContainerProjection,\n documentProjection,\n} from \"../../../sqlite/schema\";\nimport type { ClientSQLiteTransactionScope } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport { filterWritableDocumentPlacements } from \"../../container-contents/documentPlacement\";\nimport type { ContainerDocumentTombstoneInput } from \"../types\";\n\nexport interface ContainerDocumentPlacementKey {\n readonly containerId: string;\n readonly documentId: string;\n}\n\nexport type HeldContainerDocumentTombstone = Omit<\n ContainerDocumentTombstoneInput,\n \"accessEpoch\" | \"linkedContainerIds\"\n> & {\n /** Held without a verification attempt; the backoff does not advance. */\n readonly deferred?: boolean | undefined;\n /** A verified head still links it; retry the tombstone while keeping it visible. */\n readonly refuted?: boolean | undefined;\n};\n\n/** Keep `IN (...)` lists well under SQLite's bound-parameter limit. */\nconst HOLD_SQL_ID_BATCH_SIZE = 400;\n\nfunction batches<T>(values: ReadonlyArray<T>): T[][] {\n const unique = Array.from(new Set(values));\n const result: T[][] = [];\n for (let index = 0; index < unique.length; index += HOLD_SQL_ID_BATCH_SIZE) {\n result.push(unique.slice(index, index + HOLD_SQL_ID_BATCH_SIZE));\n }\n return result;\n}\n\nexport function containerDocumentPlacementKey(\n placement: ContainerDocumentPlacementKey,\n): string {\n return `${placement.documentId}\\u0000${placement.containerId}`;\n}\nconst placementKey = containerDocumentPlacementKey;\n\nexport async function deleteContainerDocumentTombstoneHoldRows(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): Promise<void> {\n for (const placement of placements) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(\n and(\n eq(containerDocumentTombstoneHolds.documentId, placement.documentId),\n eq(\n containerDocumentTombstoneHolds.containerId,\n placement.containerId,\n ),\n ),\n )\n .run();\n }\n}\n\n/**\n * A local move takes ownership of a document's placement: the intent, not a\n * server listing, now says where it lives, so every hold that was hiding one\n * of its placements is released with the link rewrite.\n */\nexport async function deleteContainerDocumentTombstoneHoldsForDocuments(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<void> {\n for (const batch of batches(documentIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.documentId, batch))\n .run();\n }\n}\n\n/** Holds on containers whose link rows are being removed locally. */\nexport async function deleteContainerDocumentTombstoneHoldsForContainers(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n for (const batch of batches(containerIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.containerId, batch))\n .run();\n }\n}\n\n/**\n * The placements among `placements` that exist locally: a link row, or a\n * document projection row whose primary container is the named container.\n * A tombstone for any other placement has nothing to remove or hide, so it\n * is not worth fetching and verifying the document head for.\n */\nexport async function listKnownContainerDocumentPlacementsInTransaction(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): Promise<ContainerDocumentPlacementKey[]> {\n const known = new Set<string>();\n for (const batch of batches(placements.map((entry) => entry.documentId))) {\n const [linkRows, projectionRows] = await Promise.all([\n tx\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, batch)),\n tx\n .select({\n containerId: documentProjection.containerId,\n documentId: documentProjection.documentId,\n })\n .from(documentProjection)\n .where(inArray(documentProjection.documentId, batch)),\n ]);\n for (const row of [...linkRows, ...projectionRows]) {\n if (row.documentId !== null && row.containerId !== null) {\n known.add(\n placementKey({\n containerId: row.containerId,\n documentId: row.documentId,\n }),\n );\n }\n }\n }\n return placements.filter((placement) => known.has(placementKey(placement)));\n}\n\n/**\n * Hold listing tombstones whose signed evidence is unavailable. A placement a\n * pending move intent owns is never held: the intent, not the listing, decides\n * where the document lives until it settles (`ProtectPendingTombstones`).\n */\nexport async function holdContainerDocumentTombstonesInTransaction(\n tx: ClientSQLiteTransactionScope,\n tombstones: ReadonlyArray<HeldContainerDocumentTombstone>,\n now: string,\n): Promise<void> {\n const writable = new Set(\n (\n await filterWritableDocumentPlacements(\n tx,\n tombstones.map((tombstone) => ({\n documentId: tombstone.documentId,\n containerIds: [],\n })),\n )\n ).map((input) => input.documentId),\n );\n for (const tombstone of tombstones) {\n if (!writable.has(tombstone.documentId)) continue;\n const row = {\n containerId: tombstone.containerId,\n documentId: tombstone.documentId,\n tombstonedAt: tombstone.updatedAt,\n hidden: !tombstone.refuted,\n updatedAt: now,\n };\n // A deferred hold was not attempted: it keeps its attempt count and its\n // last-attempt time, so it stays due and is not starved by the cap.\n await tx\n .insert(containerDocumentTombstoneHolds)\n .values({ ...row, attempts: tombstone.deferred ? 0 : 1 })\n .onConflictDoUpdate({\n target: [\n containerDocumentTombstoneHolds.documentId,\n containerDocumentTombstoneHolds.containerId,\n ],\n set: tombstone.deferred\n ? { tombstonedAt: row.tombstonedAt }\n : {\n ...row,\n hidden: tombstone.refuted\n ? false\n : containerDocumentTombstoneHolds.hidden,\n attempts: sql`${containerDocumentTombstoneHolds.attempts} + 1`,\n },\n })\n .run();\n }\n}\n\n/** Release the holds on placements a listing has re-asserted as linked. */\nexport async function deleteContainerDocumentTombstoneHoldRowsForLinks(\n tx: ClientSQLiteTransactionScope,\n links: ReadonlyArray<{\n readonly containerIds: ReadonlyArray<string>;\n readonly documentId: string;\n }>,\n): Promise<void> {\n for (const link of links) {\n for (const batch of batches(link.containerIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(\n and(\n eq(containerDocumentTombstoneHolds.documentId, link.documentId),\n inArray(containerDocumentTombstoneHolds.containerId, batch),\n ),\n )\n .run();\n }\n }\n}\n\nexport interface ContainerDocumentTombstoneHoldRow\n extends ContainerDocumentPlacementKey {\n readonly attempts: number;\n readonly hidden: boolean;\n readonly tombstonedAt: string;\n readonly updatedAt: string;\n}\n\nconst holdRowSelection = {\n attempts: containerDocumentTombstoneHolds.attempts,\n hidden: containerDocumentTombstoneHolds.hidden,\n containerId: containerDocumentTombstoneHolds.containerId,\n documentId: containerDocumentTombstoneHolds.documentId,\n tombstonedAt: containerDocumentTombstoneHolds.tombstonedAt,\n updatedAt: containerDocumentTombstoneHolds.updatedAt,\n};\n\n/** Every hold on the given containers, including visible refuted retries. */\nexport async function listContainerDocumentTombstoneHoldsInTransaction(\n handle: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<ContainerDocumentTombstoneHoldRow[]> {\n const holds: ContainerDocumentTombstoneHoldRow[] = [];\n for (const batch of batches(containerIds)) {\n holds.push(\n ...(await handle\n .select(holdRowSelection)\n .from(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.containerId, batch))\n .orderBy(\n asc(containerDocumentTombstoneHolds.containerId),\n asc(containerDocumentTombstoneHolds.documentId),\n )),\n );\n }\n return holds;\n}\n\n/** Every hidden hold on the given documents, in any container. */\nexport async function listContainerDocumentTombstoneHoldsForDocumentsInTransaction(\n handle: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<ContainerDocumentTombstoneHoldRow[]> {\n const holds: ContainerDocumentTombstoneHoldRow[] = [];\n for (const batch of batches(documentIds)) {\n holds.push(\n ...(await handle\n .select(holdRowSelection)\n .from(containerDocumentTombstoneHolds)\n .where(\n and(\n inArray(containerDocumentTombstoneHolds.documentId, batch),\n eq(containerDocumentTombstoneHolds.hidden, true),\n ),\n )\n .orderBy(\n asc(containerDocumentTombstoneHolds.documentId),\n asc(containerDocumentTombstoneHolds.containerId),\n )),\n );\n }\n return holds;\n}\n\n/**\n * Split the holds on the given containers into those whose placement is gone\n * (the document was purged, reset, or relinked locally: nothing left to hide,\n * so the hold is dropped) and those still hiding a placement.\n */\nexport async function partitionContainerDocumentTombstoneHolds(\n handle: ClientSQLiteTransactionScope,\n holds: ReadonlyArray<ContainerDocumentTombstoneHoldRow>,\n): Promise<{\n live: ContainerDocumentTombstoneHoldRow[];\n orphaned: ContainerDocumentTombstoneHoldRow[];\n}> {\n const known = new Set(\n (\n await listKnownContainerDocumentPlacementsInTransaction(handle, holds)\n ).map(placementKey),\n );\n return {\n live: holds.filter((hold) => known.has(placementKey(hold))),\n orphaned: holds.filter((hold) => !known.has(placementKey(hold))),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"containerDocumentTombstoneHolds.js","sourceRoot":"","sources":["containerDocumentTombstoneHolds.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray, or, type SQL, sql } from \"drizzle-orm\";\nimport type { SQLiteColumn } from \"drizzle-orm/sqlite-core\";\nimport {\n containerDocumentTombstoneHolds,\n documentContainerProjection,\n documentProjection,\n} from \"../../../sqlite/schema\";\nimport { remoteResetBatches } from \"../../../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport { filterWritableDocumentPlacements } from \"../../container-contents/documentPlacement\";\nimport type { ContainerDocumentTombstoneInput } from \"../types\";\n\nexport interface ContainerDocumentPlacementKey {\n readonly containerId: string;\n readonly documentId: string;\n}\n\nexport type HeldContainerDocumentTombstone = Omit<\n ContainerDocumentTombstoneInput,\n \"accessEpoch\" | \"linkedContainerIds\"\n> & {\n /** Held without a verification attempt; the backoff does not advance. */\n readonly deferred?: boolean | undefined;\n /** A verified head still links it; retry the tombstone while keeping it visible. */\n readonly refuted?: boolean | undefined;\n};\n\n/** Keep `IN (...)` lists well under SQLite's bound-parameter limit. */\nconst HOLD_SQL_ID_BATCH_SIZE = 400;\n\nfunction batches<T>(values: ReadonlyArray<T>): T[][] {\n const unique = Array.from(new Set(values));\n const result: T[][] = [];\n for (let index = 0; index < unique.length; index += HOLD_SQL_ID_BATCH_SIZE) {\n result.push(unique.slice(index, index + HOLD_SQL_ID_BATCH_SIZE));\n }\n return result;\n}\n\nexport function containerDocumentPlacementKey(\n placement: ContainerDocumentPlacementKey,\n): string {\n return `${placement.documentId}\\u0000${placement.containerId}`;\n}\nconst placementKey = containerDocumentPlacementKey;\n\n/** Matches the listed (document, container) placements by primary key. */\nexport function placementPairsMatch(\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n documentIdColumn: SQLiteColumn,\n containerIdColumn: SQLiteColumn,\n): SQL | undefined {\n return or(\n ...placements.map((placement) =>\n and(\n eq(documentIdColumn, placement.documentId),\n eq(containerIdColumn, placement.containerId),\n ),\n ),\n );\n}\n\nfunction uniquePlacements(\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): ContainerDocumentPlacementKey[] {\n return Array.from(\n new Map(\n placements.map((placement) => [placementKey(placement), placement]),\n ).values(),\n );\n}\n\nexport async function deleteContainerDocumentTombstoneHoldRows(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): Promise<void> {\n for (const batch of remoteResetBatches(uniquePlacements(placements))) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(\n placementPairsMatch(\n batch,\n containerDocumentTombstoneHolds.documentId,\n containerDocumentTombstoneHolds.containerId,\n ),\n )\n .run();\n }\n}\n\n/**\n * A local move takes ownership of a document's placement: the intent, not a\n * server listing, now says where it lives, so every hold that was hiding one\n * of its placements is released with the link rewrite.\n */\nexport async function deleteContainerDocumentTombstoneHoldsForDocuments(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<void> {\n for (const batch of batches(documentIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.documentId, batch))\n .run();\n }\n}\n\n/** Holds on containers whose link rows are being removed locally. */\nexport async function deleteContainerDocumentTombstoneHoldsForContainers(\n tx: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n for (const batch of batches(containerIds)) {\n await tx\n .delete(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.containerId, batch))\n .run();\n }\n}\n\n/**\n * The placements among `placements` that exist locally: a link row, or a\n * document projection row whose primary container is the named container.\n * A tombstone for any other placement has nothing to remove or hide, so it\n * is not worth fetching and verifying the document head for.\n */\nexport async function listKnownContainerDocumentPlacementsInTransaction(\n tx: ClientSQLiteTransactionScope,\n placements: ReadonlyArray<ContainerDocumentPlacementKey>,\n): Promise<ContainerDocumentPlacementKey[]> {\n const known = new Set<string>();\n for (const batch of batches(placements.map((entry) => entry.documentId))) {\n const [linkRows, projectionRows] = await Promise.all([\n tx\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, batch)),\n tx\n .select({\n containerId: documentProjection.containerId,\n documentId: documentProjection.documentId,\n })\n .from(documentProjection)\n .where(inArray(documentProjection.documentId, batch)),\n ]);\n for (const row of [...linkRows, ...projectionRows]) {\n if (row.documentId !== null && row.containerId !== null) {\n known.add(\n placementKey({\n containerId: row.containerId,\n documentId: row.documentId,\n }),\n );\n }\n }\n }\n return placements.filter((placement) => known.has(placementKey(placement)));\n}\n\n/**\n * Hold listing tombstones whose signed evidence is unavailable. A placement a\n * pending move intent owns is never held: the intent, not the listing, decides\n * where the document lives until it settles (`ProtectPendingTombstones`).\n */\nexport async function holdContainerDocumentTombstonesInTransaction(\n tx: ClientSQLiteTransactionScope,\n tombstones: ReadonlyArray<HeldContainerDocumentTombstone>,\n now: string,\n): Promise<void> {\n const writable = new Set(\n (\n await filterWritableDocumentPlacements(\n tx,\n tombstones.map((tombstone) => ({\n documentId: tombstone.documentId,\n containerIds: [],\n })),\n )\n ).map((input) => input.documentId),\n );\n for (const tombstone of tombstones) {\n if (!writable.has(tombstone.documentId)) continue;\n const row = {\n containerId: tombstone.containerId,\n documentId: tombstone.documentId,\n tombstonedAt: tombstone.updatedAt,\n hidden: !tombstone.refuted,\n updatedAt: now,\n };\n // A deferred hold was not attempted: it keeps its attempt count and its\n // last-attempt time, so it stays due and is not starved by the cap.\n await tx\n .insert(containerDocumentTombstoneHolds)\n .values({ ...row, attempts: tombstone.deferred ? 0 : 1 })\n .onConflictDoUpdate({\n target: [\n containerDocumentTombstoneHolds.documentId,\n containerDocumentTombstoneHolds.containerId,\n ],\n set: tombstone.deferred\n ? { tombstonedAt: row.tombstonedAt }\n : {\n ...row,\n hidden: tombstone.refuted\n ? false\n : containerDocumentTombstoneHolds.hidden,\n attempts: sql`${containerDocumentTombstoneHolds.attempts} + 1`,\n },\n })\n .run();\n }\n}\n\n/** Release the holds on placements a listing has re-asserted as linked. */\nexport async function deleteContainerDocumentTombstoneHoldRowsForLinks(\n tx: ClientSQLiteTransactionScope,\n links: ReadonlyArray<{\n readonly containerIds: ReadonlyArray<string>;\n readonly documentId: string;\n }>,\n): Promise<void> {\n await deleteContainerDocumentTombstoneHoldRows(\n tx,\n links.flatMap((link) =>\n link.containerIds.map((containerId) => ({\n containerId,\n documentId: link.documentId,\n })),\n ),\n );\n}\n\nexport interface ContainerDocumentTombstoneHoldRow\n extends ContainerDocumentPlacementKey {\n readonly attempts: number;\n readonly hidden: boolean;\n readonly tombstonedAt: string;\n readonly updatedAt: string;\n}\n\nconst holdRowSelection = {\n attempts: containerDocumentTombstoneHolds.attempts,\n hidden: containerDocumentTombstoneHolds.hidden,\n containerId: containerDocumentTombstoneHolds.containerId,\n documentId: containerDocumentTombstoneHolds.documentId,\n tombstonedAt: containerDocumentTombstoneHolds.tombstonedAt,\n updatedAt: containerDocumentTombstoneHolds.updatedAt,\n};\n\n/** Every hold on the given containers, including visible refuted retries. */\nexport async function listContainerDocumentTombstoneHoldsInTransaction(\n handle: ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<ContainerDocumentTombstoneHoldRow[]> {\n const holds: ContainerDocumentTombstoneHoldRow[] = [];\n for (const batch of batches(containerIds)) {\n holds.push(\n ...(await handle\n .select(holdRowSelection)\n .from(containerDocumentTombstoneHolds)\n .where(inArray(containerDocumentTombstoneHolds.containerId, batch))\n .orderBy(\n asc(containerDocumentTombstoneHolds.containerId),\n asc(containerDocumentTombstoneHolds.documentId),\n )),\n );\n }\n return holds;\n}\n\n/** Every hidden hold on the given documents, in any container. */\nexport async function listContainerDocumentTombstoneHoldsForDocumentsInTransaction(\n handle: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<ContainerDocumentTombstoneHoldRow[]> {\n const holds: ContainerDocumentTombstoneHoldRow[] = [];\n for (const batch of batches(documentIds)) {\n holds.push(\n ...(await handle\n .select(holdRowSelection)\n .from(containerDocumentTombstoneHolds)\n .where(\n and(\n inArray(containerDocumentTombstoneHolds.documentId, batch),\n eq(containerDocumentTombstoneHolds.hidden, true),\n ),\n )\n .orderBy(\n asc(containerDocumentTombstoneHolds.documentId),\n asc(containerDocumentTombstoneHolds.containerId),\n )),\n );\n }\n return holds;\n}\n\n/**\n * Split the holds on the given containers into those whose placement is gone\n * (the document was purged, reset, or relinked locally: nothing left to hide,\n * so the hold is dropped) and those still hiding a placement.\n */\nexport async function partitionContainerDocumentTombstoneHolds(\n handle: ClientSQLiteTransactionScope,\n holds: ReadonlyArray<ContainerDocumentTombstoneHoldRow>,\n): Promise<{\n live: ContainerDocumentTombstoneHoldRow[];\n orphaned: ContainerDocumentTombstoneHoldRow[];\n}> {\n const known = new Set(\n (\n await listKnownContainerDocumentPlacementsInTransaction(handle, holds)\n ).map(placementKey),\n );\n return {\n live: holds.filter((hold) => known.has(placementKey(hold))),\n orphaned: holds.filter((hold) => !known.has(placementKey(hold))),\n };\n}\n"]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { and, asc, desc, eq, inArray } from "drizzle-orm";
|
|
2
2
|
import { DOCUMENTS_APP_KIND } from "../../../documents/documentConstants.js";
|
|
3
3
|
import { documentContainerProjection, documentProjection, documents, } from "../../../sqlite/schema/index.js";
|
|
4
|
+
import { sqliteRowsPerStatement } from "../../../sqlite/sqliteBindBudget.js";
|
|
4
5
|
import { getClientSQLitePersistenceRuntime, } from "../../../sqlite/sqlitePersistenceRuntime.js";
|
|
5
|
-
import {
|
|
6
|
+
import { deleteDocumentPlacementRowsInTransaction, deleteUnlinkedDocumentPlacementRowsInTransaction, } from "../../container-contents/documentContainerProjectionPersistence.js";
|
|
6
7
|
import { filterWritableDocumentPlacements } from "../../container-contents/documentPlacement.js";
|
|
7
8
|
import { getLatestTimestamp } from "../../latestTimestamp.js";
|
|
8
9
|
import { deleteContainerDocumentTombstoneHoldRows } from "./containerDocumentTombstoneHolds.js";
|
|
@@ -37,12 +38,8 @@ function buildContainerDocumentTombstoneState(uniqueTombstones) {
|
|
|
37
38
|
* signed link set never named. Rows the head links are kept as they are.
|
|
38
39
|
*/
|
|
39
40
|
async function deleteContainerDocumentTombstoneRows(tx, uniqueTombstones, verifiedLinkedContainerIdsByDocumentId) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
for (const [documentId, linked] of verifiedLinkedContainerIdsByDocumentId) {
|
|
44
|
-
await deleteUnlinkedDocumentPlacementRowsInTransaction(tx, documentId, linked);
|
|
45
|
-
}
|
|
41
|
+
await deleteDocumentPlacementRowsInTransaction(tx, uniqueTombstones);
|
|
42
|
+
await deleteUnlinkedDocumentPlacementRowsInTransaction(tx, verifiedLinkedContainerIdsByDocumentId);
|
|
46
43
|
}
|
|
47
44
|
/**
|
|
48
45
|
* The primary container after a removal: the first remaining local row the
|
|
@@ -55,8 +52,75 @@ async function deleteContainerDocumentTombstoneRows(tx, uniqueTombstones, verifi
|
|
|
55
52
|
function selectNextContainerId(remainingContainerIds, verifiedLinkedContainerIds) {
|
|
56
53
|
return (remainingContainerIds.find((containerId) => verifiedLinkedContainerIds.has(containerId)) ?? null);
|
|
57
54
|
}
|
|
55
|
+
function pushGrouped(map, key, value) {
|
|
56
|
+
const values = map.get(key);
|
|
57
|
+
if (values)
|
|
58
|
+
values.push(value);
|
|
59
|
+
else
|
|
60
|
+
map.set(key, [value]);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Every row the repair reads, for all tombstoned documents at once. The
|
|
64
|
+
* deletes above have already run, and the repair below writes only
|
|
65
|
+
* projection rows of the document it repairs, so reading before any repair
|
|
66
|
+
* sees what each per-document read saw.
|
|
67
|
+
*/
|
|
68
|
+
async function loadTombstoneRepairReads(tx, documentIds) {
|
|
69
|
+
const localIdsByDocumentId = new Map();
|
|
70
|
+
const projectionByLocalId = new Map();
|
|
71
|
+
const remainingLinksByDocumentId = new Map();
|
|
72
|
+
// The app kind binds one value; the rest carry ids.
|
|
73
|
+
const chunkSize = sqliteRowsPerStatement(1, 1);
|
|
74
|
+
for (let offset = 0; offset < documentIds.length; offset += chunkSize) {
|
|
75
|
+
const batch = documentIds.slice(offset, offset + chunkSize);
|
|
76
|
+
const localRows = await tx
|
|
77
|
+
.select({ documentId: documents.documentId, localId: documents.localId })
|
|
78
|
+
.from(documents)
|
|
79
|
+
.where(and(eq(documents.appKind, DOCUMENTS_APP_KIND), inArray(documents.documentId, batch)));
|
|
80
|
+
for (const row of localRows) {
|
|
81
|
+
if (row.documentId !== null) {
|
|
82
|
+
pushGrouped(localIdsByDocumentId, row.documentId, row.localId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const linkRows = await tx
|
|
86
|
+
.select({
|
|
87
|
+
containerId: documentContainerProjection.containerId,
|
|
88
|
+
documentId: documentContainerProjection.documentId,
|
|
89
|
+
})
|
|
90
|
+
.from(documentContainerProjection)
|
|
91
|
+
.where(inArray(documentContainerProjection.documentId, batch))
|
|
92
|
+
.orderBy(asc(documentContainerProjection.documentId), asc(documentContainerProjection.containerId));
|
|
93
|
+
for (const row of linkRows) {
|
|
94
|
+
pushGrouped(remainingLinksByDocumentId, row.documentId, row.containerId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const localIds = [...localIdsByDocumentId.values()].flat();
|
|
98
|
+
for (let offset = 0; offset < localIds.length; offset += chunkSize) {
|
|
99
|
+
const projectionRows = await tx
|
|
100
|
+
.select({
|
|
101
|
+
localId: documentProjection.localId,
|
|
102
|
+
containerId: documentProjection.containerId,
|
|
103
|
+
updatedAt: documentProjection.updatedAt,
|
|
104
|
+
})
|
|
105
|
+
.from(documentProjection)
|
|
106
|
+
.where(inArray(documentProjection.localId, localIds.slice(offset, offset + chunkSize)));
|
|
107
|
+
for (const row of projectionRows) {
|
|
108
|
+
if (row.localId !== null) {
|
|
109
|
+
projectionByLocalId.set(row.localId, {
|
|
110
|
+
containerId: row.containerId,
|
|
111
|
+
updatedAt: row.updatedAt,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
localIdsByDocumentId,
|
|
118
|
+
projectionByLocalId,
|
|
119
|
+
remainingLinksByDocumentId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
58
122
|
async function updateSelectedContainersForDocumentTombstones(input) {
|
|
59
|
-
const { documentId, removedContainerIds, tombstoneUpdatedAt, tx } = input;
|
|
123
|
+
const { documentId, reads, removedContainerIds, tombstoneUpdatedAt, tx } = input;
|
|
60
124
|
// A server document can own more than one local projection row: identity
|
|
61
125
|
// recovery legitimately rematerializes a second projection for the same
|
|
62
126
|
// documentId before semantic convergence completes. The link delete above is
|
|
@@ -65,39 +129,23 @@ async function updateSelectedContainersForDocumentTombstones(input) {
|
|
|
65
129
|
// at a removed container must be repaired here — resolving a single localId
|
|
66
130
|
// left a duplicate row stranded at the source, keeping a moved document
|
|
67
131
|
// visible in the container it was unlinked from.
|
|
68
|
-
const localIds = (
|
|
69
|
-
.select({ localId: documents.localId })
|
|
70
|
-
.from(documents)
|
|
71
|
-
.where(and(eq(documents.appKind, DOCUMENTS_APP_KIND), eq(documents.documentId, documentId)))).map((row) => row.localId);
|
|
72
|
-
if (localIds.length === 0) {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
const projectionRows = await tx
|
|
76
|
-
.select({
|
|
77
|
-
localId: documentProjection.localId,
|
|
78
|
-
containerId: documentProjection.containerId,
|
|
79
|
-
updatedAt: documentProjection.updatedAt,
|
|
80
|
-
})
|
|
81
|
-
.from(documentProjection)
|
|
82
|
-
.where(inArray(documentProjection.localId, localIds));
|
|
132
|
+
const localIds = reads.localIdsByDocumentId.get(documentId) ?? [];
|
|
83
133
|
// Every projection row whose primary container is tombstoned OR outside the
|
|
84
134
|
// verified link set is repointed: a primary at a listing-seeded row would
|
|
85
135
|
// otherwise keep showing the document in a container the head never named.
|
|
86
|
-
const rowsAtRemovedContainer =
|
|
87
|
-
row
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
136
|
+
const rowsAtRemovedContainer = localIds.flatMap((localId) => {
|
|
137
|
+
const row = reads.projectionByLocalId.get(localId);
|
|
138
|
+
return row !== undefined &&
|
|
139
|
+
row.containerId !== null &&
|
|
140
|
+
(removedContainerIds.has(row.containerId) ||
|
|
141
|
+
!input.verifiedLinkedContainerIds.has(row.containerId))
|
|
142
|
+
? [{ localId, updatedAt: row.updatedAt ?? undefined }]
|
|
143
|
+
: [];
|
|
144
|
+
});
|
|
92
145
|
if (rowsAtRemovedContainer.length === 0) {
|
|
93
146
|
return [];
|
|
94
147
|
}
|
|
95
|
-
const
|
|
96
|
-
.select({ containerId: documentContainerProjection.containerId })
|
|
97
|
-
.from(documentContainerProjection)
|
|
98
|
-
.where(eq(documentContainerProjection.documentId, documentId))
|
|
99
|
-
.orderBy(asc(documentContainerProjection.containerId));
|
|
100
|
-
const nextContainerId = selectNextContainerId(remainingLinkRows.map((row) => row.containerId), input.verifiedLinkedContainerIds);
|
|
148
|
+
const nextContainerId = selectNextContainerId(reads.remainingLinksByDocumentId.get(documentId) ?? [], input.verifiedLinkedContainerIds);
|
|
101
149
|
const changedLocalIds = [];
|
|
102
150
|
for (const row of rowsAtRemovedContainer) {
|
|
103
151
|
await tx
|
|
@@ -132,10 +180,14 @@ export async function applyContainerDocumentTombstonesWithExec(execSql, tombston
|
|
|
132
180
|
const { removedContainerIdsByDocumentId, tombstoneUpdatedAtByDocumentId, verifiedLinkedContainerIdsByDocumentId, } = buildContainerDocumentTombstoneState(applicable);
|
|
133
181
|
await deleteContainerDocumentTombstoneRows(tx, applicable, verifiedLinkedContainerIdsByDocumentId);
|
|
134
182
|
await deleteContainerDocumentTombstoneHoldRows(tx, applicable);
|
|
183
|
+
const reads = await loadTombstoneRepairReads(tx, [
|
|
184
|
+
...removedContainerIdsByDocumentId.keys(),
|
|
185
|
+
]);
|
|
135
186
|
const changedLocalIds = [];
|
|
136
187
|
for (const [documentId, removedContainerIds,] of removedContainerIdsByDocumentId) {
|
|
137
188
|
changedLocalIds.push(...(await updateSelectedContainersForDocumentTombstones({
|
|
138
189
|
documentId,
|
|
190
|
+
reads,
|
|
139
191
|
removedContainerIds,
|
|
140
192
|
tombstoneUpdatedAt: tombstoneUpdatedAtByDocumentId.get(documentId),
|
|
141
193
|
tx,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"containerDocumentTombstones.js","sourceRoot":"","sources":["containerDocumentTombstones.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, desc, eq, inArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../../documents/documentConstants\";\nimport {\n documentContainerProjection,\n documentProjection,\n documents,\n} from \"../../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport {\n deleteDocumentPlacementRowInTransaction,\n deleteUnlinkedDocumentPlacementRowsInTransaction,\n} from \"../../container-contents/documentContainerProjectionPersistence\";\nimport { filterWritableDocumentPlacements } from \"../../container-contents/documentPlacement\";\nimport { getLatestTimestamp } from \"../../latestTimestamp\";\nimport type { ContainerDocumentTombstoneInput } from \"../types\";\nimport { deleteContainerDocumentTombstoneHoldRows } from \"./containerDocumentTombstoneHolds\";\nimport {\n documentSummaryJoin,\n documentSummarySelection,\n mapDocumentSummary,\n} from \"./documentProjectionRows\";\n\nfunction dedupeContainerDocumentTombstones(\n tombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n): ContainerDocumentTombstoneInput[] {\n return Array.from(\n new Map(\n tombstones.map((tombstone) => [\n `${tombstone.documentId}\\u0000${tombstone.containerId}`,\n tombstone,\n ]),\n ).values(),\n );\n}\n\nfunction buildContainerDocumentTombstoneState(\n uniqueTombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n): {\n removedContainerIdsByDocumentId: Map<string, Set<string>>;\n tombstoneUpdatedAtByDocumentId: Map<string, string>;\n verifiedLinkedContainerIdsByDocumentId: Map<string, ReadonlySet<string>>;\n} {\n const removedContainerIdsByDocumentId = new Map<string, Set<string>>();\n const tombstoneUpdatedAtByDocumentId = new Map<string, string>();\n const verifiedLinkedContainerIdsByDocumentId = new Map<\n string,\n ReadonlySet<string>\n >();\n\n for (const tombstone of uniqueTombstones) {\n verifiedLinkedContainerIdsByDocumentId.set(\n tombstone.documentId,\n new Set(tombstone.linkedContainerIds),\n );\n const removedContainerIds =\n removedContainerIdsByDocumentId.get(tombstone.documentId) ?? new Set();\n removedContainerIds.add(tombstone.containerId);\n removedContainerIdsByDocumentId.set(\n tombstone.documentId,\n removedContainerIds,\n );\n tombstoneUpdatedAtByDocumentId.set(\n tombstone.documentId,\n getLatestTimestamp(\n tombstoneUpdatedAtByDocumentId.get(tombstone.documentId),\n tombstone.updatedAt,\n ),\n );\n }\n\n return {\n removedContainerIdsByDocumentId,\n tombstoneUpdatedAtByDocumentId,\n verifiedLinkedContainerIdsByDocumentId,\n };\n}\n\n/**\n * Remove the tombstoned placement and, with the head now verified, every\n * remaining local link row the head does not link: those are listing-seeded\n * rows, and leaving one would keep showing the document in a container the\n * signed link set never named. Rows the head links are kept as they are.\n */\nasync function deleteContainerDocumentTombstoneRows(\n tx: ClientSQLiteTransactionScope,\n uniqueTombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n verifiedLinkedContainerIdsByDocumentId: ReadonlyMap<\n string,\n ReadonlySet<string>\n >,\n): Promise<void> {\n for (const tombstone of uniqueTombstones) {\n await deleteDocumentPlacementRowInTransaction(tx, tombstone);\n }\n for (const [documentId, linked] of verifiedLinkedContainerIdsByDocumentId) {\n await deleteUnlinkedDocumentPlacementRowsInTransaction(\n tx,\n documentId,\n linked,\n );\n }\n}\n\n/**\n * The primary container after a removal: the first remaining local row the\n * verified head links. A remaining row the head does not link is a\n * listing-seeded row and never becomes the primary, so a listing cannot\n * re-home the document. With no such row the document is unplaced (`null`)\n * and reachable through orphan recovery, rather than pointed at a head\n * container this device may not be able to read.\n */\nfunction selectNextContainerId(\n remainingContainerIds: ReadonlyArray<string>,\n verifiedLinkedContainerIds: ReadonlySet<string>,\n): string | null {\n return (\n remainingContainerIds.find((containerId) =>\n verifiedLinkedContainerIds.has(containerId),\n ) ?? null\n );\n}\n\nasync function updateSelectedContainersForDocumentTombstones(input: {\n documentId: string;\n removedContainerIds: ReadonlySet<string>;\n tombstoneUpdatedAt: string | undefined;\n tx: ClientSQLiteTransactionScope;\n verifiedLinkedContainerIds: ReadonlySet<string>;\n}): Promise<string[]> {\n const { documentId, removedContainerIds, tombstoneUpdatedAt, tx } = input;\n // A server document can own more than one local projection row: identity\n // recovery legitimately rematerializes a second projection for the same\n // documentId before semantic convergence completes. The link delete above is\n // keyed by (documentId, containerId) and the container item view surfaces a\n // document by its primary container_id, so EVERY projection row still pointing\n // at a removed container must be repaired here — resolving a single localId\n // left a duplicate row stranded at the source, keeping a moved document\n // visible in the container it was unlinked from.\n const localIds = (\n await tx\n .select({ localId: documents.localId })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.documentId, documentId),\n ),\n )\n ).map((row) => row.localId);\n if (localIds.length === 0) {\n return [];\n }\n\n const projectionRows = await tx\n .select({\n localId: documentProjection.localId,\n containerId: documentProjection.containerId,\n updatedAt: documentProjection.updatedAt,\n })\n .from(documentProjection)\n .where(inArray(documentProjection.localId, localIds));\n // Every projection row whose primary container is tombstoned OR outside the\n // verified link set is repointed: a primary at a listing-seeded row would\n // otherwise keep showing the document in a container the head never named.\n const rowsAtRemovedContainer = projectionRows.flatMap((row) =>\n row.localId !== null &&\n row.containerId !== null &&\n (removedContainerIds.has(row.containerId) ||\n !input.verifiedLinkedContainerIds.has(row.containerId))\n ? [{ localId: row.localId, updatedAt: row.updatedAt ?? undefined }]\n : [],\n );\n if (rowsAtRemovedContainer.length === 0) {\n return [];\n }\n\n const remainingLinkRows = await tx\n .select({ containerId: documentContainerProjection.containerId })\n .from(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, documentId))\n .orderBy(asc(documentContainerProjection.containerId));\n const nextContainerId = selectNextContainerId(\n remainingLinkRows.map((row) => row.containerId),\n input.verifiedLinkedContainerIds,\n );\n\n const changedLocalIds: string[] = [];\n for (const row of rowsAtRemovedContainer) {\n await tx\n .update(documentProjection)\n .set({\n containerId: nextContainerId,\n updatedAt: getLatestTimestamp(row.updatedAt, tombstoneUpdatedAt),\n })\n .where(eq(documentProjection.localId, row.localId))\n .run();\n changedLocalIds.push(row.localId);\n }\n\n return changedLocalIds;\n}\n\nexport async function applyContainerDocumentTombstonesWithExec(\n execSql: ExecSql,\n tombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n) {\n const uniqueTombstones = dedupeContainerDocumentTombstones(tombstones);\n if (uniqueTombstones.length === 0) {\n return [];\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n\n return db.transaction(async (tx) => {\n // Besides the pending-intent guard, the verified head must not be older\n // than local document state: a lagging or replayed head must never\n // delete rows a newer listing or settled move wrote (`CheckEpoch`).\n const writable = await filterWritableDocumentPlacements(\n tx,\n uniqueTombstones.map((tombstone) => ({\n accessEpoch: tombstone.accessEpoch,\n documentId: tombstone.documentId,\n containerIds: [],\n })),\n );\n const writableIds = new Set(writable.map((input) => input.documentId));\n const applicable = uniqueTombstones.filter((tombstone) =>\n writableIds.has(tombstone.documentId),\n );\n const {\n removedContainerIdsByDocumentId,\n tombstoneUpdatedAtByDocumentId,\n verifiedLinkedContainerIdsByDocumentId,\n } = buildContainerDocumentTombstoneState(applicable);\n await deleteContainerDocumentTombstoneRows(\n tx,\n applicable,\n verifiedLinkedContainerIdsByDocumentId,\n );\n await deleteContainerDocumentTombstoneHoldRows(tx, applicable);\n\n const changedLocalIds: string[] = [];\n for (const [\n documentId,\n removedContainerIds,\n ] of removedContainerIdsByDocumentId) {\n changedLocalIds.push(\n ...(await updateSelectedContainersForDocumentTombstones({\n documentId,\n removedContainerIds,\n tombstoneUpdatedAt: tombstoneUpdatedAtByDocumentId.get(documentId),\n tx,\n verifiedLinkedContainerIds:\n verifiedLinkedContainerIdsByDocumentId.get(documentId) ?? new Set(),\n })),\n );\n }\n\n if (changedLocalIds.length === 0) {\n return [];\n }\n\n const rows = await tx\n .select(documentSummarySelection)\n .from(documentProjection)\n .leftJoin(documents, documentSummaryJoin)\n .where(inArray(documentProjection.localId, changedLocalIds))\n .orderBy(\n desc(documentProjection.updatedAt),\n desc(documentProjection.localId),\n );\n\n return rows.map(mapDocumentSummary);\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"containerDocumentTombstones.js","sourceRoot":"","sources":["containerDocumentTombstones.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, desc, eq, inArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../../documents/documentConstants\";\nimport {\n documentContainerProjection,\n documentProjection,\n documents,\n} from \"../../../sqlite/schema\";\nimport { sqliteRowsPerStatement } from \"../../../sqlite/sqliteBindBudget\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport {\n deleteDocumentPlacementRowsInTransaction,\n deleteUnlinkedDocumentPlacementRowsInTransaction,\n} from \"../../container-contents/documentContainerProjectionPersistence\";\nimport { filterWritableDocumentPlacements } from \"../../container-contents/documentPlacement\";\nimport { getLatestTimestamp } from \"../../latestTimestamp\";\nimport type { ContainerDocumentTombstoneInput } from \"../types\";\nimport { deleteContainerDocumentTombstoneHoldRows } from \"./containerDocumentTombstoneHolds\";\nimport {\n documentSummaryJoin,\n documentSummarySelection,\n mapDocumentSummary,\n} from \"./documentProjectionRows\";\n\nfunction dedupeContainerDocumentTombstones(\n tombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n): ContainerDocumentTombstoneInput[] {\n return Array.from(\n new Map(\n tombstones.map((tombstone) => [\n `${tombstone.documentId}\\u0000${tombstone.containerId}`,\n tombstone,\n ]),\n ).values(),\n );\n}\n\nfunction buildContainerDocumentTombstoneState(\n uniqueTombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n): {\n removedContainerIdsByDocumentId: Map<string, Set<string>>;\n tombstoneUpdatedAtByDocumentId: Map<string, string>;\n verifiedLinkedContainerIdsByDocumentId: Map<string, ReadonlySet<string>>;\n} {\n const removedContainerIdsByDocumentId = new Map<string, Set<string>>();\n const tombstoneUpdatedAtByDocumentId = new Map<string, string>();\n const verifiedLinkedContainerIdsByDocumentId = new Map<\n string,\n ReadonlySet<string>\n >();\n\n for (const tombstone of uniqueTombstones) {\n verifiedLinkedContainerIdsByDocumentId.set(\n tombstone.documentId,\n new Set(tombstone.linkedContainerIds),\n );\n const removedContainerIds =\n removedContainerIdsByDocumentId.get(tombstone.documentId) ?? new Set();\n removedContainerIds.add(tombstone.containerId);\n removedContainerIdsByDocumentId.set(\n tombstone.documentId,\n removedContainerIds,\n );\n tombstoneUpdatedAtByDocumentId.set(\n tombstone.documentId,\n getLatestTimestamp(\n tombstoneUpdatedAtByDocumentId.get(tombstone.documentId),\n tombstone.updatedAt,\n ),\n );\n }\n\n return {\n removedContainerIdsByDocumentId,\n tombstoneUpdatedAtByDocumentId,\n verifiedLinkedContainerIdsByDocumentId,\n };\n}\n\n/**\n * Remove the tombstoned placement and, with the head now verified, every\n * remaining local link row the head does not link: those are listing-seeded\n * rows, and leaving one would keep showing the document in a container the\n * signed link set never named. Rows the head links are kept as they are.\n */\nasync function deleteContainerDocumentTombstoneRows(\n tx: ClientSQLiteTransactionScope,\n uniqueTombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n verifiedLinkedContainerIdsByDocumentId: ReadonlyMap<\n string,\n ReadonlySet<string>\n >,\n): Promise<void> {\n await deleteDocumentPlacementRowsInTransaction(tx, uniqueTombstones);\n await deleteUnlinkedDocumentPlacementRowsInTransaction(\n tx,\n verifiedLinkedContainerIdsByDocumentId,\n );\n}\n\n/**\n * The primary container after a removal: the first remaining local row the\n * verified head links. A remaining row the head does not link is a\n * listing-seeded row and never becomes the primary, so a listing cannot\n * re-home the document. With no such row the document is unplaced (`null`)\n * and reachable through orphan recovery, rather than pointed at a head\n * container this device may not be able to read.\n */\nfunction selectNextContainerId(\n remainingContainerIds: ReadonlyArray<string>,\n verifiedLinkedContainerIds: ReadonlySet<string>,\n): string | null {\n return (\n remainingContainerIds.find((containerId) =>\n verifiedLinkedContainerIds.has(containerId),\n ) ?? null\n );\n}\n\ninterface TombstoneRepairReads {\n /** Local ids per document id. */\n readonly localIdsByDocumentId: ReadonlyMap<string, string[]>;\n readonly projectionByLocalId: ReadonlyMap<\n string,\n { containerId: string | null; updatedAt: string | null }\n >;\n /** Remaining link containers per document, in container id order. */\n readonly remainingLinksByDocumentId: ReadonlyMap<string, string[]>;\n}\n\nfunction pushGrouped(\n map: Map<string, string[]>,\n key: string,\n value: string,\n): void {\n const values = map.get(key);\n if (values) values.push(value);\n else map.set(key, [value]);\n}\n\n/**\n * Every row the repair reads, for all tombstoned documents at once. The\n * deletes above have already run, and the repair below writes only\n * projection rows of the document it repairs, so reading before any repair\n * sees what each per-document read saw.\n */\nasync function loadTombstoneRepairReads(\n tx: ClientSQLiteTransactionScope,\n documentIds: ReadonlyArray<string>,\n): Promise<TombstoneRepairReads> {\n const localIdsByDocumentId = new Map<string, string[]>();\n const projectionByLocalId = new Map<\n string,\n { containerId: string | null; updatedAt: string | null }\n >();\n const remainingLinksByDocumentId = new Map<string, string[]>();\n // The app kind binds one value; the rest carry ids.\n const chunkSize = sqliteRowsPerStatement(1, 1);\n for (let offset = 0; offset < documentIds.length; offset += chunkSize) {\n const batch = documentIds.slice(offset, offset + chunkSize);\n const localRows = await tx\n .select({ documentId: documents.documentId, 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 localRows) {\n if (row.documentId !== null) {\n pushGrouped(localIdsByDocumentId, row.documentId, row.localId);\n }\n }\n const linkRows = await tx\n .select({\n containerId: documentContainerProjection.containerId,\n documentId: documentContainerProjection.documentId,\n })\n .from(documentContainerProjection)\n .where(inArray(documentContainerProjection.documentId, batch))\n .orderBy(\n asc(documentContainerProjection.documentId),\n asc(documentContainerProjection.containerId),\n );\n for (const row of linkRows) {\n pushGrouped(remainingLinksByDocumentId, row.documentId, row.containerId);\n }\n }\n const localIds = [...localIdsByDocumentId.values()].flat();\n for (let offset = 0; offset < localIds.length; offset += chunkSize) {\n const projectionRows = await tx\n .select({\n localId: documentProjection.localId,\n containerId: documentProjection.containerId,\n updatedAt: documentProjection.updatedAt,\n })\n .from(documentProjection)\n .where(\n inArray(\n documentProjection.localId,\n localIds.slice(offset, offset + chunkSize),\n ),\n );\n for (const row of projectionRows) {\n if (row.localId !== null) {\n projectionByLocalId.set(row.localId, {\n containerId: row.containerId,\n updatedAt: row.updatedAt,\n });\n }\n }\n }\n return {\n localIdsByDocumentId,\n projectionByLocalId,\n remainingLinksByDocumentId,\n };\n}\n\nasync function updateSelectedContainersForDocumentTombstones(input: {\n documentId: string;\n reads: TombstoneRepairReads;\n removedContainerIds: ReadonlySet<string>;\n tombstoneUpdatedAt: string | undefined;\n tx: ClientSQLiteTransactionScope;\n verifiedLinkedContainerIds: ReadonlySet<string>;\n}): Promise<string[]> {\n const { documentId, reads, removedContainerIds, tombstoneUpdatedAt, tx } =\n input;\n // A server document can own more than one local projection row: identity\n // recovery legitimately rematerializes a second projection for the same\n // documentId before semantic convergence completes. The link delete above is\n // keyed by (documentId, containerId) and the container item view surfaces a\n // document by its primary container_id, so EVERY projection row still pointing\n // at a removed container must be repaired here — resolving a single localId\n // left a duplicate row stranded at the source, keeping a moved document\n // visible in the container it was unlinked from.\n const localIds = reads.localIdsByDocumentId.get(documentId) ?? [];\n // Every projection row whose primary container is tombstoned OR outside the\n // verified link set is repointed: a primary at a listing-seeded row would\n // otherwise keep showing the document in a container the head never named.\n const rowsAtRemovedContainer = localIds.flatMap((localId) => {\n const row = reads.projectionByLocalId.get(localId);\n return row !== undefined &&\n row.containerId !== null &&\n (removedContainerIds.has(row.containerId) ||\n !input.verifiedLinkedContainerIds.has(row.containerId))\n ? [{ localId, updatedAt: row.updatedAt ?? undefined }]\n : [];\n });\n if (rowsAtRemovedContainer.length === 0) {\n return [];\n }\n\n const nextContainerId = selectNextContainerId(\n reads.remainingLinksByDocumentId.get(documentId) ?? [],\n input.verifiedLinkedContainerIds,\n );\n\n const changedLocalIds: string[] = [];\n for (const row of rowsAtRemovedContainer) {\n await tx\n .update(documentProjection)\n .set({\n containerId: nextContainerId,\n updatedAt: getLatestTimestamp(row.updatedAt, tombstoneUpdatedAt),\n })\n .where(eq(documentProjection.localId, row.localId))\n .run();\n changedLocalIds.push(row.localId);\n }\n\n return changedLocalIds;\n}\n\nexport async function applyContainerDocumentTombstonesWithExec(\n execSql: ExecSql,\n tombstones: ReadonlyArray<ContainerDocumentTombstoneInput>,\n) {\n const uniqueTombstones = dedupeContainerDocumentTombstones(tombstones);\n if (uniqueTombstones.length === 0) {\n return [];\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n\n return db.transaction(async (tx) => {\n // Besides the pending-intent guard, the verified head must not be older\n // than local document state: a lagging or replayed head must never\n // delete rows a newer listing or settled move wrote (`CheckEpoch`).\n const writable = await filterWritableDocumentPlacements(\n tx,\n uniqueTombstones.map((tombstone) => ({\n accessEpoch: tombstone.accessEpoch,\n documentId: tombstone.documentId,\n containerIds: [],\n })),\n );\n const writableIds = new Set(writable.map((input) => input.documentId));\n const applicable = uniqueTombstones.filter((tombstone) =>\n writableIds.has(tombstone.documentId),\n );\n const {\n removedContainerIdsByDocumentId,\n tombstoneUpdatedAtByDocumentId,\n verifiedLinkedContainerIdsByDocumentId,\n } = buildContainerDocumentTombstoneState(applicable);\n await deleteContainerDocumentTombstoneRows(\n tx,\n applicable,\n verifiedLinkedContainerIdsByDocumentId,\n );\n await deleteContainerDocumentTombstoneHoldRows(tx, applicable);\n\n const reads = await loadTombstoneRepairReads(tx, [\n ...removedContainerIdsByDocumentId.keys(),\n ]);\n const changedLocalIds: string[] = [];\n for (const [\n documentId,\n removedContainerIds,\n ] of removedContainerIdsByDocumentId) {\n changedLocalIds.push(\n ...(await updateSelectedContainersForDocumentTombstones({\n documentId,\n reads,\n removedContainerIds,\n tombstoneUpdatedAt: tombstoneUpdatedAtByDocumentId.get(documentId),\n tx,\n verifiedLinkedContainerIds:\n verifiedLinkedContainerIdsByDocumentId.get(documentId) ?? new Set(),\n })),\n );\n }\n\n if (changedLocalIds.length === 0) {\n return [];\n }\n\n const rows = await tx\n .select(documentSummarySelection)\n .from(documentProjection)\n .leftJoin(documents, documentSummaryJoin)\n .where(inArray(documentProjection.localId, changedLocalIds))\n .orderBy(\n desc(documentProjection.updatedAt),\n desc(documentProjection.localId),\n );\n\n return rows.map(mapDocumentSummary);\n });\n}\n"]}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { DEFAULT_DOCUMENT_ACCESS_EPOCH } from "../../../documents/documentConstants.js";
|
|
2
|
-
import { loadDocumentMovePlacement } from "../../container-contents/documentPlacement.js";
|
|
2
|
+
import { loadDocumentMovePlacement, } from "../../container-contents/documentPlacement.js";
|
|
3
3
|
import { resolvePersistedAccessStateHash } from "./documentRuntimeState.js";
|
|
4
4
|
export async function resolveDiscoveredDocumentPlacement(tx, existing, input) {
|
|
5
|
-
|
|
5
|
+
return discoveredDocumentPlacement(existing, input, await loadDocumentMovePlacement(tx, input.documentId));
|
|
6
|
+
}
|
|
7
|
+
/** The placement discovery writes, given the document's pending move intent. */
|
|
8
|
+
export function discoveredDocumentPlacement(existing, input, intent) {
|
|
6
9
|
const stale = existing !== null && existing.accessEpoch > input.accessEpoch;
|
|
7
10
|
const accessEpoch = Math.max(existing?.accessEpoch ?? DEFAULT_DOCUMENT_ACCESS_EPOCH, input.accessEpoch);
|
|
8
11
|
const pendingContainerId = intent?.intentType === "document.link"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discoveredDocumentPlacement.js","sourceRoot":"","sources":["discoveredDocumentPlacement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAGrF,OAAO,
|
|
1
|
+
{"version":3,"file":"discoveredDocumentPlacement.js","sourceRoot":"","sources":["discoveredDocumentPlacement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAGrF,OAAO,EAEL,yBAAyB,GAC1B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,EAAgC,EAChC,QAAqC,EACrC,KAA8B;IAE9B,OAAO,2BAA2B,CAChC,QAAQ,EACR,KAAK,EACL,MAAM,yBAAyB,CAAC,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,CACtD,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,2BAA2B,CACzC,QAAqC,EACrC,KAA8B,EAC9B,MAAyC;IAEzC,MAAM,KAAK,GAAG,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;IAC5E,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAC1B,QAAQ,EAAE,WAAW,IAAI,6BAA6B,EACtD,KAAK,CAAC,WAAW,CAClB,CAAC;IACF,MAAM,kBAAkB,GACtB,MAAM,EAAE,UAAU,KAAK,eAAe;QACpC,CAAC,CAAC,CAAC,QAAQ,EAAE,WAAW,IAAI,MAAM,CAAC,iBAAiB,CAAC;QACrD,CAAC,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAChC,MAAM,WAAW,GACf,kBAAkB;QAClB,CAAC,KAAK;YACJ,CAAC,CAAC,QAAQ,CAAC,WAAW;YACtB,CAAC,CAAC,QAAQ,EAAE,WAAW;gBACnB,KAAK,CAAC,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC;gBACzD,CAAC,CAAC,QAAQ,CAAC,WAAW;gBACtB,CAAC,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,KAAK,CAAC,WAAW,CAAC;oBAChE,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC;oBAC3B,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC5B,OAAO;QACL,WAAW;QACX,WAAW;QACX,eAAe,EAAE,+BAA+B,CAAC,QAAQ,EAAE;YACzD,WAAW;YACX,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe;YACzE,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["import { DEFAULT_DOCUMENT_ACCESS_EPOCH } from \"../../../documents/documentConstants\";\nimport type { DiscoveredDocumentInput } from \"../../../documents/documentSummary\";\nimport type { ClientSQLiteTransactionScope } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type DocumentMovePlacement,\n loadDocumentMovePlacement,\n} from \"../../container-contents/documentPlacement\";\nimport type { StoredDocumentRecord } from \"../types\";\nimport { resolvePersistedAccessStateHash } from \"./documentRuntimeState\";\n\nexport async function resolveDiscoveredDocumentPlacement(\n tx: ClientSQLiteTransactionScope,\n existing: StoredDocumentRecord | null,\n input: DiscoveredDocumentInput,\n) {\n return discoveredDocumentPlacement(\n existing,\n input,\n await loadDocumentMovePlacement(tx, input.documentId),\n );\n}\n\n/** The placement discovery writes, given the document's pending move intent. */\nexport function discoveredDocumentPlacement(\n existing: StoredDocumentRecord | null,\n input: DiscoveredDocumentInput,\n intent: DocumentMovePlacement | undefined,\n) {\n const stale = existing !== null && existing.accessEpoch > input.accessEpoch;\n const accessEpoch = Math.max(\n existing?.accessEpoch ?? DEFAULT_DOCUMENT_ACCESS_EPOCH,\n input.accessEpoch,\n );\n const pendingContainerId =\n intent?.intentType === \"document.link\"\n ? (existing?.containerId ?? intent.targetContainerId)\n : intent?.targetContainerId;\n const containerId =\n pendingContainerId ??\n (stale\n ? existing.containerId\n : existing?.containerId &&\n input.linkedContainerIds.includes(existing.containerId)\n ? existing.containerId\n : (input.linkedContainerIds.find((id) => id === input.containerId) ??\n input.linkedContainerIds[0] ??\n input.containerId));\n return {\n accessEpoch,\n containerId,\n accessStateHash: resolvePersistedAccessStateHash(existing, {\n accessEpoch,\n accessStateHash: stale ? existing.accessStateHash : input.accessStateHash,\n documentId: input.documentId,\n }),\n };\n}\n"]}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { KeyingVerificationError } from "../../../../internal/crypto/index.js";
|
|
2
|
+
import { DEFAULT_DOCUMENT_KIND, DOCUMENTS_APP_KIND, } from "../../../documents/documentConstants.js";
|
|
3
|
+
import { loadDocumentMovePlacements, } from "../../container-contents/documentPlacement.js";
|
|
4
|
+
import { hasDocumentPurgeCheckpointInTransaction, listPurgedDocumentIdsInTransaction, } from "../../keyingCheckpoints/documentPurgeCheckpointPersistence.js";
|
|
5
|
+
import { findLocalIdByDocumentId, findLocalIdsByDocumentIds } from "../records/index.js";
|
|
6
|
+
import { discoveredDocumentPlacement, resolveDiscoveredDocumentPlacement, } from "./discoveredDocumentPlacement.js";
|
|
7
|
+
import { toDocumentSummary } from "./documentProjectionRows.js";
|
|
8
|
+
import { documentSaveTimestampFrom, resolveDocumentSaveTimestamp, saveDocumentRows, saveDocumentRowsBatch, } from "./documentRows.js";
|
|
9
|
+
import { resolvePersistedDocumentRuntimeState } from "./documentRuntimeState.js";
|
|
10
|
+
import { loadPendingCreateSummary } from "./pendingCreateAdoption.js";
|
|
11
|
+
import { loadStoredDocumentState, loadStoredDocumentStates, } from "./rowQueryPersistence.js";
|
|
12
|
+
const DISCOVERED_DOCUMENT_PLACEHOLDER_TITLE = "Syncing document...";
|
|
13
|
+
function purgedDocumentError() {
|
|
14
|
+
return new KeyingVerificationError("rollback", "Document discovery targets a permanently purged document");
|
|
15
|
+
}
|
|
16
|
+
function fenceSkippedError() {
|
|
17
|
+
return new Error("Document recovery generation changed during discovery");
|
|
18
|
+
}
|
|
19
|
+
function nextDiscoveredDocument(localId, input, existingDocument, placement) {
|
|
20
|
+
return {
|
|
21
|
+
id: localId,
|
|
22
|
+
documentId: input.documentId,
|
|
23
|
+
documentKind: existingDocument?.documentKind ?? DEFAULT_DOCUMENT_KIND,
|
|
24
|
+
text: existingDocument?.text ?? "",
|
|
25
|
+
title: existingDocument?.title ?? DISCOVERED_DOCUMENT_PLACEHOLDER_TITLE,
|
|
26
|
+
snapshotEndVersion: existingDocument?.snapshotEndVersion ?? "",
|
|
27
|
+
recoveryGeneration: existingDocument?.recoveryGeneration ?? 0,
|
|
28
|
+
...placement,
|
|
29
|
+
effectiveAccessLevel: input.effectiveAccessLevel ??
|
|
30
|
+
existingDocument?.effectiveAccessLevel ??
|
|
31
|
+
null,
|
|
32
|
+
...resolvePersistedDocumentRuntimeState(existingDocument, {
|
|
33
|
+
accessEpoch: placement.accessEpoch,
|
|
34
|
+
documentId: input.documentId,
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** One discovered document, read and written on its own (the per-row path). */
|
|
39
|
+
async function upsertDiscoveredDocumentWithExec(execSql, input, pendingCreates, tx) {
|
|
40
|
+
if (await hasDocumentPurgeCheckpointInTransaction(tx, input.documentId)) {
|
|
41
|
+
throw purgedDocumentError();
|
|
42
|
+
}
|
|
43
|
+
const existingLocalId = await findLocalIdByDocumentId(execSql, DOCUMENTS_APP_KIND, input.documentId);
|
|
44
|
+
const localId = existingLocalId ?? input.documentId;
|
|
45
|
+
const pendingLocalId = pendingCreates.get(input.documentId);
|
|
46
|
+
if (!existingLocalId && pendingLocalId) {
|
|
47
|
+
const pending = await loadPendingCreateSummary(execSql, pendingLocalId);
|
|
48
|
+
if (!pending)
|
|
49
|
+
throw new Error("Pending create projection is unavailable");
|
|
50
|
+
return pending;
|
|
51
|
+
}
|
|
52
|
+
const existingDocument = (await loadStoredDocumentState(execSql, localId))?.record ?? null;
|
|
53
|
+
const placement = await resolveDiscoveredDocumentPlacement(tx, existingDocument, input);
|
|
54
|
+
const nextDocument = nextDiscoveredDocument(localId, input, existingDocument, placement);
|
|
55
|
+
const updatedAt = await resolveDocumentSaveTimestamp({
|
|
56
|
+
document: nextDocument,
|
|
57
|
+
options: existingDocument ? undefined : { updatedAt: input.createdAt },
|
|
58
|
+
tx,
|
|
59
|
+
});
|
|
60
|
+
if (!(await saveDocumentRows({ document: nextDocument, tx, updatedAt }))) {
|
|
61
|
+
throw fenceSkippedError();
|
|
62
|
+
}
|
|
63
|
+
return toDocumentSummary(nextDocument, updatedAt);
|
|
64
|
+
}
|
|
65
|
+
async function upsertDiscoveredDocumentsPerRow(execSql, inputs, pendingCreates, tx) {
|
|
66
|
+
const summaries = [];
|
|
67
|
+
for (const input of inputs) {
|
|
68
|
+
summaries.push(await upsertDiscoveredDocumentWithExec(execSql, input, pendingCreates, tx));
|
|
69
|
+
}
|
|
70
|
+
return summaries;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolves every input to its pending-create summary or its target local id,
|
|
74
|
+
* in input order, so the first purged or unavailable input throws exactly as
|
|
75
|
+
* the per-row walk would. Returns null when one input's write could change
|
|
76
|
+
* another input's reads, which only the per-row walk handles.
|
|
77
|
+
*/
|
|
78
|
+
async function resolveDiscoveryTargets(execSql, inputs, pendingCreates, tx) {
|
|
79
|
+
const documentIds = inputs.map((input) => input.documentId);
|
|
80
|
+
const purged = await listPurgedDocumentIdsInTransaction(tx, documentIds);
|
|
81
|
+
const existingLocalIds = await findLocalIdsByDocumentIds(execSql, DOCUMENTS_APP_KIND, documentIds);
|
|
82
|
+
const targets = [];
|
|
83
|
+
const pendingLocalIds = new Set();
|
|
84
|
+
for (const input of inputs) {
|
|
85
|
+
if (purged.has(input.documentId))
|
|
86
|
+
throw purgedDocumentError();
|
|
87
|
+
const existingLocalId = existingLocalIds.get(input.documentId);
|
|
88
|
+
const pendingLocalId = pendingCreates.get(input.documentId);
|
|
89
|
+
if (!existingLocalId && pendingLocalId) {
|
|
90
|
+
const pending = await loadPendingCreateSummary(execSql, pendingLocalId);
|
|
91
|
+
if (!pending)
|
|
92
|
+
throw new Error("Pending create projection is unavailable");
|
|
93
|
+
pendingLocalIds.add(pendingLocalId);
|
|
94
|
+
targets.push({ kind: "pending", summary: pending });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
targets.push({
|
|
98
|
+
input,
|
|
99
|
+
kind: "write",
|
|
100
|
+
localId: existingLocalId ?? input.documentId,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const writeLocalIds = targets.flatMap((target) => target.kind === "write" ? [target.localId] : []);
|
|
104
|
+
const independent = new Set(writeLocalIds).size === writeLocalIds.length &&
|
|
105
|
+
!writeLocalIds.some((localId) => pendingLocalIds.has(localId));
|
|
106
|
+
return independent ? targets : null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether writing each target leaves every other target's reads unchanged: a
|
|
110
|
+
* stored row at a target local id that carries a different document id would
|
|
111
|
+
* be re-pointed by its write, which another input's lookup could have read.
|
|
112
|
+
*/
|
|
113
|
+
function targetsReadDisjointRows(targets, states) {
|
|
114
|
+
return targets.every((target) => target.kind !== "write" ||
|
|
115
|
+
(states.get(target.localId)?.record.documentId ??
|
|
116
|
+
target.input.documentId) === target.input.documentId);
|
|
117
|
+
}
|
|
118
|
+
function plannedDiscoveryWrite(target, state, intent) {
|
|
119
|
+
const existingDocument = state?.record ?? null;
|
|
120
|
+
const document = nextDiscoveredDocument(target.localId, target.input, existingDocument, discoveredDocumentPlacement(existingDocument, target.input, intent));
|
|
121
|
+
// The joined state read is the row and projection the per-row walk reads
|
|
122
|
+
// again for its timestamp; nothing earlier in this transaction writes it.
|
|
123
|
+
const updatedAt = state
|
|
124
|
+
? documentSaveTimestampFrom({
|
|
125
|
+
projection: state.projection,
|
|
126
|
+
snapshotEndVersion: state.record.snapshotEndVersion,
|
|
127
|
+
}, document)
|
|
128
|
+
: target.input.createdAt;
|
|
129
|
+
return { document, updatedAt };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Discovery as set statements: one read per table for every input, then
|
|
133
|
+
* multi-row upserts. Inputs whose rows could interact (a repeated document
|
|
134
|
+
* id, a shared local id, or a row another input would re-point) keep the
|
|
135
|
+
* per-row walk, which reads each input after the previous one's writes.
|
|
136
|
+
*/
|
|
137
|
+
export async function upsertDiscoveredDocumentsInTransaction(execSql, inputs, pendingCreates, tx) {
|
|
138
|
+
const documentIds = inputs.map((input) => input.documentId);
|
|
139
|
+
if (new Set(documentIds).size !== documentIds.length) {
|
|
140
|
+
return upsertDiscoveredDocumentsPerRow(execSql, inputs, pendingCreates, tx);
|
|
141
|
+
}
|
|
142
|
+
const targets = await resolveDiscoveryTargets(execSql, inputs, pendingCreates, tx);
|
|
143
|
+
if (!targets) {
|
|
144
|
+
return upsertDiscoveredDocumentsPerRow(execSql, inputs, pendingCreates, tx);
|
|
145
|
+
}
|
|
146
|
+
const writes = targets.flatMap((target) => target.kind === "write" ? [target] : []);
|
|
147
|
+
if (writes.length === 0) {
|
|
148
|
+
return targets.flatMap((target) => target.kind === "pending" ? [target.summary] : []);
|
|
149
|
+
}
|
|
150
|
+
const states = await loadStoredDocumentStates(execSql, writes.map((target) => target.localId));
|
|
151
|
+
if (!targetsReadDisjointRows(targets, states)) {
|
|
152
|
+
return upsertDiscoveredDocumentsPerRow(execSql, inputs, pendingCreates, tx);
|
|
153
|
+
}
|
|
154
|
+
const intents = await loadDocumentMovePlacements(tx, writes.map((target) => target.input.documentId));
|
|
155
|
+
const planned = new Map(writes.map((target) => [
|
|
156
|
+
target.localId,
|
|
157
|
+
plannedDiscoveryWrite(target, states.get(target.localId), intents.get(target.input.documentId)),
|
|
158
|
+
]));
|
|
159
|
+
const saved = await saveDocumentRowsBatch({
|
|
160
|
+
entries: [...planned.values()],
|
|
161
|
+
tx,
|
|
162
|
+
});
|
|
163
|
+
if (saved.size !== planned.size)
|
|
164
|
+
throw fenceSkippedError();
|
|
165
|
+
return targets.map((target) => {
|
|
166
|
+
if (target.kind === "pending")
|
|
167
|
+
return target.summary;
|
|
168
|
+
const write = planned.get(target.localId);
|
|
169
|
+
if (!write)
|
|
170
|
+
throw new Error("Discovered document write is missing");
|
|
171
|
+
return toDocumentSummary(write.document, write.updatedAt);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=discoveredDocumentUpsert.js.map
|