@tearleads/client-sdk 0.1.112 → 0.1.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/data/persistence/container-contents/containerContentsPersistence.js +3 -8
  2. package/data/persistence/container-contents/containerContentsPersistence.js.map +1 -1
  3. package/data/persistence/container-contents/containerHydrationPersistence.js +15 -5
  4. package/data/persistence/container-contents/containerHydrationPersistence.js.map +1 -1
  5. package/data/persistence/container-contents/containerMetadataMutationPersistence.js +8 -2
  6. package/data/persistence/container-contents/containerMetadataMutationPersistence.js.map +1 -1
  7. package/data/persistence/container-contents/containerMetadataRows.js +34 -2
  8. package/data/persistence/container-contents/containerMetadataRows.js.map +1 -1
  9. package/data/persistence/container-contents/containerPersistence.d.ts +231 -0
  10. package/data/persistence/container-contents/containerPersistence.js +20 -16
  11. package/data/persistence/container-contents/containerPersistence.js.map +1 -1
  12. package/data/persistence/container-contents/containerRecoveryPersistence.js +15 -13
  13. package/data/persistence/container-contents/containerRecoveryPersistence.js.map +1 -1
  14. package/data/persistence/container-contents/documentContainerProjectionPersistence.js +10 -2
  15. package/data/persistence/container-contents/documentContainerProjectionPersistence.js.map +1 -1
  16. package/data/persistence/container-contents/documentMoveIntentPersistence.js +29 -5
  17. package/data/persistence/container-contents/documentMoveIntentPersistence.js.map +1 -1
  18. package/data/persistence/container-contents/documentPlacementIntentEnqueue.js +1 -1
  19. package/data/persistence/container-contents/documentPlacementIntentEnqueue.js.map +1 -1
  20. package/data/persistence/documents/internal/documentMutationPersistence.js +15 -15
  21. package/data/persistence/documents/internal/documentMutationPersistence.js.map +1 -1
  22. package/data/persistence/documents/internal/documentRows.js +26 -10
  23. package/data/persistence/documents/internal/documentRows.js.map +1 -1
  24. package/data/persistence/documents/internal/rowQueryPersistence.js +52 -27
  25. package/data/persistence/documents/internal/rowQueryPersistence.js.map +1 -1
  26. package/data/persistence/documents/records/documentHistoryPersistence.js +17 -11
  27. package/data/persistence/documents/records/documentHistoryPersistence.js.map +1 -1
  28. package/data/persistence/keyingCheckpoints/keyingCheckpointAdvancePersistence.js +16 -11
  29. package/data/persistence/keyingCheckpoints/keyingCheckpointAdvancePersistence.js.map +1 -1
  30. package/data/persistence/keyingCheckpoints/keyingCheckpointPersistence.js +150 -2
  31. package/data/persistence/keyingCheckpoints/keyingCheckpointPersistence.js.map +1 -1
  32. package/data/persistence/keyingCheckpoints/principalAuthorizationCheckpoints.js +3 -2
  33. package/data/persistence/keyingCheckpoints/principalAuthorizationCheckpoints.js.map +1 -1
  34. package/data/persistence/principalHistory/principalHistoryEvidencePersistence.d.ts +14 -1
  35. package/data/persistence/principalHistory/principalHistoryEvidencePersistence.js +128 -34
  36. package/data/persistence/principalHistory/principalHistoryEvidencePersistence.js.map +1 -1
  37. package/data/persistence/principalHistory/principalHistoryNodeRetention.js +64 -24
  38. package/data/persistence/principalHistory/principalHistoryNodeRetention.js.map +1 -1
  39. package/data/persistence/principalHistory/principalHistoryPrefixPersistence.js +6 -1
  40. package/data/persistence/principalHistory/principalHistoryPrefixPersistence.js.map +1 -1
  41. package/data/persistence/principalHistory/principalHistoryRootOwnership.d.ts +45 -0
  42. package/data/persistence/principalHistory/principalHistoryRootOwnership.js +41 -4
  43. package/data/persistence/principalHistory/principalHistoryRootOwnership.js.map +1 -1
  44. package/data/persistence/principalHistory/principalHistoryStagePersistence.js +12 -8
  45. package/data/persistence/principalHistory/principalHistoryStagePersistence.js.map +1 -1
  46. package/data/persistence/principalKeys/principalGrantRetirementPersistence.js +35 -1
  47. package/data/persistence/principalKeys/principalGrantRetirementPersistence.js.map +1 -1
  48. package/data/persistence/principalPolicy/principalPolicyOrganizationPersistence.js +62 -1
  49. package/data/persistence/principalPolicy/principalPolicyOrganizationPersistence.js.map +1 -1
  50. package/data/persistence/trustedUserIdentityPinPersistence.js +46 -23
  51. package/data/persistence/trustedUserIdentityPinPersistence.js.map +1 -1
  52. package/data/persistence/verifiedManifestBundlePersistence.js +27 -10
  53. package/data/persistence/verifiedManifestBundlePersistence.js.map +1 -1
  54. package/data/sqlite/schema/principalHistoryNodeRetentionSchema.d.ts +222 -0
  55. package/package.json +1 -1
  56. package/workflows/container-contents/documentQueries/index.js +21 -6
  57. package/workflows/container-contents/documentQueries/index.js.map +1 -1
  58. package/workflows/documents/documentMutationCommit.js +11 -12
  59. package/workflows/documents/documentMutationCommit.js.map +1 -1
  60. package/workflows/documents/documentMutationPreparation.d.ts +1 -0
  61. package/workflows/documents/documentMutationPreparation.js +1 -0
  62. package/workflows/documents/documentMutationPreparation.js.map +1 -1
  63. package/workflows/documents/documentPersistGuards.js +24 -0
  64. package/workflows/documents/documentPersistGuards.js.map +1 -1
  65. package/workflows/principals/historyRecovery/loadRecoveredPrincipalHistoryPage.js +13 -12
  66. package/workflows/principals/historyRecovery/loadRecoveredPrincipalHistoryPage.js.map +1 -1
  67. package/workflows/principals/historyRecovery/principalHistoryRecoveryReferences.js +17 -19
  68. package/workflows/principals/historyRecovery/principalHistoryRecoveryReferences.js.map +1 -1
  69. package/workflows/principals/publicHistory/selectPublicPrincipalHistory.js +8 -10
  70. package/workflows/principals/publicHistory/selectPublicPrincipalHistory.js.map +1 -1
@@ -1,11 +1,60 @@
1
- import { desc, eq, notInArray } from "drizzle-orm";
1
+ import { and, desc, eq, notInArray } from "drizzle-orm";
2
2
  import { DOCUMENTS_APP_KIND } from "../../../documents/documentConstants.js";
3
3
  import { HIDDEN_DOCUMENT_SUMMARY_KINDS } from "../../../documents/documentSummary.js";
4
4
  import { documentProjection, documentProjectionText, documents, } from "../../../sqlite/schema/index.js";
5
5
  import { getClientSQLitePersistenceRuntime } from "../../../sqlite/sqlitePersistenceRuntime.js";
6
- import { findLocalIdByDocumentId, loadDocumentRecord } from "../records/index.js";
6
+ import { documentRecordSelection, findLocalIdByDocumentId, loadDocumentRecord, mapSelectedDocumentRecord, } from "../records/index.js";
7
7
  import { documentSummaryJoin, documentSummarySelection, getProjectionContainerId, getProjectionDocumentKind, getProjectionText, getProjectionTitle, mapDocumentSummary, } from "./documentProjectionRows.js";
8
8
  import { getDocumentScope, hasDocumentRow } from "./documentRows.js";
9
+ /**
10
+ * The canonical record and its projection from ONE joined statement, so the
11
+ * pair comes from one snapshot and a save inside the same transaction can
12
+ * reuse the projection instead of reading it again.
13
+ */
14
+ export async function loadStoredDocumentState(execSql, localId) {
15
+ const { db } = getClientSQLitePersistenceRuntime(execSql);
16
+ const [row] = await db
17
+ .select({
18
+ ...documentRecordSelection,
19
+ projectionContainerId: documentProjection.containerId,
20
+ projectionDocumentKind: documentProjection.documentKind,
21
+ projectionText: documentProjectionText.text,
22
+ projectionTitle: documentProjection.title,
23
+ projectionUpdatedAt: documentProjection.updatedAt,
24
+ })
25
+ .from(documents)
26
+ .leftJoin(documentProjection, eq(documentProjection.localId, documents.localId))
27
+ .leftJoin(documentProjectionText, eq(documentProjectionText.localId, documentProjection.localId))
28
+ .where(and(eq(documents.appKind, DOCUMENTS_APP_KIND), eq(documents.localId, localId)))
29
+ .limit(1);
30
+ if (!row)
31
+ return null;
32
+ const { projectionContainerId, projectionDocumentKind, projectionText, projectionTitle, projectionUpdatedAt, ...recordRow } = row;
33
+ // Projection columns are NOT NULL, so they are null exactly when the
34
+ // projection row is absent.
35
+ const projectionRow = projectionDocumentKind !== null &&
36
+ projectionTitle !== null &&
37
+ projectionUpdatedAt !== null
38
+ ? {
39
+ containerId: projectionContainerId,
40
+ documentKind: projectionDocumentKind,
41
+ text: projectionText,
42
+ title: projectionTitle,
43
+ }
44
+ : undefined;
45
+ return {
46
+ projection: projectionRow && projectionUpdatedAt !== null
47
+ ? { ...projectionRow, updatedAt: projectionUpdatedAt }
48
+ : null,
49
+ record: {
50
+ ...mapSelectedDocumentRecord(recordRow),
51
+ containerId: getProjectionContainerId(projectionRow),
52
+ documentKind: getProjectionDocumentKind(projectionRow),
53
+ text: getProjectionText(projectionRow),
54
+ title: getProjectionTitle(projectionRow),
55
+ },
56
+ };
57
+ }
9
58
  export const documentRowQueryPersistence = {
10
59
  async listDocuments(execSql) {
11
60
  const { db } = getClientSQLitePersistenceRuntime(execSql);
@@ -48,31 +97,7 @@ export const documentRowQueryPersistence = {
48
97
  (record.recoveryGeneration ?? 0) === expectedRecoveryGeneration);
49
98
  },
50
99
  async loadDocument(execSql, localId) {
51
- const { db } = getClientSQLitePersistenceRuntime(execSql);
52
- const [documentRecord, projectionRows] = await Promise.all([
53
- loadDocumentRecord(execSql, getDocumentScope(localId)),
54
- db
55
- .select({
56
- documentKind: documentProjection.documentKind,
57
- text: documentProjectionText.text,
58
- title: documentProjection.title,
59
- containerId: documentProjection.containerId,
60
- })
61
- .from(documentProjection)
62
- .leftJoin(documentProjectionText, eq(documentProjectionText.localId, documentProjection.localId))
63
- .where(eq(documentProjection.localId, localId))
64
- .limit(1),
65
- ]);
66
- if (!documentRecord) {
67
- return null;
68
- }
69
- return {
70
- ...documentRecord,
71
- containerId: getProjectionContainerId(projectionRows[0]),
72
- documentKind: getProjectionDocumentKind(projectionRows[0]),
73
- text: getProjectionText(projectionRows[0]),
74
- title: getProjectionTitle(projectionRows[0]),
75
- };
100
+ return (await loadStoredDocumentState(execSql, localId))?.record ?? null;
76
101
  },
77
102
  async loadDocumentContainer(execSql, localId) {
78
103
  const { db } = getClientSQLitePersistenceRuntime(execSql);
@@ -1 +1 @@
1
- {"version":3,"file":"rowQueryPersistence.js","sourceRoot":"","sources":["rowQueryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { desc, eq, notInArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../../documents/documentConstants\";\nimport { HIDDEN_DOCUMENT_SUMMARY_KINDS } from \"../../../documents/documentSummary\";\nimport {\n documentProjection,\n documentProjectionText,\n documents,\n} from \"../../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport { findLocalIdByDocumentId, loadDocumentRecord } from \"../records\";\nimport type { DocumentsPersistence } from \"../types\";\nimport {\n documentSummaryJoin,\n documentSummarySelection,\n getProjectionContainerId,\n getProjectionDocumentKind,\n getProjectionText,\n getProjectionTitle,\n mapDocumentSummary,\n} from \"./documentProjectionRows\";\nimport { getDocumentScope, hasDocumentRow } from \"./documentRows\";\n\ntype DocumentRowQueryPersistence = Pick<\n DocumentsPersistence,\n | \"listDocuments\"\n | \"findDocumentLocalIdsByContainerId\"\n | \"findLocalIdByDocumentId\"\n | \"hasDocument\"\n | \"documentIdentityMatches\"\n | \"loadDocument\"\n | \"loadDocumentContainer\"\n>;\n\nexport const documentRowQueryPersistence: DocumentRowQueryPersistence = {\n async listDocuments(execSql) {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select(documentSummarySelection)\n .from(documentProjection)\n .leftJoin(documents, documentSummaryJoin)\n .where(\n notInArray(documentProjection.documentKind, [\n ...HIDDEN_DOCUMENT_SUMMARY_KINDS,\n ]),\n )\n .orderBy(\n desc(documentProjection.updatedAt),\n desc(documentProjection.localId),\n );\n\n return rows.map(mapDocumentSummary);\n },\n async findDocumentLocalIdsByContainerId(execSql, containerId) {\n // Unlike `listDocumentsByContainerIdsOrDocumentIds`, this deliberately does\n // NOT drop `HIDDEN_DOCUMENT_SUMMARY_KINDS` — it exists to reach the hidden\n // `organization_profile` document by the container it is linked to, which is\n // how a *foreign* org's display name is found on a member who synced the doc\n // under its server documentId rather than the provisioner-only local alias.\n // `documentProjection` is written and deleted in lockstep with the\n // `documents` rows (all DOCUMENTS_APP_KIND), so filtering by containerId\n // alone is sufficient — no join to `documents` is needed to select localId.\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({ localId: documentProjection.localId })\n .from(documentProjection)\n .where(eq(documentProjection.containerId, containerId))\n .orderBy(\n desc(documentProjection.updatedAt),\n desc(documentProjection.localId),\n );\n return rows\n .map((row) => row.localId)\n .filter((localId): localId is string => localId !== null);\n },\n findLocalIdByDocumentId: (execSql, documentId) =>\n findLocalIdByDocumentId(execSql, DOCUMENTS_APP_KIND, documentId),\n async hasDocument(execSql, localId) {\n return hasDocumentRow(execSql, localId);\n },\n async documentIdentityMatches(\n execSql,\n localId,\n expectedDocumentId,\n expectedRecoveryGeneration,\n ) {\n const record = await loadDocumentRecord(execSql, getDocumentScope(localId));\n return (\n record?.documentId === expectedDocumentId &&\n (record.recoveryGeneration ?? 0) === expectedRecoveryGeneration\n );\n },\n async loadDocument(execSql, localId) {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const [documentRecord, projectionRows] = await Promise.all([\n loadDocumentRecord(execSql, getDocumentScope(localId)),\n db\n .select({\n documentKind: documentProjection.documentKind,\n text: documentProjectionText.text,\n title: documentProjection.title,\n containerId: documentProjection.containerId,\n })\n .from(documentProjection)\n .leftJoin(\n documentProjectionText,\n eq(documentProjectionText.localId, documentProjection.localId),\n )\n .where(eq(documentProjection.localId, localId))\n .limit(1),\n ]);\n\n if (!documentRecord) {\n return null;\n }\n\n return {\n ...documentRecord,\n containerId: getProjectionContainerId(projectionRows[0]),\n documentKind: getProjectionDocumentKind(projectionRows[0]),\n text: getProjectionText(projectionRows[0]),\n title: getProjectionTitle(projectionRows[0]),\n };\n },\n async loadDocumentContainer(execSql, localId) {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n // Select the row itself, not just its container, so an existing row with a\n // null container is reported as `{ containerId: null }` while a missing row\n // is reported as `undefined` — the caller relies on that distinction to know\n // whether the projection has authoritative placement to defer to.\n const projectionRows = await db\n .select({ containerId: documentProjection.containerId })\n .from(documentProjection)\n .where(eq(documentProjection.localId, localId))\n .limit(1);\n const projectionRow = projectionRows[0];\n if (!projectionRow) {\n return undefined;\n }\n\n return { containerId: projectionRow.containerId };\n },\n};\n"]}
1
+ {"version":3,"file":"rowQueryPersistence.js","sourceRoot":"","sources":["rowQueryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, desc, eq, notInArray } from \"drizzle-orm\";\nimport { DOCUMENTS_APP_KIND } from \"../../../documents/documentConstants\";\nimport type { StoredDocumentKind } from \"../../../documents/documentKinds\";\nimport { HIDDEN_DOCUMENT_SUMMARY_KINDS } from \"../../../documents/documentSummary\";\nimport {\n documentProjection,\n documentProjectionText,\n documents,\n} from \"../../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport {\n documentRecordSelection,\n findLocalIdByDocumentId,\n loadDocumentRecord,\n mapSelectedDocumentRecord,\n} from \"../records\";\nimport type { DocumentsPersistence, StoredDocumentRecord } from \"../types\";\nimport {\n documentSummaryJoin,\n documentSummarySelection,\n getProjectionContainerId,\n getProjectionDocumentKind,\n getProjectionText,\n getProjectionTitle,\n mapDocumentSummary,\n} from \"./documentProjectionRows\";\nimport { getDocumentScope, hasDocumentRow } from \"./documentRows\";\n\n/** The projection columns a stored record was loaded with, if any. */\nexport interface StoredDocumentProjectionState {\n documentKind: StoredDocumentKind;\n text: string | null;\n title: string;\n updatedAt: string;\n}\n\n/**\n * The canonical record and its projection from ONE joined statement, so the\n * pair comes from one snapshot and a save inside the same transaction can\n * reuse the projection instead of reading it again.\n */\nexport async function loadStoredDocumentState(\n execSql: Parameters<DocumentsPersistence[\"loadDocument\"]>[0],\n localId: string,\n): Promise<{\n projection: StoredDocumentProjectionState | null;\n record: StoredDocumentRecord;\n} | null> {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const [row] = await db\n .select({\n ...documentRecordSelection,\n projectionContainerId: documentProjection.containerId,\n projectionDocumentKind: documentProjection.documentKind,\n projectionText: documentProjectionText.text,\n projectionTitle: documentProjection.title,\n projectionUpdatedAt: documentProjection.updatedAt,\n })\n .from(documents)\n .leftJoin(\n documentProjection,\n eq(documentProjection.localId, documents.localId),\n )\n .leftJoin(\n documentProjectionText,\n eq(documentProjectionText.localId, documentProjection.localId),\n )\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.localId, localId),\n ),\n )\n .limit(1);\n if (!row) return null;\n const {\n projectionContainerId,\n projectionDocumentKind,\n projectionText,\n projectionTitle,\n projectionUpdatedAt,\n ...recordRow\n } = row;\n // Projection columns are NOT NULL, so they are null exactly when the\n // projection row is absent.\n const projectionRow =\n projectionDocumentKind !== null &&\n projectionTitle !== null &&\n projectionUpdatedAt !== null\n ? {\n containerId: projectionContainerId,\n documentKind: projectionDocumentKind,\n text: projectionText,\n title: projectionTitle,\n }\n : undefined;\n return {\n projection:\n projectionRow && projectionUpdatedAt !== null\n ? { ...projectionRow, updatedAt: projectionUpdatedAt }\n : null,\n record: {\n ...mapSelectedDocumentRecord(recordRow),\n containerId: getProjectionContainerId(projectionRow),\n documentKind: getProjectionDocumentKind(projectionRow),\n text: getProjectionText(projectionRow),\n title: getProjectionTitle(projectionRow),\n },\n };\n}\n\ntype DocumentRowQueryPersistence = Pick<\n DocumentsPersistence,\n | \"listDocuments\"\n | \"findDocumentLocalIdsByContainerId\"\n | \"findLocalIdByDocumentId\"\n | \"hasDocument\"\n | \"documentIdentityMatches\"\n | \"loadDocument\"\n | \"loadDocumentContainer\"\n>;\n\nexport const documentRowQueryPersistence: DocumentRowQueryPersistence = {\n async listDocuments(execSql) {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select(documentSummarySelection)\n .from(documentProjection)\n .leftJoin(documents, documentSummaryJoin)\n .where(\n notInArray(documentProjection.documentKind, [\n ...HIDDEN_DOCUMENT_SUMMARY_KINDS,\n ]),\n )\n .orderBy(\n desc(documentProjection.updatedAt),\n desc(documentProjection.localId),\n );\n\n return rows.map(mapDocumentSummary);\n },\n async findDocumentLocalIdsByContainerId(execSql, containerId) {\n // Unlike `listDocumentsByContainerIdsOrDocumentIds`, this deliberately does\n // NOT drop `HIDDEN_DOCUMENT_SUMMARY_KINDS` — it exists to reach the hidden\n // `organization_profile` document by the container it is linked to, which is\n // how a *foreign* org's display name is found on a member who synced the doc\n // under its server documentId rather than the provisioner-only local alias.\n // `documentProjection` is written and deleted in lockstep with the\n // `documents` rows (all DOCUMENTS_APP_KIND), so filtering by containerId\n // alone is sufficient — no join to `documents` is needed to select localId.\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({ localId: documentProjection.localId })\n .from(documentProjection)\n .where(eq(documentProjection.containerId, containerId))\n .orderBy(\n desc(documentProjection.updatedAt),\n desc(documentProjection.localId),\n );\n return rows\n .map((row) => row.localId)\n .filter((localId): localId is string => localId !== null);\n },\n findLocalIdByDocumentId: (execSql, documentId) =>\n findLocalIdByDocumentId(execSql, DOCUMENTS_APP_KIND, documentId),\n async hasDocument(execSql, localId) {\n return hasDocumentRow(execSql, localId);\n },\n async documentIdentityMatches(\n execSql,\n localId,\n expectedDocumentId,\n expectedRecoveryGeneration,\n ) {\n const record = await loadDocumentRecord(execSql, getDocumentScope(localId));\n return (\n record?.documentId === expectedDocumentId &&\n (record.recoveryGeneration ?? 0) === expectedRecoveryGeneration\n );\n },\n async loadDocument(execSql, localId) {\n return (await loadStoredDocumentState(execSql, localId))?.record ?? null;\n },\n async loadDocumentContainer(execSql, localId) {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n // Select the row itself, not just its container, so an existing row with a\n // null container is reported as `{ containerId: null }` while a missing row\n // is reported as `undefined` — the caller relies on that distinction to know\n // whether the projection has authoritative placement to defer to.\n const projectionRows = await db\n .select({ containerId: documentProjection.containerId })\n .from(documentProjection)\n .where(eq(documentProjection.localId, localId))\n .limit(1);\n const projectionRow = projectionRows[0];\n if (!projectionRow) {\n return undefined;\n }\n\n return { containerId: projectionRow.containerId };\n },\n};\n"]}
@@ -1,9 +1,11 @@
1
1
  import { base64ToBytes } from "../../../../internal/encoding/index.js";
2
2
  import { createDocument, exportFullHistoryIdentity, importSnapshot, satisfiesVersionVector, } from "../../../../internal/loro/index.js";
3
- import { and, eq, inArray, isNull, ne, sql } from "drizzle-orm";
3
+ import { and, eq, exists, inArray, isNull, ne, sql } from "drizzle-orm";
4
4
  import { documentHistoryCheckpoints, documentHistoryUpdates, documentTables, } from "../../../sqlite/schema/index.js";
5
+ import { sqliteInsertBatchSize } from "../../../sqlite/sqliteBindBudget.js";
5
6
  import { getClientSQLitePersistenceRuntime } from "../../../sqlite/sqlitePersistenceRuntime.js";
6
7
  import { ensureSqlTables } from "../../../sqlite/sqlSchema.js";
8
+ const HISTORY_INSERT_BATCH_SIZE = sqliteInsertBatchSize(documentHistoryUpdates);
7
9
  /**
8
10
  * Compaction thresholds: once the tail carries this many rows or bytes, the
9
11
  * next persist of a full-history-capable document exports a fresh checkpoint
@@ -35,10 +37,12 @@ export async function appendDocumentHistoryUpdates(execSql, scope, updates, orig
35
37
  await ensureSqlTables(execSql, documentTables);
36
38
  const createdAt = new Date().toISOString();
37
39
  await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {
38
- // Chunked: a catch-up pull can carry thousands of updates, and a single
39
- // multi-row insert would exceed SQLite's bound-variable limit.
40
- for (let index = 0; index < updates.length; index += HISTORY_ROW_CHUNK) {
41
- await db.insert(documentHistoryUpdates).values(updates.slice(index, index + HISTORY_ROW_CHUNK).map((updateData) => ({
40
+ // A pull page carries at most 64 updates, which fits one statement; the
41
+ // chunking keeps any longer append within SQLite's bind budget.
42
+ for (let index = 0; index < updates.length; index += HISTORY_INSERT_BATCH_SIZE) {
43
+ await db.insert(documentHistoryUpdates).values(updates
44
+ .slice(index, index + HISTORY_INSERT_BATCH_SIZE)
45
+ .map((updateData) => ({
42
46
  id: crypto.randomUUID(),
43
47
  appKind: scope.appKind,
44
48
  localId: scope.localId,
@@ -106,21 +110,22 @@ export async function loadDocumentHistoryRestoreState(execSql, scope) {
106
110
  export async function readDocumentHistoryTailSize(execSql, scope) {
107
111
  await ensureSqlTables(execSql, documentTables);
108
112
  const { db } = getClientSQLitePersistenceRuntime(execSql);
113
+ // One statement: an aggregate without GROUP BY always returns one row, and
114
+ // the checkpoint probe rides along as an EXISTS column.
109
115
  const [totals] = await db
110
116
  .select({
111
117
  byteLength: sql `coalesce(sum(length(${documentHistoryUpdates.updateData})), 0)`,
118
+ hasCheckpoint: sql `${exists(db
119
+ .select({ localId: documentHistoryCheckpoints.localId })
120
+ .from(documentHistoryCheckpoints)
121
+ .where(and(eq(documentHistoryCheckpoints.appKind, scope.appKind), eq(documentHistoryCheckpoints.localId, scope.localId))))}`,
112
122
  rowCount: sql `count(*)`,
113
123
  })
114
124
  .from(documentHistoryUpdates)
115
125
  .where(compactableTailInScope(scope));
116
- const [checkpoint] = await db
117
- .select({ localId: documentHistoryCheckpoints.localId })
118
- .from(documentHistoryCheckpoints)
119
- .where(and(eq(documentHistoryCheckpoints.appKind, scope.appKind), eq(documentHistoryCheckpoints.localId, scope.localId)))
120
- .limit(1);
121
126
  return {
122
127
  byteLength: Number(totals?.byteLength ?? 0),
123
- hasCheckpoint: checkpoint !== undefined,
128
+ hasCheckpoint: Number(totals?.hasCheckpoint ?? 0) !== 0,
124
129
  rowCount: Number(totals?.rowCount ?? 0),
125
130
  };
126
131
  }
@@ -142,6 +147,7 @@ export async function listDocumentHistoryTailEntries(execSql, scope) {
142
147
  .where(compactableTailInScope(scope));
143
148
  return rows.flatMap((row) => row.id === null ? [] : [{ id: row.id, updateData: row.updateData }]);
144
149
  }
150
+ /** Covered-tail ids per delete; the scope adds only a few binds. */
145
151
  const HISTORY_ROW_CHUNK = 400;
146
152
  /**
147
153
  * Replace the scope's checkpoint with a fresh full-history snapshot and
@@ -1 +1 @@
1
- {"version":3,"file":"documentHistoryPersistence.js","sourceRoot":"","sources":["documentHistoryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { base64ToBytes } from \"@tearleads/encoding\";\nimport {\n createDocument,\n exportFullHistoryIdentity,\n importSnapshot,\n satisfiesVersionVector,\n} from \"@tearleads/loro\";\nimport { and, eq, inArray, isNull, ne, sql } from \"drizzle-orm\";\nimport {\n documentHistoryCheckpoints,\n documentHistoryUpdates,\n documentTables,\n} from \"../../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../../sqlite/sqlSchema\";\nimport type { DocumentScope } from \"./documentPersistenceTypes\";\n\n/**\n * Compaction thresholds: once the tail carries this many rows or bytes, the\n * next persist of a full-history-capable document exports a fresh checkpoint\n * and clears the tail. Small enough to keep restore replay short, large\n * enough that a full-history export (O(history)) happens once per burst of\n * edits instead of per keystroke.\n */\nexport const DOCUMENT_HISTORY_COMPACTION_MAX_ROWS = 200;\nexport const DOCUMENT_HISTORY_COMPACTION_MAX_BYTES = 512 * 1024;\n\n/**\n * Row provenance: \"local\" for updates authored on this device, \"remote\" for\n * decrypted pulled updates. Restores advance the outgoing-delta marker across\n * remote rows (already server-side); local rows never advance it.\n */\ntype DocumentHistoryUpdateOrigin = \"local\" | \"remote\";\n\n/**\n * Rows of the update log that are history tail: every row except an \"outbox\"\n * row, which only waits to be sent (see `documentHistoryUpdates`).\n */\nfunction tailInScope(scope: DocumentScope) {\n return and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ne(documentHistoryUpdates.origin, \"outbox\"),\n );\n}\n\n/**\n * Tail rows compaction may fold into a checkpoint and delete: a queued row is\n * the only durable copy of an unsent update, so it waits for settlement even\n * when a checkpoint already covers its operations.\n */\nfunction compactableTailInScope(scope: DocumentScope) {\n return and(tailInScope(scope), isNull(documentHistoryUpdates.updateId));\n}\n\ninterface DocumentHistoryTailUpdate {\n readonly origin: DocumentHistoryUpdateOrigin;\n readonly updateData: string;\n}\n\ninterface DocumentHistoryRestoreState {\n readonly snapshot: string;\n readonly tailUpdates: readonly DocumentHistoryTailUpdate[];\n}\n\nexport async function appendDocumentHistoryUpdates(\n execSql: ExecSql,\n scope: DocumentScope,\n updates: readonly string[],\n origin: DocumentHistoryUpdateOrigin,\n): Promise<void> {\n if (updates.length === 0) {\n return;\n }\n await ensureSqlTables(execSql, documentTables);\n const createdAt = new Date().toISOString();\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {\n // Chunked: a catch-up pull can carry thousands of updates, and a single\n // multi-row insert would exceed SQLite's bound-variable limit.\n for (let index = 0; index < updates.length; index += HISTORY_ROW_CHUNK) {\n await db.insert(documentHistoryUpdates).values(\n updates.slice(index, index + HISTORY_ROW_CHUNK).map((updateData) => ({\n id: crypto.randomUUID(),\n appKind: scope.appKind,\n localId: scope.localId,\n updateData,\n origin,\n createdAt,\n })),\n );\n }\n });\n}\n\n/**\n * The checkpoint + ordered tail for a scope, or null when NEITHER exists (a\n * discovered shell that has not hydrated content yet; the caller starts the\n * document empty and the remote pull supplies the content). A tail WITHOUT a\n * checkpoint — a crash between the first enqueued edit's tail append and the\n * birth-checkpoint seed — restores as tail-only (`snapshot: \"\"`): those tail\n * rows can be the only durable copy of a local edit, so they must never be\n * silently ignored.\n */\nexport async function loadDocumentHistoryRestoreState(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<DocumentHistoryRestoreState | null> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n // Read the TAIL before the (full) checkpoint: if another pane compacts\n // between the two reads, this order yields old-tail + new-checkpoint — a\n // safe superset (the new checkpoint subsumes the old tail, and replay is\n // idempotent by op identity). The reverse order could yield old-checkpoint\n // + already-emptied tail, a torn read that silently drops the compacted\n // ops for the whole session.\n const tail = await db\n .select({\n id: documentHistoryUpdates.id,\n origin: documentHistoryUpdates.origin,\n updateData: documentHistoryUpdates.updateData,\n })\n .from(documentHistoryUpdates)\n .where(tailInScope(scope))\n // Insertion order: same-millisecond appends make createdAt tie and a\n // random-uuid tiebreak would replay out of order. importUpdates tolerates\n // out-of-order batches, but replay should still be deterministic.\n .orderBy(sql`rowid`);\n\n const [checkpoint] = await db\n .select({ snapshot: documentHistoryCheckpoints.snapshot })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n if (!checkpoint && tail.length === 0) {\n return null;\n }\n\n return {\n snapshot: checkpoint?.snapshot ?? \"\",\n tailUpdates: tail.map((row) => ({\n // Fail toward re-sending: an unrecognized origin never advances the\n // marker, which is the recoverable direction (a redundant re-submit\n // is idempotent; a skipped local op would be orphaned from sync).\n origin:\n row.origin === \"remote\" ? (\"remote\" as const) : (\"local\" as const),\n updateData: row.updateData,\n })),\n };\n}\n\n/**\n * Size of the tail compaction can shrink. Queued rows are excluded: they stay\n * until settlement, so counting them would re-export full history on every\n * persist while offline edits wait to sync.\n */\nexport async function readDocumentHistoryTailSize(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<{ byteLength: number; hasCheckpoint: boolean; rowCount: number }> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const [totals] = await db\n .select({\n byteLength: sql<number>`coalesce(sum(length(${documentHistoryUpdates.updateData})), 0)`,\n rowCount: sql<number>`count(*)`,\n })\n .from(documentHistoryUpdates)\n .where(compactableTailInScope(scope));\n const [checkpoint] = await db\n .select({ localId: documentHistoryCheckpoints.localId })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n return {\n byteLength: Number(totals?.byteLength ?? 0),\n hasCheckpoint: checkpoint !== undefined,\n rowCount: Number(totals?.rowCount ?? 0),\n };\n}\n\ninterface DocumentHistoryTailEntry {\n readonly id: string;\n readonly updateData: string;\n}\n\n/**\n * The current compactable tail rows WITH their payloads, so a compactor can\n * prove each row's ops are covered by the snapshot it is about to install\n * (another pane's append may carry ops this pane's document has not merged\n * yet). Queued rows are never candidates.\n */\nexport async function listDocumentHistoryTailEntries(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<DocumentHistoryTailEntry[]> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n id: documentHistoryUpdates.id,\n updateData: documentHistoryUpdates.updateData,\n })\n .from(documentHistoryUpdates)\n .where(compactableTailInScope(scope));\n return rows.flatMap((row) =>\n row.id === null ? [] : [{ id: row.id, updateData: row.updateData }],\n );\n}\n\nconst HISTORY_ROW_CHUNK = 400;\n\n/**\n * Replace the scope's checkpoint with a fresh full-history snapshot and\n * delete ONLY the tail rows the caller captured before exporting it. Every\n * append path writes its tail row after the ops are already in the live\n * document, so any row captured before the export is provably covered by\n * the snapshot — while a row appended concurrently (between export and this\n * delete) survives for the next compaction instead of being lost.\n */\nexport async function replaceDocumentHistoryCheckpoint(\n execSql: ExecSql,\n scope: DocumentScope,\n input: {\n coveredTailIds: readonly string[];\n endVersionVector: string;\n /**\n * Skip the monotonic version gate. Only for document CREATION, where any\n * existing checkpoint under this localId is the orphan of a failed prior\n * attempt (checkpoint written, record row never committed) — a retry's\n * fresh document need not dominate that orphan to replace it.\n */\n force?: boolean;\n snapshot: string;\n /**\n * Re-checked after acquiring the serialized mutation slot (like the\n * record-save path): a store reset/recreation that happened while this\n * call waited in the queue must abort the write, or a stale compactor\n * could install the OLD document's checkpoint over the new identity's.\n */\n stillCurrent?: () => boolean;\n },\n): Promise<boolean> {\n await ensureSqlTables(execSql, documentTables);\n // Sequential (non-transactional) so callers already inside a transaction\n // can nest this. Crash between the statements leaves the new checkpoint\n // plus covered tail rows — a safe superset, since tail replay is\n // idempotent by op identity. The replacement itself is a compare-and-swap\n // on the stored revision, so the version gate cannot be bypassed by a\n // concurrent compactor between the read and the write: whoever loses the\n // CAS re-reads and re-evaluates the gate against the winner's checkpoint.\n return getClientSQLitePersistenceRuntime(execSql).runMutation(async (tx) => {\n if (input.stillCurrent?.() === false) {\n return false;\n }\n for (let attempt = 0; attempt < CHECKPOINT_CAS_ATTEMPTS; attempt += 1) {\n let replaced: boolean;\n try {\n replaced = await tryReplaceCheckpointOnce(tx, scope, input);\n } catch (error) {\n if (error instanceof CheckpointGateRejected) {\n // A fresher checkpoint dominates this candidate: no write, and\n // the caller's covered rows must stay for the covering pane.\n return false;\n }\n throw error;\n }\n if (!replaced) {\n // CAS lost to a concurrent writer — re-read and re-evaluate the\n // gate against the winner's checkpoint.\n continue;\n }\n for (\n let index = 0;\n index < input.coveredTailIds.length;\n index += HISTORY_ROW_CHUNK\n ) {\n // Never a queued row, even if a caller names one: its operations may\n // be in the checkpoint, but it is still the unsent update itself.\n await tx\n .delete(documentHistoryUpdates)\n .where(\n and(\n compactableTailInScope(scope),\n inArray(\n documentHistoryUpdates.id,\n input.coveredTailIds.slice(index, index + HISTORY_ROW_CHUNK),\n ),\n ),\n );\n }\n return true;\n }\n // Exhaustion must not read as durable success: creation and rebuild\n // callers persist record rows assuming the checkpoint landed.\n throw new Error(\n \"Document history checkpoint replacement lost every CAS attempt\",\n );\n });\n}\n\nconst CHECKPOINT_CAS_ATTEMPTS = 16;\n\ntype MutationDb = Parameters<\n Parameters<\n ReturnType<typeof getClientSQLitePersistenceRuntime>[\"runMutation\"]\n >[0]\n>[0];\n\n/**\n * One compare-and-swap attempt. Returns true when this caller's checkpoint\n * became the stored one, false when a concurrent writer won the CAS (the\n * caller re-reads and retries), and throws CheckpointGateRejected when a\n * fresher stored checkpoint dominates the candidate.\n */\nasync function tryReplaceCheckpointOnce(\n tx: MutationDb,\n scope: DocumentScope,\n input: {\n endVersionVector: string;\n force?: boolean;\n snapshot: string;\n },\n): Promise<boolean> {\n const updatedAt = new Date().toISOString();\n const revision = crypto.randomUUID();\n const [stored] = await tx\n .select({\n endVersionVector: documentHistoryCheckpoints.endVersionVector,\n revision: documentHistoryCheckpoints.revision,\n snapshot: documentHistoryCheckpoints.snapshot,\n })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n\n if (!stored) {\n const inserted = await tx\n .insert(documentHistoryCheckpoints)\n .values({\n appKind: scope.appKind,\n localId: scope.localId,\n snapshot: input.snapshot,\n endVersionVector: input.endVersionVector,\n revision,\n updatedAt,\n })\n .onConflictDoNothing({\n target: [\n documentHistoryCheckpoints.appKind,\n documentHistoryCheckpoints.localId,\n ],\n })\n .returning({ localId: documentHistoryCheckpoints.localId });\n // Conflict means a concurrent writer inserted first — retry the gate.\n return inserted.length > 0;\n }\n\n // A stale compactor (a pane whose document has not merged another pane's\n // ops) must never replace a checkpoint it does not cover: the covered\n // pane may already have deleted its tail rows, so regressing the\n // checkpoint would leave those ops with no durable copy at all. Vector\n // dominance proves counter coverage; the retained-prefix identity also\n // proves the candidate carries the exact stored operations at those\n // counters rather than a same-frontier fork.\n if (input.force !== true) {\n if (\n !satisfiesVersionVector(input.endVersionVector, stored.endVersionVector)\n ) {\n throw new CheckpointGateRejected();\n }\n if (\n input.snapshot !== stored.snapshot &&\n !(await candidateContainsStoredCheckpointHistory({\n candidateSnapshot: input.snapshot,\n storedEndVersion: stored.endVersionVector,\n storedSnapshot: stored.snapshot,\n }))\n ) {\n throw new CheckpointGateRejected();\n }\n }\n\n const replaced = await tx\n .update(documentHistoryCheckpoints)\n .set({\n snapshot: input.snapshot,\n endVersionVector: input.endVersionVector,\n revision,\n updatedAt,\n })\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n eq(documentHistoryCheckpoints.revision, stored.revision),\n ),\n )\n .returning({ localId: documentHistoryCheckpoints.localId });\n return replaced.length > 0;\n}\n\nasync function candidateContainsStoredCheckpointHistory(input: {\n candidateSnapshot: string;\n storedEndVersion: string;\n storedSnapshot: string;\n}): Promise<boolean> {\n const [candidateDocument, storedDocument] = await Promise.all([\n createDocument(\"checkpoint-candidate-history-gate\"),\n createDocument(\"checkpoint-stored-history-gate\"),\n ]);\n try {\n importSnapshot(candidateDocument, base64ToBytes(input.candidateSnapshot));\n importSnapshot(storedDocument, base64ToBytes(input.storedSnapshot));\n return (\n exportFullHistoryIdentity(candidateDocument, input.storedEndVersion) ===\n exportFullHistoryIdentity(storedDocument)\n );\n } finally {\n candidateDocument.free();\n storedDocument.free();\n }\n}\n\nclass CheckpointGateRejected extends Error {\n constructor() {\n super(\"checkpoint gate rejected\");\n this.name = \"CheckpointGateRejected\";\n }\n}\n\nexport async function deleteDocumentHistory(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<void> {\n await ensureSqlTables(execSql, documentTables);\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (tx) => {\n await tx\n .delete(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n );\n await tx\n .delete(documentHistoryUpdates)\n .where(\n and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ),\n );\n });\n}\n"]}
1
+ {"version":3,"file":"documentHistoryPersistence.js","sourceRoot":"","sources":["documentHistoryPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { base64ToBytes } from \"@tearleads/encoding\";\nimport {\n createDocument,\n exportFullHistoryIdentity,\n importSnapshot,\n satisfiesVersionVector,\n} from \"@tearleads/loro\";\nimport { and, eq, exists, inArray, isNull, ne, sql } from \"drizzle-orm\";\nimport {\n documentHistoryCheckpoints,\n documentHistoryUpdates,\n documentTables,\n} from \"../../../sqlite/schema\";\nimport { sqliteInsertBatchSize } from \"../../../sqlite/sqliteBindBudget\";\nimport { getClientSQLitePersistenceRuntime } from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../../sqlite/sqlSchema\";\nimport type { DocumentScope } from \"./documentPersistenceTypes\";\n\nconst HISTORY_INSERT_BATCH_SIZE = sqliteInsertBatchSize(documentHistoryUpdates);\n\n/**\n * Compaction thresholds: once the tail carries this many rows or bytes, the\n * next persist of a full-history-capable document exports a fresh checkpoint\n * and clears the tail. Small enough to keep restore replay short, large\n * enough that a full-history export (O(history)) happens once per burst of\n * edits instead of per keystroke.\n */\nexport const DOCUMENT_HISTORY_COMPACTION_MAX_ROWS = 200;\nexport const DOCUMENT_HISTORY_COMPACTION_MAX_BYTES = 512 * 1024;\n\n/**\n * Row provenance: \"local\" for updates authored on this device, \"remote\" for\n * decrypted pulled updates. Restores advance the outgoing-delta marker across\n * remote rows (already server-side); local rows never advance it.\n */\ntype DocumentHistoryUpdateOrigin = \"local\" | \"remote\";\n\n/**\n * Rows of the update log that are history tail: every row except an \"outbox\"\n * row, which only waits to be sent (see `documentHistoryUpdates`).\n */\nfunction tailInScope(scope: DocumentScope) {\n return and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ne(documentHistoryUpdates.origin, \"outbox\"),\n );\n}\n\n/**\n * Tail rows compaction may fold into a checkpoint and delete: a queued row is\n * the only durable copy of an unsent update, so it waits for settlement even\n * when a checkpoint already covers its operations.\n */\nfunction compactableTailInScope(scope: DocumentScope) {\n return and(tailInScope(scope), isNull(documentHistoryUpdates.updateId));\n}\n\ninterface DocumentHistoryTailUpdate {\n readonly origin: DocumentHistoryUpdateOrigin;\n readonly updateData: string;\n}\n\ninterface DocumentHistoryRestoreState {\n readonly snapshot: string;\n readonly tailUpdates: readonly DocumentHistoryTailUpdate[];\n}\n\nexport async function appendDocumentHistoryUpdates(\n execSql: ExecSql,\n scope: DocumentScope,\n updates: readonly string[],\n origin: DocumentHistoryUpdateOrigin,\n): Promise<void> {\n if (updates.length === 0) {\n return;\n }\n await ensureSqlTables(execSql, documentTables);\n const createdAt = new Date().toISOString();\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {\n // A pull page carries at most 64 updates, which fits one statement; the\n // chunking keeps any longer append within SQLite's bind budget.\n for (\n let index = 0;\n index < updates.length;\n index += HISTORY_INSERT_BATCH_SIZE\n ) {\n await db.insert(documentHistoryUpdates).values(\n updates\n .slice(index, index + HISTORY_INSERT_BATCH_SIZE)\n .map((updateData) => ({\n id: crypto.randomUUID(),\n appKind: scope.appKind,\n localId: scope.localId,\n updateData,\n origin,\n createdAt,\n })),\n );\n }\n });\n}\n\n/**\n * The checkpoint + ordered tail for a scope, or null when NEITHER exists (a\n * discovered shell that has not hydrated content yet; the caller starts the\n * document empty and the remote pull supplies the content). A tail WITHOUT a\n * checkpoint — a crash between the first enqueued edit's tail append and the\n * birth-checkpoint seed — restores as tail-only (`snapshot: \"\"`): those tail\n * rows can be the only durable copy of a local edit, so they must never be\n * silently ignored.\n */\nexport async function loadDocumentHistoryRestoreState(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<DocumentHistoryRestoreState | null> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n // Read the TAIL before the (full) checkpoint: if another pane compacts\n // between the two reads, this order yields old-tail + new-checkpoint — a\n // safe superset (the new checkpoint subsumes the old tail, and replay is\n // idempotent by op identity). The reverse order could yield old-checkpoint\n // + already-emptied tail, a torn read that silently drops the compacted\n // ops for the whole session.\n const tail = await db\n .select({\n id: documentHistoryUpdates.id,\n origin: documentHistoryUpdates.origin,\n updateData: documentHistoryUpdates.updateData,\n })\n .from(documentHistoryUpdates)\n .where(tailInScope(scope))\n // Insertion order: same-millisecond appends make createdAt tie and a\n // random-uuid tiebreak would replay out of order. importUpdates tolerates\n // out-of-order batches, but replay should still be deterministic.\n .orderBy(sql`rowid`);\n\n const [checkpoint] = await db\n .select({ snapshot: documentHistoryCheckpoints.snapshot })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n if (!checkpoint && tail.length === 0) {\n return null;\n }\n\n return {\n snapshot: checkpoint?.snapshot ?? \"\",\n tailUpdates: tail.map((row) => ({\n // Fail toward re-sending: an unrecognized origin never advances the\n // marker, which is the recoverable direction (a redundant re-submit\n // is idempotent; a skipped local op would be orphaned from sync).\n origin:\n row.origin === \"remote\" ? (\"remote\" as const) : (\"local\" as const),\n updateData: row.updateData,\n })),\n };\n}\n\n/**\n * Size of the tail compaction can shrink. Queued rows are excluded: they stay\n * until settlement, so counting them would re-export full history on every\n * persist while offline edits wait to sync.\n */\nexport async function readDocumentHistoryTailSize(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<{ byteLength: number; hasCheckpoint: boolean; rowCount: number }> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n // One statement: an aggregate without GROUP BY always returns one row, and\n // the checkpoint probe rides along as an EXISTS column.\n const [totals] = await db\n .select({\n byteLength: sql<number>`coalesce(sum(length(${documentHistoryUpdates.updateData})), 0)`,\n hasCheckpoint: sql<number>`${exists(\n db\n .select({ localId: documentHistoryCheckpoints.localId })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n ),\n )}`,\n rowCount: sql<number>`count(*)`,\n })\n .from(documentHistoryUpdates)\n .where(compactableTailInScope(scope));\n return {\n byteLength: Number(totals?.byteLength ?? 0),\n hasCheckpoint: Number(totals?.hasCheckpoint ?? 0) !== 0,\n rowCount: Number(totals?.rowCount ?? 0),\n };\n}\n\ninterface DocumentHistoryTailEntry {\n readonly id: string;\n readonly updateData: string;\n}\n\n/**\n * The current compactable tail rows WITH their payloads, so a compactor can\n * prove each row's ops are covered by the snapshot it is about to install\n * (another pane's append may carry ops this pane's document has not merged\n * yet). Queued rows are never candidates.\n */\nexport async function listDocumentHistoryTailEntries(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<DocumentHistoryTailEntry[]> {\n await ensureSqlTables(execSql, documentTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n id: documentHistoryUpdates.id,\n updateData: documentHistoryUpdates.updateData,\n })\n .from(documentHistoryUpdates)\n .where(compactableTailInScope(scope));\n return rows.flatMap((row) =>\n row.id === null ? [] : [{ id: row.id, updateData: row.updateData }],\n );\n}\n\n/** Covered-tail ids per delete; the scope adds only a few binds. */\nconst HISTORY_ROW_CHUNK = 400;\n\n/**\n * Replace the scope's checkpoint with a fresh full-history snapshot and\n * delete ONLY the tail rows the caller captured before exporting it. Every\n * append path writes its tail row after the ops are already in the live\n * document, so any row captured before the export is provably covered by\n * the snapshot — while a row appended concurrently (between export and this\n * delete) survives for the next compaction instead of being lost.\n */\nexport async function replaceDocumentHistoryCheckpoint(\n execSql: ExecSql,\n scope: DocumentScope,\n input: {\n coveredTailIds: readonly string[];\n endVersionVector: string;\n /**\n * Skip the monotonic version gate. Only for document CREATION, where any\n * existing checkpoint under this localId is the orphan of a failed prior\n * attempt (checkpoint written, record row never committed) — a retry's\n * fresh document need not dominate that orphan to replace it.\n */\n force?: boolean;\n snapshot: string;\n /**\n * Re-checked after acquiring the serialized mutation slot (like the\n * record-save path): a store reset/recreation that happened while this\n * call waited in the queue must abort the write, or a stale compactor\n * could install the OLD document's checkpoint over the new identity's.\n */\n stillCurrent?: () => boolean;\n },\n): Promise<boolean> {\n await ensureSqlTables(execSql, documentTables);\n // Sequential (non-transactional) so callers already inside a transaction\n // can nest this. Crash between the statements leaves the new checkpoint\n // plus covered tail rows — a safe superset, since tail replay is\n // idempotent by op identity. The replacement itself is a compare-and-swap\n // on the stored revision, so the version gate cannot be bypassed by a\n // concurrent compactor between the read and the write: whoever loses the\n // CAS re-reads and re-evaluates the gate against the winner's checkpoint.\n return getClientSQLitePersistenceRuntime(execSql).runMutation(async (tx) => {\n if (input.stillCurrent?.() === false) {\n return false;\n }\n for (let attempt = 0; attempt < CHECKPOINT_CAS_ATTEMPTS; attempt += 1) {\n let replaced: boolean;\n try {\n replaced = await tryReplaceCheckpointOnce(tx, scope, input);\n } catch (error) {\n if (error instanceof CheckpointGateRejected) {\n // A fresher checkpoint dominates this candidate: no write, and\n // the caller's covered rows must stay for the covering pane.\n return false;\n }\n throw error;\n }\n if (!replaced) {\n // CAS lost to a concurrent writer — re-read and re-evaluate the\n // gate against the winner's checkpoint.\n continue;\n }\n for (\n let index = 0;\n index < input.coveredTailIds.length;\n index += HISTORY_ROW_CHUNK\n ) {\n // Never a queued row, even if a caller names one: its operations may\n // be in the checkpoint, but it is still the unsent update itself.\n await tx\n .delete(documentHistoryUpdates)\n .where(\n and(\n compactableTailInScope(scope),\n inArray(\n documentHistoryUpdates.id,\n input.coveredTailIds.slice(index, index + HISTORY_ROW_CHUNK),\n ),\n ),\n );\n }\n return true;\n }\n // Exhaustion must not read as durable success: creation and rebuild\n // callers persist record rows assuming the checkpoint landed.\n throw new Error(\n \"Document history checkpoint replacement lost every CAS attempt\",\n );\n });\n}\n\nconst CHECKPOINT_CAS_ATTEMPTS = 16;\n\ntype MutationDb = Parameters<\n Parameters<\n ReturnType<typeof getClientSQLitePersistenceRuntime>[\"runMutation\"]\n >[0]\n>[0];\n\n/**\n * One compare-and-swap attempt. Returns true when this caller's checkpoint\n * became the stored one, false when a concurrent writer won the CAS (the\n * caller re-reads and retries), and throws CheckpointGateRejected when a\n * fresher stored checkpoint dominates the candidate.\n */\nasync function tryReplaceCheckpointOnce(\n tx: MutationDb,\n scope: DocumentScope,\n input: {\n endVersionVector: string;\n force?: boolean;\n snapshot: string;\n },\n): Promise<boolean> {\n const updatedAt = new Date().toISOString();\n const revision = crypto.randomUUID();\n const [stored] = await tx\n .select({\n endVersionVector: documentHistoryCheckpoints.endVersionVector,\n revision: documentHistoryCheckpoints.revision,\n snapshot: documentHistoryCheckpoints.snapshot,\n })\n .from(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n )\n .limit(1);\n\n if (!stored) {\n const inserted = await tx\n .insert(documentHistoryCheckpoints)\n .values({\n appKind: scope.appKind,\n localId: scope.localId,\n snapshot: input.snapshot,\n endVersionVector: input.endVersionVector,\n revision,\n updatedAt,\n })\n .onConflictDoNothing({\n target: [\n documentHistoryCheckpoints.appKind,\n documentHistoryCheckpoints.localId,\n ],\n })\n .returning({ localId: documentHistoryCheckpoints.localId });\n // Conflict means a concurrent writer inserted first — retry the gate.\n return inserted.length > 0;\n }\n\n // A stale compactor (a pane whose document has not merged another pane's\n // ops) must never replace a checkpoint it does not cover: the covered\n // pane may already have deleted its tail rows, so regressing the\n // checkpoint would leave those ops with no durable copy at all. Vector\n // dominance proves counter coverage; the retained-prefix identity also\n // proves the candidate carries the exact stored operations at those\n // counters rather than a same-frontier fork.\n if (input.force !== true) {\n if (\n !satisfiesVersionVector(input.endVersionVector, stored.endVersionVector)\n ) {\n throw new CheckpointGateRejected();\n }\n if (\n input.snapshot !== stored.snapshot &&\n !(await candidateContainsStoredCheckpointHistory({\n candidateSnapshot: input.snapshot,\n storedEndVersion: stored.endVersionVector,\n storedSnapshot: stored.snapshot,\n }))\n ) {\n throw new CheckpointGateRejected();\n }\n }\n\n const replaced = await tx\n .update(documentHistoryCheckpoints)\n .set({\n snapshot: input.snapshot,\n endVersionVector: input.endVersionVector,\n revision,\n updatedAt,\n })\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n eq(documentHistoryCheckpoints.revision, stored.revision),\n ),\n )\n .returning({ localId: documentHistoryCheckpoints.localId });\n return replaced.length > 0;\n}\n\nasync function candidateContainsStoredCheckpointHistory(input: {\n candidateSnapshot: string;\n storedEndVersion: string;\n storedSnapshot: string;\n}): Promise<boolean> {\n const [candidateDocument, storedDocument] = await Promise.all([\n createDocument(\"checkpoint-candidate-history-gate\"),\n createDocument(\"checkpoint-stored-history-gate\"),\n ]);\n try {\n importSnapshot(candidateDocument, base64ToBytes(input.candidateSnapshot));\n importSnapshot(storedDocument, base64ToBytes(input.storedSnapshot));\n return (\n exportFullHistoryIdentity(candidateDocument, input.storedEndVersion) ===\n exportFullHistoryIdentity(storedDocument)\n );\n } finally {\n candidateDocument.free();\n storedDocument.free();\n }\n}\n\nclass CheckpointGateRejected extends Error {\n constructor() {\n super(\"checkpoint gate rejected\");\n this.name = \"CheckpointGateRejected\";\n }\n}\n\nexport async function deleteDocumentHistory(\n execSql: ExecSql,\n scope: DocumentScope,\n): Promise<void> {\n await ensureSqlTables(execSql, documentTables);\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (tx) => {\n await tx\n .delete(documentHistoryCheckpoints)\n .where(\n and(\n eq(documentHistoryCheckpoints.appKind, scope.appKind),\n eq(documentHistoryCheckpoints.localId, scope.localId),\n ),\n );\n await tx\n .delete(documentHistoryUpdates)\n .where(\n and(\n eq(documentHistoryUpdates.appKind, scope.appKind),\n eq(documentHistoryUpdates.localId, scope.localId),\n ),\n );\n });\n}\n"]}
@@ -3,11 +3,11 @@ import { keyingCheckpointTables, principalPolicyTables, } from "../../sqlite/sch
3
3
  import { verifiedManifestBundleTables } from "../../sqlite/schema/verifiedManifestBundleSchema.js";
4
4
  import { getClientSQLitePersistenceRuntime, } from "../../sqlite/sqlitePersistenceRuntime.js";
5
5
  import { ensureSqlTables } from "../../sqlite/sqlSchema.js";
6
- import { assertContainerNotRetired } from "../principalKeys/principalGrantRetirementPersistence.js";
6
+ import { loadContainerRetirementCheck } from "../principalKeys/principalGrantRetirementPersistence.js";
7
7
  import { verifiedManifestBundleRows, writeVerifiedManifestBundlesInTransaction, } from "../verifiedManifestBundlePersistence.js";
8
8
  import { storeDocumentPurgeCheckpointInTransaction, } from "./documentPurgeCheckpointPersistence.js";
9
9
  import { validateAccessManifestCheckpointEvidence } from "./keyingCheckpointEvidence.js";
10
- import { accessManifestObjectKey, loadStoredAccessManifestCheckpoint, loadStoredPrincipalPolicyCheckpoint, principalPolicyKey, upsertAccessManifestCheckpointInTransaction, upsertPrincipalPolicyCheckpointInTransaction, } from "./keyingCheckpointPersistence.js";
10
+ import { accessManifestObjectKey, loadAccessManifestCheckpointRows, loadStoredPrincipalPolicyCheckpoints, principalPolicyKey, upsertAccessManifestCheckpointsInTransaction, upsertPrincipalPolicyCheckpointsInTransaction, } from "./keyingCheckpointPersistence.js";
11
11
  import { validatePrincipalAuthorizationCheckpoints } from "./principalAuthorizationCheckpoints.js";
12
12
  function validateAccessAdvance(advance, localCheckpoint) {
13
13
  const current = {
@@ -40,13 +40,21 @@ function validateAccessAdvance(advance, localCheckpoint) {
40
40
  }
41
41
  async function validateAccessAdvances(tx, advances) {
42
42
  const pending = new Map();
43
+ if (advances.length === 0)
44
+ return pending;
45
+ // Set reads first; the checks then replay in order, so the same advance
46
+ // fails first with the same error.
47
+ const heads = advances.map((advance) => advance.head.checkpoint);
48
+ const assertNotRetired = await loadContainerRetirementCheck(tx, heads);
49
+ const stored = await loadAccessManifestCheckpointRows(tx, heads);
43
50
  for (const advance of advances) {
44
- await assertContainerNotRetired(tx, advance.head.checkpoint);
51
+ assertNotRetired(advance.head.checkpoint);
45
52
  const key = accessManifestObjectKey(advance.head.checkpoint);
46
53
  if (pending.has(key)) {
47
54
  throw new KeyingVerificationError("equivocation", `projection declares multiple access checkpoint heads for ${key}`);
48
55
  }
49
- const localCheckpoint = await loadStoredAccessManifestCheckpoint(tx, advance.head.checkpoint);
56
+ const row = stored.get(key);
57
+ const localCheckpoint = row ? { ...advance.head.checkpoint, ...row } : null;
50
58
  pending.set(key, validateAccessAdvance(advance, localCheckpoint));
51
59
  }
52
60
  return pending;
@@ -72,6 +80,7 @@ export async function validatePrincipalPolicyCheckpointAdvances(tx, policies) {
72
80
  policiesByPrincipal.set(key, candidates);
73
81
  }
74
82
  const pending = new Map();
83
+ const stored = await loadStoredPrincipalPolicyCheckpoints(tx, policies);
75
84
  for (const key of [...policiesByPrincipal.keys()].sort()) {
76
85
  const candidates = policiesByPrincipal.get(key) ?? [];
77
86
  const maxVersion = Math.max(...candidates.map((policy) => policy.version));
@@ -91,7 +100,7 @@ export async function validatePrincipalPolicyCheckpointAdvances(tx, policies) {
91
100
  throw new KeyingVerificationError("stale_predecessor", `principal policy head does not extend observed state for ${key}`);
92
101
  }
93
102
  }
94
- const localCheckpoint = await loadStoredPrincipalPolicyCheckpoint(tx, head);
103
+ const localCheckpoint = stored.get(key) ?? null;
95
104
  verifyPrincipalPolicyCheckpoint({
96
105
  chain: "retainedHistory" in head ? head.retainedHistory : (head.history ?? []),
97
106
  currentState: head.state,
@@ -102,15 +111,11 @@ export async function validatePrincipalPolicyCheckpointAdvances(tx, policies) {
102
111
  return pending;
103
112
  }
104
113
  async function writeAccessCheckpoints(tx, pending, updatedAt) {
105
- for (const { checkpoint } of pending.values()) {
106
- await upsertAccessManifestCheckpointInTransaction(tx, checkpoint, updatedAt);
107
- }
114
+ await upsertAccessManifestCheckpointsInTransaction(tx, [...pending.values()].map(({ checkpoint }) => checkpoint), updatedAt);
108
115
  }
109
116
  /** Pins the heads `validatePrincipalPolicyCheckpointAdvances` returned. */
110
117
  export async function writePrincipalPolicyCheckpoints(tx, pending, updatedAt, organizationId) {
111
- for (const policy of pending.values()) {
112
- await upsertPrincipalPolicyCheckpointInTransaction(tx, policy.checkpoint, updatedAt, organizationId);
113
- }
118
+ await upsertPrincipalPolicyCheckpointsInTransaction(tx, [...pending.values()].map((policy) => policy.checkpoint), updatedAt, organizationId);
114
119
  }
115
120
  /**
116
121
  * Digests the verified bundles and ensures every table the batch touches,
@@ -1 +1 @@
1
- {"version":3,"file":"keyingCheckpointAdvancePersistence.js","sourceRoot":"","sources":["keyingCheckpointAdvancePersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AccessManifestCheckpoint,\n type AnyVerifiedPrincipalPolicy,\n KeyingVerificationError,\n type VerifiedAccessManifestCheckpointEvidence,\n type VerifiedPrincipalPolicyCurrent,\n type VerifiedPrincipalPolicySelection,\n verifyAccessManifestLocalCheckpoint,\n verifyPrincipalPolicyCheckpoint,\n} from \"@tearleads/crypto\";\nimport {\n keyingCheckpointTables,\n principalPolicyTables,\n} from \"../../sqlite/schema\";\nimport { verifiedManifestBundleTables } from \"../../sqlite/schema/verifiedManifestBundleSchema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\nimport { assertContainerNotRetired } from \"../principalKeys/principalGrantRetirementPersistence\";\nimport {\n type VerifiedManifestBundle,\n type VerifiedManifestBundleRow,\n verifiedManifestBundleRows,\n writeVerifiedManifestBundlesInTransaction,\n} from \"../verifiedManifestBundlePersistence\";\nimport {\n type DocumentPurgeCheckpoint,\n storeDocumentPurgeCheckpointInTransaction,\n} from \"./documentPurgeCheckpointPersistence\";\nimport { validateAccessManifestCheckpointEvidence } from \"./keyingCheckpointEvidence\";\nimport {\n accessManifestObjectKey,\n loadStoredAccessManifestCheckpoint,\n loadStoredPrincipalPolicyCheckpoint,\n principalPolicyKey,\n upsertAccessManifestCheckpointInTransaction,\n upsertPrincipalPolicyCheckpointInTransaction,\n} from \"./keyingCheckpointPersistence\";\nimport { validatePrincipalAuthorizationCheckpoints } from \"./principalAuthorizationCheckpoints\";\n\nexport interface AccessManifestCheckpointAdvance {\n readonly head: VerifiedAccessManifestCheckpointEvidence;\n readonly predecessors: readonly VerifiedAccessManifestCheckpointEvidence[];\n}\n\ntype CheckpointPolicy =\n | AnyVerifiedPrincipalPolicy\n | VerifiedPrincipalPolicyCurrent;\n\ninterface KeyingCheckpointValidationInput {\n readonly access: readonly AccessManifestCheckpointAdvance[];\n readonly execSql: ExecSql;\n readonly policies: readonly CheckpointPolicy[];\n readonly authorizationPolicies?:\n | readonly VerifiedPrincipalPolicySelection[]\n | undefined;\n readonly stillCurrent?: (() => boolean) | undefined;\n /**\n * Bundles this projection verified. They are stored for later citation in\n * the same transaction, so they commit only with the batch they verified.\n */\n readonly verifiedBundles?: readonly VerifiedManifestBundle[] | undefined;\n}\n\ninterface PendingAccessCheckpoint {\n readonly checkpoint: AccessManifestCheckpoint;\n}\n\nfunction validateAccessAdvance(\n advance: AccessManifestCheckpointAdvance,\n localCheckpoint: AccessManifestCheckpoint | null,\n): PendingAccessCheckpoint {\n const current = {\n ...advance.head.checkpoint,\n previousManifestHash: advance.head.manifest.previousManifestHash,\n };\n const predecessors = [...advance.predecessors].sort(\n (left, right) => left.checkpoint.epoch - right.checkpoint.epoch,\n );\n\n if (localCheckpoint && current.epoch <= localCheckpoint.epoch) {\n verifyAccessManifestLocalCheckpoint({\n current,\n localCheckpoint,\n checkpointPredecessors: undefined,\n });\n }\n\n validateAccessManifestCheckpointEvidence({\n head: advance.head,\n predecessors,\n localCheckpoint,\n });\n\n verifyAccessManifestLocalCheckpoint({\n current,\n localCheckpoint,\n checkpointPredecessors: predecessors.filter(\n (manifest) =>\n localCheckpoint !== null &&\n manifest.checkpoint.epoch > localCheckpoint.epoch &&\n manifest.checkpoint.epoch < current.epoch,\n ),\n });\n\n return {\n checkpoint: advance.head.checkpoint,\n };\n}\n\nasync function validateAccessAdvances(\n tx: ClientSQLiteTransactionScope,\n advances: readonly AccessManifestCheckpointAdvance[],\n): Promise<Map<string, PendingAccessCheckpoint>> {\n const pending = new Map<string, PendingAccessCheckpoint>();\n\n for (const advance of advances) {\n await assertContainerNotRetired(tx, advance.head.checkpoint);\n const key = accessManifestObjectKey(advance.head.checkpoint);\n if (pending.has(key)) {\n throw new KeyingVerificationError(\n \"equivocation\",\n `projection declares multiple access checkpoint heads for ${key}`,\n );\n }\n const localCheckpoint = await loadStoredAccessManifestCheckpoint(\n tx,\n advance.head.checkpoint,\n );\n pending.set(key, validateAccessAdvance(advance, localCheckpoint));\n }\n\n return pending;\n}\n\nfunction extendsObservedPolicy(\n head: CheckpointPolicy,\n candidate: CheckpointPolicy,\n): boolean {\n const history =\n \"retainedHistory\" in head ? head.retainedHistory : head.history;\n return (\n history?.some(\n ({ state }) =>\n state.principalType === candidate.principalType &&\n state.principalId === candidate.principalId &&\n state.version === candidate.version &&\n state.stateHash === candidate.stateHash,\n ) ?? false\n );\n}\n\n/**\n * Checks, inside the caller's transaction, that each principal's newest policy\n * extends every other candidate and its durable checkpoint. Returns the heads\n * to pin, keyed by principal.\n */\nexport async function validatePrincipalPolicyCheckpointAdvances(\n tx: ClientSQLiteTransactionScope,\n policies: readonly CheckpointPolicy[],\n): Promise<Map<string, CheckpointPolicy>> {\n const policiesByPrincipal = new Map<string, CheckpointPolicy[]>();\n for (const policy of policies) {\n const key = principalPolicyKey(policy);\n const candidates = policiesByPrincipal.get(key) ?? [];\n candidates.push(policy);\n policiesByPrincipal.set(key, candidates);\n }\n\n const pending = new Map<string, CheckpointPolicy>();\n for (const key of [...policiesByPrincipal.keys()].sort()) {\n const candidates = policiesByPrincipal.get(key) ?? [];\n const maxVersion = Math.max(...candidates.map((policy) => policy.version));\n const heads = candidates.filter((policy) => policy.version === maxVersion);\n const head = heads[0];\n if (!head) {\n continue;\n }\n if (heads.some((candidate) => candidate.stateHash !== head.stateHash)) {\n throw new KeyingVerificationError(\n \"equivocation\",\n `principal policy declares conflicting heads for ${key}`,\n );\n }\n for (const candidate of candidates) {\n if (candidate.version === head.version) {\n continue;\n }\n if (!extendsObservedPolicy(head, candidate)) {\n throw new KeyingVerificationError(\n \"stale_predecessor\",\n `principal policy head does not extend observed state for ${key}`,\n );\n }\n }\n\n const localCheckpoint = await loadStoredPrincipalPolicyCheckpoint(tx, head);\n verifyPrincipalPolicyCheckpoint({\n chain:\n \"retainedHistory\" in head ? head.retainedHistory : (head.history ?? []),\n currentState: head.state,\n localCheckpoint,\n });\n pending.set(key, head);\n }\n\n return pending;\n}\n\nasync function writeAccessCheckpoints(\n tx: ClientSQLiteTransactionScope,\n pending: ReadonlyMap<string, PendingAccessCheckpoint>,\n updatedAt: string,\n): Promise<void> {\n for (const { checkpoint } of pending.values()) {\n await upsertAccessManifestCheckpointInTransaction(\n tx,\n checkpoint,\n updatedAt,\n );\n }\n}\n\n/** Pins the heads `validatePrincipalPolicyCheckpointAdvances` returned. */\nexport async function writePrincipalPolicyCheckpoints(\n tx: ClientSQLiteTransactionScope,\n pending: ReadonlyMap<string, CheckpointPolicy>,\n updatedAt: string,\n organizationId?: string | undefined,\n): Promise<void> {\n for (const policy of pending.values()) {\n await upsertPrincipalPolicyCheckpointInTransaction(\n tx,\n policy.checkpoint,\n updatedAt,\n organizationId,\n );\n }\n}\n\n/**\n * Digests the verified bundles and ensures every table the batch touches,\n * all before its transaction begins.\n */\nasync function prepareKeyingCheckpointBatch(\n input: KeyingCheckpointValidationInput,\n): Promise<VerifiedManifestBundleRow[]> {\n const bundleRows = await verifiedManifestBundleRows(\n input.verifiedBundles ?? [],\n );\n await ensureSqlTables(input.execSql, [\n ...(input.policies.length > 0 ||\n (input.authorizationPolicies?.length ?? 0) > 0\n ? principalPolicyTables\n : []),\n ...keyingCheckpointTables,\n ...(bundleRows.length > 0 ? verifiedManifestBundleTables : []),\n ]);\n return bundleRows;\n}\n\n/**\n * Re-checks verified candidates against durable pins without advancing them.\n * Only the verified bundles' citation rows are written.\n */\nexport async function validateKeyingCheckpointsAtomically(\n input: KeyingCheckpointValidationInput,\n): Promise<void> {\n const bundleRows = await prepareKeyingCheckpointBatch(input);\n const verifiedAt = new Date().toISOString();\n const validate = async (tx: ClientSQLiteTransactionScope) => {\n await validateAccessAdvances(tx, input.access);\n await validatePrincipalPolicyCheckpointAdvances(tx, input.policies);\n await validatePrincipalAuthorizationCheckpoints(\n tx,\n input.authorizationPolicies ?? [],\n );\n await writeVerifiedManifestBundlesInTransaction(tx, bundleRows, verifiedAt);\n };\n const runtime = getClientSQLitePersistenceRuntime(input.execSql);\n if (input.stillCurrent) {\n await runtime.guardedTransaction(validate, input.stillCurrent, {\n behavior: \"immediate\",\n });\n return;\n }\n await runtime.transaction(validate, { behavior: \"immediate\" });\n}\n\n/**\n * Re-check every verified projection candidate against the latest durable pins\n * and advance the complete batch atomically. Signature verification and remote\n * fetching happen before this short transaction; only checkpoint comparison\n * and persistence, with the batch's verified bundles, are serialized here.\n */\nexport async function advanceKeyingCheckpointsAtomically(\n input: KeyingCheckpointValidationInput & {\n readonly documentPurgeCheckpoint?: DocumentPurgeCheckpoint | undefined;\n readonly organizationId?: string | undefined;\n },\n): Promise<void> {\n const bundleRows = await prepareKeyingCheckpointBatch(input);\n const updatedAt = new Date().toISOString();\n\n const runtime = getClientSQLitePersistenceRuntime(input.execSql);\n const advance = async (tx: ClientSQLiteTransactionScope) => {\n const access = await validateAccessAdvances(tx, input.access);\n const policies = await validatePrincipalPolicyCheckpointAdvances(\n tx,\n input.policies,\n );\n await validatePrincipalAuthorizationCheckpoints(\n tx,\n input.authorizationPolicies ?? [],\n );\n\n await writeAccessCheckpoints(tx, access, updatedAt);\n await writePrincipalPolicyCheckpoints(\n tx,\n policies,\n updatedAt,\n input.organizationId,\n );\n if (input.documentPurgeCheckpoint) {\n await storeDocumentPurgeCheckpointInTransaction(\n tx,\n input.documentPurgeCheckpoint,\n updatedAt,\n );\n }\n await writeVerifiedManifestBundlesInTransaction(tx, bundleRows, updatedAt);\n };\n if (input.stillCurrent) {\n await runtime.guardedTransaction(advance, input.stillCurrent, {\n behavior: \"immediate\",\n });\n return;\n }\n await runtime.transaction(advance, { behavior: \"immediate\" });\n}\n"]}
1
+ {"version":3,"file":"keyingCheckpointAdvancePersistence.js","sourceRoot":"","sources":["keyingCheckpointAdvancePersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AccessManifestCheckpoint,\n type AnyVerifiedPrincipalPolicy,\n KeyingVerificationError,\n type VerifiedAccessManifestCheckpointEvidence,\n type VerifiedPrincipalPolicyCurrent,\n type VerifiedPrincipalPolicySelection,\n verifyAccessManifestLocalCheckpoint,\n verifyPrincipalPolicyCheckpoint,\n} from \"@tearleads/crypto\";\nimport {\n keyingCheckpointTables,\n principalPolicyTables,\n} from \"../../sqlite/schema\";\nimport { verifiedManifestBundleTables } from \"../../sqlite/schema/verifiedManifestBundleSchema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\nimport { loadContainerRetirementCheck } from \"../principalKeys/principalGrantRetirementPersistence\";\nimport {\n type VerifiedManifestBundle,\n type VerifiedManifestBundleRow,\n verifiedManifestBundleRows,\n writeVerifiedManifestBundlesInTransaction,\n} from \"../verifiedManifestBundlePersistence\";\nimport {\n type DocumentPurgeCheckpoint,\n storeDocumentPurgeCheckpointInTransaction,\n} from \"./documentPurgeCheckpointPersistence\";\nimport { validateAccessManifestCheckpointEvidence } from \"./keyingCheckpointEvidence\";\nimport {\n accessManifestObjectKey,\n loadAccessManifestCheckpointRows,\n loadStoredPrincipalPolicyCheckpoints,\n principalPolicyKey,\n upsertAccessManifestCheckpointsInTransaction,\n upsertPrincipalPolicyCheckpointsInTransaction,\n} from \"./keyingCheckpointPersistence\";\nimport { validatePrincipalAuthorizationCheckpoints } from \"./principalAuthorizationCheckpoints\";\n\nexport interface AccessManifestCheckpointAdvance {\n readonly head: VerifiedAccessManifestCheckpointEvidence;\n readonly predecessors: readonly VerifiedAccessManifestCheckpointEvidence[];\n}\n\ntype CheckpointPolicy =\n | AnyVerifiedPrincipalPolicy\n | VerifiedPrincipalPolicyCurrent;\n\ninterface KeyingCheckpointValidationInput {\n readonly access: readonly AccessManifestCheckpointAdvance[];\n readonly execSql: ExecSql;\n readonly policies: readonly CheckpointPolicy[];\n readonly authorizationPolicies?:\n | readonly VerifiedPrincipalPolicySelection[]\n | undefined;\n readonly stillCurrent?: (() => boolean) | undefined;\n /**\n * Bundles this projection verified. They are stored for later citation in\n * the same transaction, so they commit only with the batch they verified.\n */\n readonly verifiedBundles?: readonly VerifiedManifestBundle[] | undefined;\n}\n\ninterface PendingAccessCheckpoint {\n readonly checkpoint: AccessManifestCheckpoint;\n}\n\nfunction validateAccessAdvance(\n advance: AccessManifestCheckpointAdvance,\n localCheckpoint: AccessManifestCheckpoint | null,\n): PendingAccessCheckpoint {\n const current = {\n ...advance.head.checkpoint,\n previousManifestHash: advance.head.manifest.previousManifestHash,\n };\n const predecessors = [...advance.predecessors].sort(\n (left, right) => left.checkpoint.epoch - right.checkpoint.epoch,\n );\n\n if (localCheckpoint && current.epoch <= localCheckpoint.epoch) {\n verifyAccessManifestLocalCheckpoint({\n current,\n localCheckpoint,\n checkpointPredecessors: undefined,\n });\n }\n\n validateAccessManifestCheckpointEvidence({\n head: advance.head,\n predecessors,\n localCheckpoint,\n });\n\n verifyAccessManifestLocalCheckpoint({\n current,\n localCheckpoint,\n checkpointPredecessors: predecessors.filter(\n (manifest) =>\n localCheckpoint !== null &&\n manifest.checkpoint.epoch > localCheckpoint.epoch &&\n manifest.checkpoint.epoch < current.epoch,\n ),\n });\n\n return {\n checkpoint: advance.head.checkpoint,\n };\n}\n\nasync function validateAccessAdvances(\n tx: ClientSQLiteTransactionScope,\n advances: readonly AccessManifestCheckpointAdvance[],\n): Promise<Map<string, PendingAccessCheckpoint>> {\n const pending = new Map<string, PendingAccessCheckpoint>();\n if (advances.length === 0) return pending;\n // Set reads first; the checks then replay in order, so the same advance\n // fails first with the same error.\n const heads = advances.map((advance) => advance.head.checkpoint);\n const assertNotRetired = await loadContainerRetirementCheck(tx, heads);\n const stored = await loadAccessManifestCheckpointRows(tx, heads);\n\n for (const advance of advances) {\n assertNotRetired(advance.head.checkpoint);\n const key = accessManifestObjectKey(advance.head.checkpoint);\n if (pending.has(key)) {\n throw new KeyingVerificationError(\n \"equivocation\",\n `projection declares multiple access checkpoint heads for ${key}`,\n );\n }\n const row = stored.get(key);\n const localCheckpoint = row ? { ...advance.head.checkpoint, ...row } : null;\n pending.set(key, validateAccessAdvance(advance, localCheckpoint));\n }\n\n return pending;\n}\n\nfunction extendsObservedPolicy(\n head: CheckpointPolicy,\n candidate: CheckpointPolicy,\n): boolean {\n const history =\n \"retainedHistory\" in head ? head.retainedHistory : head.history;\n return (\n history?.some(\n ({ state }) =>\n state.principalType === candidate.principalType &&\n state.principalId === candidate.principalId &&\n state.version === candidate.version &&\n state.stateHash === candidate.stateHash,\n ) ?? false\n );\n}\n\n/**\n * Checks, inside the caller's transaction, that each principal's newest policy\n * extends every other candidate and its durable checkpoint. Returns the heads\n * to pin, keyed by principal.\n */\nexport async function validatePrincipalPolicyCheckpointAdvances(\n tx: ClientSQLiteTransactionScope,\n policies: readonly CheckpointPolicy[],\n): Promise<Map<string, CheckpointPolicy>> {\n const policiesByPrincipal = new Map<string, CheckpointPolicy[]>();\n for (const policy of policies) {\n const key = principalPolicyKey(policy);\n const candidates = policiesByPrincipal.get(key) ?? [];\n candidates.push(policy);\n policiesByPrincipal.set(key, candidates);\n }\n\n const pending = new Map<string, CheckpointPolicy>();\n const stored = await loadStoredPrincipalPolicyCheckpoints(tx, policies);\n for (const key of [...policiesByPrincipal.keys()].sort()) {\n const candidates = policiesByPrincipal.get(key) ?? [];\n const maxVersion = Math.max(...candidates.map((policy) => policy.version));\n const heads = candidates.filter((policy) => policy.version === maxVersion);\n const head = heads[0];\n if (!head) {\n continue;\n }\n if (heads.some((candidate) => candidate.stateHash !== head.stateHash)) {\n throw new KeyingVerificationError(\n \"equivocation\",\n `principal policy declares conflicting heads for ${key}`,\n );\n }\n for (const candidate of candidates) {\n if (candidate.version === head.version) {\n continue;\n }\n if (!extendsObservedPolicy(head, candidate)) {\n throw new KeyingVerificationError(\n \"stale_predecessor\",\n `principal policy head does not extend observed state for ${key}`,\n );\n }\n }\n\n const localCheckpoint = stored.get(key) ?? null;\n verifyPrincipalPolicyCheckpoint({\n chain:\n \"retainedHistory\" in head ? head.retainedHistory : (head.history ?? []),\n currentState: head.state,\n localCheckpoint,\n });\n pending.set(key, head);\n }\n\n return pending;\n}\n\nasync function writeAccessCheckpoints(\n tx: ClientSQLiteTransactionScope,\n pending: ReadonlyMap<string, PendingAccessCheckpoint>,\n updatedAt: string,\n): Promise<void> {\n await upsertAccessManifestCheckpointsInTransaction(\n tx,\n [...pending.values()].map(({ checkpoint }) => checkpoint),\n updatedAt,\n );\n}\n\n/** Pins the heads `validatePrincipalPolicyCheckpointAdvances` returned. */\nexport async function writePrincipalPolicyCheckpoints(\n tx: ClientSQLiteTransactionScope,\n pending: ReadonlyMap<string, CheckpointPolicy>,\n updatedAt: string,\n organizationId?: string | undefined,\n): Promise<void> {\n await upsertPrincipalPolicyCheckpointsInTransaction(\n tx,\n [...pending.values()].map((policy) => policy.checkpoint),\n updatedAt,\n organizationId,\n );\n}\n\n/**\n * Digests the verified bundles and ensures every table the batch touches,\n * all before its transaction begins.\n */\nasync function prepareKeyingCheckpointBatch(\n input: KeyingCheckpointValidationInput,\n): Promise<VerifiedManifestBundleRow[]> {\n const bundleRows = await verifiedManifestBundleRows(\n input.verifiedBundles ?? [],\n );\n await ensureSqlTables(input.execSql, [\n ...(input.policies.length > 0 ||\n (input.authorizationPolicies?.length ?? 0) > 0\n ? principalPolicyTables\n : []),\n ...keyingCheckpointTables,\n ...(bundleRows.length > 0 ? verifiedManifestBundleTables : []),\n ]);\n return bundleRows;\n}\n\n/**\n * Re-checks verified candidates against durable pins without advancing them.\n * Only the verified bundles' citation rows are written.\n */\nexport async function validateKeyingCheckpointsAtomically(\n input: KeyingCheckpointValidationInput,\n): Promise<void> {\n const bundleRows = await prepareKeyingCheckpointBatch(input);\n const verifiedAt = new Date().toISOString();\n const validate = async (tx: ClientSQLiteTransactionScope) => {\n await validateAccessAdvances(tx, input.access);\n await validatePrincipalPolicyCheckpointAdvances(tx, input.policies);\n await validatePrincipalAuthorizationCheckpoints(\n tx,\n input.authorizationPolicies ?? [],\n );\n await writeVerifiedManifestBundlesInTransaction(tx, bundleRows, verifiedAt);\n };\n const runtime = getClientSQLitePersistenceRuntime(input.execSql);\n if (input.stillCurrent) {\n await runtime.guardedTransaction(validate, input.stillCurrent, {\n behavior: \"immediate\",\n });\n return;\n }\n await runtime.transaction(validate, { behavior: \"immediate\" });\n}\n\n/**\n * Re-check every verified projection candidate against the latest durable pins\n * and advance the complete batch atomically. Signature verification and remote\n * fetching happen before this short transaction; only checkpoint comparison\n * and persistence, with the batch's verified bundles, are serialized here.\n */\nexport async function advanceKeyingCheckpointsAtomically(\n input: KeyingCheckpointValidationInput & {\n readonly documentPurgeCheckpoint?: DocumentPurgeCheckpoint | undefined;\n readonly organizationId?: string | undefined;\n },\n): Promise<void> {\n const bundleRows = await prepareKeyingCheckpointBatch(input);\n const updatedAt = new Date().toISOString();\n\n const runtime = getClientSQLitePersistenceRuntime(input.execSql);\n const advance = async (tx: ClientSQLiteTransactionScope) => {\n const access = await validateAccessAdvances(tx, input.access);\n const policies = await validatePrincipalPolicyCheckpointAdvances(\n tx,\n input.policies,\n );\n await validatePrincipalAuthorizationCheckpoints(\n tx,\n input.authorizationPolicies ?? [],\n );\n\n await writeAccessCheckpoints(tx, access, updatedAt);\n await writePrincipalPolicyCheckpoints(\n tx,\n policies,\n updatedAt,\n input.organizationId,\n );\n if (input.documentPurgeCheckpoint) {\n await storeDocumentPurgeCheckpointInTransaction(\n tx,\n input.documentPurgeCheckpoint,\n updatedAt,\n );\n }\n await writeVerifiedManifestBundlesInTransaction(tx, bundleRows, updatedAt);\n };\n if (input.stillCurrent) {\n await runtime.guardedTransaction(advance, input.stillCurrent, {\n behavior: \"immediate\",\n });\n return;\n }\n await runtime.transaction(advance, { behavior: \"immediate\" });\n}\n"]}