@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.
Files changed (66) 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 +167 -51
  13. package/data/persistence/container-contents/containerRecoveryPersistence.js.map +1 -1
  14. package/data/persistence/container-contents/documentContainerProjectionPersistence.js +56 -19
  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/documentPlacement.js +24 -5
  19. package/data/persistence/container-contents/documentPlacement.js.map +1 -1
  20. package/data/persistence/container-contents/documentPlacementIntentEnqueue.js +1 -1
  21. package/data/persistence/container-contents/documentPlacementIntentEnqueue.js.map +1 -1
  22. package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js +1 -1
  23. package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js.map +1 -1
  24. package/data/persistence/documents/documentDiscoveryEvidencePersistence.js +43 -35
  25. package/data/persistence/documents/documentDiscoveryEvidencePersistence.js.map +1 -1
  26. package/data/persistence/documents/documentsPersistence.js +4 -58
  27. package/data/persistence/documents/documentsPersistence.js.map +1 -1
  28. package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js +15 -11
  29. package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js.map +1 -1
  30. package/data/persistence/documents/internal/containerDocumentTombstones.js +87 -35
  31. package/data/persistence/documents/internal/containerDocumentTombstones.js.map +1 -1
  32. package/data/persistence/documents/internal/discoveredDocumentPlacement.js +5 -2
  33. package/data/persistence/documents/internal/discoveredDocumentPlacement.js.map +1 -1
  34. package/data/persistence/documents/internal/discoveredDocumentUpsert.js +174 -0
  35. package/data/persistence/documents/internal/discoveredDocumentUpsert.js.map +1 -0
  36. package/data/persistence/documents/internal/documentMutationPersistence.js +15 -15
  37. package/data/persistence/documents/internal/documentMutationPersistence.js.map +1 -1
  38. package/data/persistence/documents/internal/documentRows.js +122 -11
  39. package/data/persistence/documents/internal/documentRows.js.map +1 -1
  40. package/data/persistence/documents/internal/rowQueryPersistence.js +70 -27
  41. package/data/persistence/documents/internal/rowQueryPersistence.js.map +1 -1
  42. package/data/persistence/documents/records/documentHistoryPersistence.js +17 -11
  43. package/data/persistence/documents/records/documentHistoryPersistence.js.map +1 -1
  44. package/data/persistence/documents/records/documentRecordPersistence.d.ts +5 -0
  45. package/data/persistence/documents/records/documentRecordPersistence.js +41 -6
  46. package/data/persistence/documents/records/documentRecordPersistence.js.map +1 -1
  47. package/data/persistence/documents/records/index.d.ts +1 -1
  48. package/data/persistence/documents/records/index.js +1 -1
  49. package/data/persistence/documents/records/index.js.map +1 -1
  50. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.d.ts +2 -0
  51. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js +17 -1
  52. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js.map +1 -1
  53. package/package.json +1 -1
  54. package/workflows/container-contents/documentQueries/index.js +21 -6
  55. package/workflows/container-contents/documentQueries/index.js.map +1 -1
  56. package/workflows/container-contents/remoteReset/containerRemoteReset.js +12 -7
  57. package/workflows/container-contents/remoteReset/containerRemoteReset.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/documents/remoteReset/documentRemoteReset.js +31 -18
  66. package/workflows/documents/remoteReset/documentRemoteReset.js.map +1 -1
@@ -19,11 +19,241 @@ export interface ContainerRecord {
19
19
  updatedAt?: string | null;
20
20
  }
21
21
  export declare function ensureContainerTables(execSql: ExecSql): Promise<void>;
22
+ interface SelectedContainerRecord {
23
+ effectiveAccessLevel: string | null;
24
+ id: string | null;
25
+ organizationId: string;
26
+ parentId: string | null;
27
+ metadataDocumentId: string | null;
28
+ systemSlot: string | null;
29
+ name: string;
30
+ icon: string | null;
31
+ localCreatedAt: string;
32
+ localUpdatedAt: string;
33
+ serverCreatedAt: string | null;
34
+ serverUpdatedAt: string | null;
35
+ }
36
+ export declare function mapSelectedContainerRecord(row: SelectedContainerRecord): ContainerRecord;
22
37
  export declare function saveContainerRows(input: {
23
38
  tx: ClientSQLiteTransactionScope;
24
39
  record: ContainerRecord;
25
40
  localUpdatedAt: string;
26
41
  }): Promise<ContainerRecord>;
42
+ /** The ONE select shape for a container record list. */
43
+ export declare const containerRecordSelection: {
44
+ effectiveAccessLevel: import("drizzle-orm/sqlite-core").SQLiteColumn<{
45
+ name: "effective_access_level";
46
+ tableName: "containers";
47
+ dataType: "string";
48
+ columnType: "SQLiteText";
49
+ data: string;
50
+ driverParam: string;
51
+ notNull: true;
52
+ hasDefault: true;
53
+ isPrimaryKey: false;
54
+ isAutoincrement: false;
55
+ hasRuntimeDefault: false;
56
+ enumValues: [string, ...string[]];
57
+ baseColumn: never;
58
+ identity: undefined;
59
+ generated: undefined;
60
+ }, {}, {
61
+ length: number | undefined;
62
+ }>;
63
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
64
+ name: "id";
65
+ tableName: "containers";
66
+ dataType: "string";
67
+ columnType: "SQLiteText";
68
+ data: string;
69
+ driverParam: string;
70
+ notNull: false;
71
+ hasDefault: false;
72
+ isPrimaryKey: false;
73
+ isAutoincrement: false;
74
+ hasRuntimeDefault: false;
75
+ enumValues: [string, ...string[]];
76
+ baseColumn: never;
77
+ identity: undefined;
78
+ generated: undefined;
79
+ }, {}, {
80
+ length: number | undefined;
81
+ }>;
82
+ organizationId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
83
+ name: "organization_id";
84
+ tableName: "containers";
85
+ dataType: "string";
86
+ columnType: "SQLiteText";
87
+ data: string;
88
+ driverParam: string;
89
+ notNull: true;
90
+ hasDefault: false;
91
+ isPrimaryKey: false;
92
+ isAutoincrement: false;
93
+ hasRuntimeDefault: false;
94
+ enumValues: [string, ...string[]];
95
+ baseColumn: never;
96
+ identity: undefined;
97
+ generated: undefined;
98
+ }, {}, {
99
+ length: number | undefined;
100
+ }>;
101
+ parentId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
102
+ name: "parent_id";
103
+ tableName: "containers";
104
+ dataType: "string";
105
+ columnType: "SQLiteText";
106
+ data: string;
107
+ driverParam: string;
108
+ notNull: false;
109
+ hasDefault: false;
110
+ isPrimaryKey: false;
111
+ isAutoincrement: false;
112
+ hasRuntimeDefault: false;
113
+ enumValues: [string, ...string[]];
114
+ baseColumn: never;
115
+ identity: undefined;
116
+ generated: undefined;
117
+ }, {}, {
118
+ length: number | undefined;
119
+ }>;
120
+ metadataDocumentId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
121
+ name: "metadata_document_id";
122
+ tableName: "containers";
123
+ dataType: "string";
124
+ columnType: "SQLiteText";
125
+ data: string;
126
+ driverParam: string;
127
+ notNull: false;
128
+ hasDefault: false;
129
+ isPrimaryKey: false;
130
+ isAutoincrement: false;
131
+ hasRuntimeDefault: false;
132
+ enumValues: [string, ...string[]];
133
+ baseColumn: never;
134
+ identity: undefined;
135
+ generated: undefined;
136
+ }, {}, {
137
+ length: number | undefined;
138
+ }>;
139
+ systemSlot: import("drizzle-orm/sqlite-core").SQLiteColumn<{
140
+ name: "system_slot";
141
+ tableName: "containers";
142
+ dataType: "string";
143
+ columnType: "SQLiteText";
144
+ data: string;
145
+ driverParam: string;
146
+ notNull: false;
147
+ hasDefault: false;
148
+ isPrimaryKey: false;
149
+ isAutoincrement: false;
150
+ hasRuntimeDefault: false;
151
+ enumValues: [string, ...string[]];
152
+ baseColumn: never;
153
+ identity: undefined;
154
+ generated: undefined;
155
+ }, {}, {
156
+ length: number | undefined;
157
+ }>;
158
+ name: import("drizzle-orm").SQL<string>;
159
+ icon: import("drizzle-orm/sqlite-core").SQLiteColumn<{
160
+ name: "icon";
161
+ tableName: "containers";
162
+ dataType: "string";
163
+ columnType: "SQLiteText";
164
+ data: string;
165
+ driverParam: string;
166
+ notNull: false;
167
+ hasDefault: false;
168
+ isPrimaryKey: false;
169
+ isAutoincrement: false;
170
+ hasRuntimeDefault: false;
171
+ enumValues: [string, ...string[]];
172
+ baseColumn: never;
173
+ identity: undefined;
174
+ generated: undefined;
175
+ }, {}, {
176
+ length: number | undefined;
177
+ }>;
178
+ localCreatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
179
+ name: "local_created_at";
180
+ tableName: "containers";
181
+ dataType: "string";
182
+ columnType: "SQLiteText";
183
+ data: string;
184
+ driverParam: string;
185
+ notNull: true;
186
+ hasDefault: false;
187
+ isPrimaryKey: false;
188
+ isAutoincrement: false;
189
+ hasRuntimeDefault: false;
190
+ enumValues: [string, ...string[]];
191
+ baseColumn: never;
192
+ identity: undefined;
193
+ generated: undefined;
194
+ }, {}, {
195
+ length: number | undefined;
196
+ }>;
197
+ localUpdatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
198
+ name: "local_updated_at";
199
+ tableName: "containers";
200
+ dataType: "string";
201
+ columnType: "SQLiteText";
202
+ data: string;
203
+ driverParam: string;
204
+ notNull: true;
205
+ hasDefault: false;
206
+ isPrimaryKey: false;
207
+ isAutoincrement: false;
208
+ hasRuntimeDefault: false;
209
+ enumValues: [string, ...string[]];
210
+ baseColumn: never;
211
+ identity: undefined;
212
+ generated: undefined;
213
+ }, {}, {
214
+ length: number | undefined;
215
+ }>;
216
+ serverCreatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
217
+ name: "server_created_at";
218
+ tableName: "containers";
219
+ dataType: "string";
220
+ columnType: "SQLiteText";
221
+ data: string;
222
+ driverParam: string;
223
+ notNull: false;
224
+ hasDefault: false;
225
+ isPrimaryKey: false;
226
+ isAutoincrement: false;
227
+ hasRuntimeDefault: false;
228
+ enumValues: [string, ...string[]];
229
+ baseColumn: never;
230
+ identity: undefined;
231
+ generated: undefined;
232
+ }, {}, {
233
+ length: number | undefined;
234
+ }>;
235
+ serverUpdatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
236
+ name: "server_updated_at";
237
+ tableName: "containers";
238
+ dataType: "string";
239
+ columnType: "SQLiteText";
240
+ data: string;
241
+ driverParam: string;
242
+ notNull: false;
243
+ hasDefault: false;
244
+ isPrimaryKey: false;
245
+ isAutoincrement: false;
246
+ hasRuntimeDefault: false;
247
+ enumValues: [string, ...string[]];
248
+ baseColumn: never;
249
+ identity: undefined;
250
+ generated: undefined;
251
+ }, {}, {
252
+ length: number | undefined;
253
+ }>;
254
+ };
255
+ /** Container lists sort by display name, case-insensitively. */
256
+ export declare const containerListOrder: import("drizzle-orm").SQL<unknown>;
27
257
  export declare function loadContainers(execSql: ExecSql): Promise<ContainerRecord[]>;
28
258
  export declare function loadContainerById(execSql: ExecSql, containerId: string): Promise<ContainerRecord | null>;
29
259
  export declare function loadContainerDisplayNamesByIds(execSql: ExecSql, ids: ReadonlyArray<string>): Promise<Map<string, string>>;
@@ -33,3 +263,4 @@ export declare function saveContainer(execSql: ExecSql, record: ContainerRecord)
33
263
  * can compose it inside one atomic transaction.
34
264
  */
35
265
  export declare function deleteContainerRowsInTransaction(tx: ClientSQLiteTransactionScope, ids: ReadonlyArray<string>): Promise<void>;
266
+ export {};
@@ -38,7 +38,7 @@ function applyContainerTimestampDefaults(record, localUpdatedAt) {
38
38
  updatedAt: serverUpdatedAt ?? localUpdatedAt,
39
39
  };
40
40
  }
41
- function mapSelectedContainerRecord(row) {
41
+ export function mapSelectedContainerRecord(row) {
42
42
  const systemSlot = isContainerSystemSlot(row.systemSlot)
43
43
  ? row.systemSlot
44
44
  : null;
@@ -99,25 +99,29 @@ export async function saveContainerRows(input) {
99
99
  .run();
100
100
  return nextRecord;
101
101
  }
102
+ /** The ONE select shape for a container record list. */
103
+ export const containerRecordSelection = {
104
+ effectiveAccessLevel: containers.effectiveAccessLevel,
105
+ id: containers.id,
106
+ organizationId: containers.organizationId,
107
+ parentId: containers.parentId,
108
+ metadataDocumentId: containers.metadataDocumentId,
109
+ systemSlot: containers.systemSlot,
110
+ name: containerDisplayName,
111
+ icon: containers.icon,
112
+ localCreatedAt: containers.localCreatedAt,
113
+ localUpdatedAt: containers.localUpdatedAt,
114
+ serverCreatedAt: containers.serverCreatedAt,
115
+ serverUpdatedAt: containers.serverUpdatedAt,
116
+ };
117
+ /** Container lists sort by display name, case-insensitively. */
118
+ export const containerListOrder = asc(sql `${containerDisplayName} COLLATE NOCASE`);
102
119
  export async function loadContainers(execSql) {
103
120
  const { db } = getClientSQLitePersistenceRuntime(execSql);
104
121
  const rows = await db
105
- .select({
106
- effectiveAccessLevel: containers.effectiveAccessLevel,
107
- id: containers.id,
108
- organizationId: containers.organizationId,
109
- parentId: containers.parentId,
110
- metadataDocumentId: containers.metadataDocumentId,
111
- systemSlot: containers.systemSlot,
112
- name: containerDisplayName,
113
- icon: containers.icon,
114
- localCreatedAt: containers.localCreatedAt,
115
- localUpdatedAt: containers.localUpdatedAt,
116
- serverCreatedAt: containers.serverCreatedAt,
117
- serverUpdatedAt: containers.serverUpdatedAt,
118
- })
122
+ .select(containerRecordSelection)
119
123
  .from(containers)
120
- .orderBy(asc(sql `${containerDisplayName} COLLATE NOCASE`));
124
+ .orderBy(containerListOrder);
121
125
  return rows.map((row) => mapSelectedContainerRecord(row));
122
126
  }
123
127
  export async function loadContainerById(execSql, containerId) {
@@ -1 +1 @@
1
- {"version":3,"file":"containerPersistence.js","sourceRoot":"","sources":["containerPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ContainerAccessLevel } from \"@tearleads/crypto\";\nimport {\n type ContainerSystemSlot,\n isContainerSystemSlot,\n} from \"@tearleads/validators/containerSystemSlot\";\nimport { asc, eq, inArray, sql } from \"drizzle-orm\";\nimport {\n DEFAULT_EFFECTIVE_ACCESS_LEVEL,\n normalizeEffectiveAccessLevel,\n} from \"../../keying/accessLevel\";\nimport { containers, containerTables } from \"../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\n\nexport interface ContainerRecord {\n createdAt?: string | null;\n effectiveAccessLevel?: ContainerAccessLevel | null;\n id: string;\n organizationId: string;\n parentId: string | null;\n metadataDocumentId: string | null;\n systemSlot?: ContainerSystemSlot | null;\n name: string;\n icon: string | null;\n localCreatedAt?: string | null;\n localUpdatedAt?: string | null;\n serverCreatedAt?: string | null;\n serverUpdatedAt?: string | null;\n updatedAt?: string | null;\n}\n\ninterface ContainerRecordWithTimestampDefaults extends ContainerRecord {\n createdAt: string | null;\n localCreatedAt: string;\n localUpdatedAt: string;\n serverCreatedAt: string | null;\n serverUpdatedAt: string | null;\n updatedAt: string | null;\n}\n\nexport function ensureContainerTables(execSql: ExecSql): Promise<void> {\n return ensureSqlTables(execSql, containerTables);\n}\n\n// The ONE display-name fallback: a decrypted metadata name names the container;\n// without one the root reads '/' and everything else 'Untitled'.\nconst containerDisplayName = sql<string>`COALESCE(\n ${containers.displayName},\n CASE WHEN ${containers.parentId} IS NULL THEN '/' ELSE 'Untitled' END\n )`;\n\ninterface SelectedContainerRecord {\n effectiveAccessLevel: string | null;\n id: string | null;\n organizationId: string;\n parentId: string | null;\n metadataDocumentId: string | null;\n systemSlot: string | null;\n name: string;\n icon: string | null;\n localCreatedAt: string;\n localUpdatedAt: string;\n serverCreatedAt: string | null;\n serverUpdatedAt: string | null;\n}\n\nfunction toDisplayContainerRecord(record: ContainerRecord): ContainerRecord {\n return {\n ...record,\n createdAt: record.serverCreatedAt ?? record.localCreatedAt ?? null,\n updatedAt: record.serverUpdatedAt ?? record.localUpdatedAt ?? null,\n };\n}\n\nfunction applyContainerTimestampDefaults(\n record: ContainerRecord,\n localUpdatedAt: string,\n): ContainerRecordWithTimestampDefaults {\n const hasServerTimestamps =\n record.serverCreatedAt !== undefined ||\n record.serverUpdatedAt !== undefined;\n const localCreatedAt =\n record.localCreatedAt ??\n (!hasServerTimestamps ? record.createdAt : null) ??\n localUpdatedAt;\n const serverCreatedAt = record.serverCreatedAt ?? null;\n const serverUpdatedAt = record.serverUpdatedAt ?? null;\n\n return {\n ...record,\n createdAt: serverCreatedAt ?? localCreatedAt,\n localCreatedAt,\n localUpdatedAt,\n serverCreatedAt,\n serverUpdatedAt,\n updatedAt: serverUpdatedAt ?? localUpdatedAt,\n };\n}\n\nfunction mapSelectedContainerRecord(\n row: SelectedContainerRecord,\n): ContainerRecord {\n const systemSlot = isContainerSystemSlot(row.systemSlot)\n ? row.systemSlot\n : null;\n\n return toDisplayContainerRecord({\n effectiveAccessLevel: normalizeEffectiveAccessLevel(\n row.effectiveAccessLevel,\n ),\n id: String(row.id ?? \"\"),\n organizationId: row.organizationId,\n parentId: row.parentId,\n metadataDocumentId: row.metadataDocumentId,\n systemSlot,\n name: row.name,\n icon: row.icon && row.icon.length > 0 ? row.icon : null,\n localCreatedAt: row.localCreatedAt,\n localUpdatedAt: row.localUpdatedAt,\n serverCreatedAt: row.serverCreatedAt,\n serverUpdatedAt: row.serverUpdatedAt,\n });\n}\n\nexport async function saveContainerRows(input: {\n tx: ClientSQLiteTransactionScope;\n record: ContainerRecord;\n localUpdatedAt: string;\n}): Promise<ContainerRecord> {\n const { record, tx, localUpdatedAt } = input;\n const nextRecord = applyContainerTimestampDefaults(record, localUpdatedAt);\n const containerRow = {\n effectiveAccessLevel:\n nextRecord.effectiveAccessLevel ?? DEFAULT_EFFECTIVE_ACCESS_LEVEL,\n id: nextRecord.id,\n organizationId: nextRecord.organizationId,\n parentId: nextRecord.parentId,\n metadataDocumentId: nextRecord.metadataDocumentId,\n systemSlot: nextRecord.systemSlot ?? null,\n displayName: nextRecord.name,\n icon: nextRecord.icon,\n localCreatedAt: nextRecord.localCreatedAt,\n localUpdatedAt,\n serverCreatedAt: nextRecord.serverCreatedAt,\n serverUpdatedAt: nextRecord.serverUpdatedAt,\n };\n await tx\n .insert(containers)\n .values(containerRow)\n .onConflictDoUpdate({\n target: containers.id,\n set: {\n organizationId: containerRow.organizationId,\n parentId: containerRow.parentId,\n metadataDocumentId: containerRow.metadataDocumentId,\n systemSlot: containerRow.systemSlot,\n displayName: containerRow.displayName,\n icon: containerRow.icon,\n effectiveAccessLevel: containerRow.effectiveAccessLevel,\n localUpdatedAt: containerRow.localUpdatedAt,\n serverCreatedAt:\n record.serverCreatedAt === undefined\n ? sql`${containers.serverCreatedAt}`\n : containerRow.serverCreatedAt,\n serverUpdatedAt:\n record.serverUpdatedAt === undefined\n ? sql`${containers.serverUpdatedAt}`\n : containerRow.serverUpdatedAt,\n },\n })\n .run();\n\n return nextRecord;\n}\n\nexport async function loadContainers(\n execSql: ExecSql,\n): Promise<ContainerRecord[]> {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n effectiveAccessLevel: containers.effectiveAccessLevel,\n id: containers.id,\n organizationId: containers.organizationId,\n parentId: containers.parentId,\n metadataDocumentId: containers.metadataDocumentId,\n systemSlot: containers.systemSlot,\n name: containerDisplayName,\n icon: containers.icon,\n localCreatedAt: containers.localCreatedAt,\n localUpdatedAt: containers.localUpdatedAt,\n serverCreatedAt: containers.serverCreatedAt,\n serverUpdatedAt: containers.serverUpdatedAt,\n })\n .from(containers)\n .orderBy(asc(sql`${containerDisplayName} COLLATE NOCASE`));\n\n return rows.map((row) => mapSelectedContainerRecord(row));\n}\n\nexport async function loadContainerById(\n execSql: ExecSql,\n containerId: string,\n): Promise<ContainerRecord | null> {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n effectiveAccessLevel: containers.effectiveAccessLevel,\n id: containers.id,\n organizationId: containers.organizationId,\n parentId: containers.parentId,\n metadataDocumentId: containers.metadataDocumentId,\n systemSlot: containers.systemSlot,\n name: containerDisplayName,\n icon: containers.icon,\n localCreatedAt: containers.localCreatedAt,\n localUpdatedAt: containers.localUpdatedAt,\n serverCreatedAt: containers.serverCreatedAt,\n serverUpdatedAt: containers.serverUpdatedAt,\n })\n .from(containers)\n .where(eq(containers.id, containerId))\n .limit(1);\n\n return rows[0] ? mapSelectedContainerRecord(rows[0]) : null;\n}\n\nexport async function loadContainerDisplayNamesByIds(\n execSql: ExecSql,\n ids: ReadonlyArray<string>,\n): Promise<Map<string, string>> {\n const uniqueIds = Array.from(new Set(ids));\n if (uniqueIds.length === 0) {\n return new Map();\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n id: containers.id,\n name: containerDisplayName,\n })\n .from(containers)\n .where(inArray(containers.id, uniqueIds));\n\n return new Map(\n rows.flatMap((row) => {\n const displayName = row.name.trim();\n if (!row.id || displayName.length === 0) {\n return [];\n }\n\n return [[row.id, displayName]];\n }),\n );\n}\n\nexport async function saveContainer(\n execSql: ExecSql,\n record: ContainerRecord,\n): Promise<void> {\n const localUpdatedAt = new Date().toISOString();\n\n await getClientSQLitePersistenceRuntime(execSql).transaction(async (tx) => {\n await saveContainerRows({\n record,\n tx,\n localUpdatedAt,\n });\n });\n}\n\n/**\n * The container-row half of the delete cascade, statement-only so callers\n * can compose it inside one atomic transaction.\n */\nexport async function deleteContainerRowsInTransaction(\n tx: ClientSQLiteTransactionScope,\n ids: ReadonlyArray<string>,\n): Promise<void> {\n const uniqueIds = Array.from(new Set(ids));\n if (uniqueIds.length === 0) {\n return;\n }\n await tx.delete(containers).where(inArray(containers.id, uniqueIds)).run();\n}\n"]}
1
+ {"version":3,"file":"containerPersistence.js","sourceRoot":"","sources":["containerPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ContainerAccessLevel } from \"@tearleads/crypto\";\nimport {\n type ContainerSystemSlot,\n isContainerSystemSlot,\n} from \"@tearleads/validators/containerSystemSlot\";\nimport { asc, eq, inArray, sql } from \"drizzle-orm\";\nimport {\n DEFAULT_EFFECTIVE_ACCESS_LEVEL,\n normalizeEffectiveAccessLevel,\n} from \"../../keying/accessLevel\";\nimport { containers, containerTables } from \"../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\n\nexport interface ContainerRecord {\n createdAt?: string | null;\n effectiveAccessLevel?: ContainerAccessLevel | null;\n id: string;\n organizationId: string;\n parentId: string | null;\n metadataDocumentId: string | null;\n systemSlot?: ContainerSystemSlot | null;\n name: string;\n icon: string | null;\n localCreatedAt?: string | null;\n localUpdatedAt?: string | null;\n serverCreatedAt?: string | null;\n serverUpdatedAt?: string | null;\n updatedAt?: string | null;\n}\n\ninterface ContainerRecordWithTimestampDefaults extends ContainerRecord {\n createdAt: string | null;\n localCreatedAt: string;\n localUpdatedAt: string;\n serverCreatedAt: string | null;\n serverUpdatedAt: string | null;\n updatedAt: string | null;\n}\n\nexport function ensureContainerTables(execSql: ExecSql): Promise<void> {\n return ensureSqlTables(execSql, containerTables);\n}\n\n// The ONE display-name fallback: a decrypted metadata name names the container;\n// without one the root reads '/' and everything else 'Untitled'.\nconst containerDisplayName = sql<string>`COALESCE(\n ${containers.displayName},\n CASE WHEN ${containers.parentId} IS NULL THEN '/' ELSE 'Untitled' END\n )`;\n\ninterface SelectedContainerRecord {\n effectiveAccessLevel: string | null;\n id: string | null;\n organizationId: string;\n parentId: string | null;\n metadataDocumentId: string | null;\n systemSlot: string | null;\n name: string;\n icon: string | null;\n localCreatedAt: string;\n localUpdatedAt: string;\n serverCreatedAt: string | null;\n serverUpdatedAt: string | null;\n}\n\nfunction toDisplayContainerRecord(record: ContainerRecord): ContainerRecord {\n return {\n ...record,\n createdAt: record.serverCreatedAt ?? record.localCreatedAt ?? null,\n updatedAt: record.serverUpdatedAt ?? record.localUpdatedAt ?? null,\n };\n}\n\nfunction applyContainerTimestampDefaults(\n record: ContainerRecord,\n localUpdatedAt: string,\n): ContainerRecordWithTimestampDefaults {\n const hasServerTimestamps =\n record.serverCreatedAt !== undefined ||\n record.serverUpdatedAt !== undefined;\n const localCreatedAt =\n record.localCreatedAt ??\n (!hasServerTimestamps ? record.createdAt : null) ??\n localUpdatedAt;\n const serverCreatedAt = record.serverCreatedAt ?? null;\n const serverUpdatedAt = record.serverUpdatedAt ?? null;\n\n return {\n ...record,\n createdAt: serverCreatedAt ?? localCreatedAt,\n localCreatedAt,\n localUpdatedAt,\n serverCreatedAt,\n serverUpdatedAt,\n updatedAt: serverUpdatedAt ?? localUpdatedAt,\n };\n}\n\nexport function mapSelectedContainerRecord(\n row: SelectedContainerRecord,\n): ContainerRecord {\n const systemSlot = isContainerSystemSlot(row.systemSlot)\n ? row.systemSlot\n : null;\n\n return toDisplayContainerRecord({\n effectiveAccessLevel: normalizeEffectiveAccessLevel(\n row.effectiveAccessLevel,\n ),\n id: String(row.id ?? \"\"),\n organizationId: row.organizationId,\n parentId: row.parentId,\n metadataDocumentId: row.metadataDocumentId,\n systemSlot,\n name: row.name,\n icon: row.icon && row.icon.length > 0 ? row.icon : null,\n localCreatedAt: row.localCreatedAt,\n localUpdatedAt: row.localUpdatedAt,\n serverCreatedAt: row.serverCreatedAt,\n serverUpdatedAt: row.serverUpdatedAt,\n });\n}\n\nexport async function saveContainerRows(input: {\n tx: ClientSQLiteTransactionScope;\n record: ContainerRecord;\n localUpdatedAt: string;\n}): Promise<ContainerRecord> {\n const { record, tx, localUpdatedAt } = input;\n const nextRecord = applyContainerTimestampDefaults(record, localUpdatedAt);\n const containerRow = {\n effectiveAccessLevel:\n nextRecord.effectiveAccessLevel ?? DEFAULT_EFFECTIVE_ACCESS_LEVEL,\n id: nextRecord.id,\n organizationId: nextRecord.organizationId,\n parentId: nextRecord.parentId,\n metadataDocumentId: nextRecord.metadataDocumentId,\n systemSlot: nextRecord.systemSlot ?? null,\n displayName: nextRecord.name,\n icon: nextRecord.icon,\n localCreatedAt: nextRecord.localCreatedAt,\n localUpdatedAt,\n serverCreatedAt: nextRecord.serverCreatedAt,\n serverUpdatedAt: nextRecord.serverUpdatedAt,\n };\n await tx\n .insert(containers)\n .values(containerRow)\n .onConflictDoUpdate({\n target: containers.id,\n set: {\n organizationId: containerRow.organizationId,\n parentId: containerRow.parentId,\n metadataDocumentId: containerRow.metadataDocumentId,\n systemSlot: containerRow.systemSlot,\n displayName: containerRow.displayName,\n icon: containerRow.icon,\n effectiveAccessLevel: containerRow.effectiveAccessLevel,\n localUpdatedAt: containerRow.localUpdatedAt,\n serverCreatedAt:\n record.serverCreatedAt === undefined\n ? sql`${containers.serverCreatedAt}`\n : containerRow.serverCreatedAt,\n serverUpdatedAt:\n record.serverUpdatedAt === undefined\n ? sql`${containers.serverUpdatedAt}`\n : containerRow.serverUpdatedAt,\n },\n })\n .run();\n\n return nextRecord;\n}\n\n/** The ONE select shape for a container record list. */\nexport const containerRecordSelection = {\n effectiveAccessLevel: containers.effectiveAccessLevel,\n id: containers.id,\n organizationId: containers.organizationId,\n parentId: containers.parentId,\n metadataDocumentId: containers.metadataDocumentId,\n systemSlot: containers.systemSlot,\n name: containerDisplayName,\n icon: containers.icon,\n localCreatedAt: containers.localCreatedAt,\n localUpdatedAt: containers.localUpdatedAt,\n serverCreatedAt: containers.serverCreatedAt,\n serverUpdatedAt: containers.serverUpdatedAt,\n};\n\n/** Container lists sort by display name, case-insensitively. */\nexport const containerListOrder = asc(\n sql`${containerDisplayName} COLLATE NOCASE`,\n);\n\nexport async function loadContainers(\n execSql: ExecSql,\n): Promise<ContainerRecord[]> {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select(containerRecordSelection)\n .from(containers)\n .orderBy(containerListOrder);\n\n return rows.map((row) => mapSelectedContainerRecord(row));\n}\n\nexport async function loadContainerById(\n execSql: ExecSql,\n containerId: string,\n): Promise<ContainerRecord | null> {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n effectiveAccessLevel: containers.effectiveAccessLevel,\n id: containers.id,\n organizationId: containers.organizationId,\n parentId: containers.parentId,\n metadataDocumentId: containers.metadataDocumentId,\n systemSlot: containers.systemSlot,\n name: containerDisplayName,\n icon: containers.icon,\n localCreatedAt: containers.localCreatedAt,\n localUpdatedAt: containers.localUpdatedAt,\n serverCreatedAt: containers.serverCreatedAt,\n serverUpdatedAt: containers.serverUpdatedAt,\n })\n .from(containers)\n .where(eq(containers.id, containerId))\n .limit(1);\n\n return rows[0] ? mapSelectedContainerRecord(rows[0]) : null;\n}\n\nexport async function loadContainerDisplayNamesByIds(\n execSql: ExecSql,\n ids: ReadonlyArray<string>,\n): Promise<Map<string, string>> {\n const uniqueIds = Array.from(new Set(ids));\n if (uniqueIds.length === 0) {\n return new Map();\n }\n\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({\n id: containers.id,\n name: containerDisplayName,\n })\n .from(containers)\n .where(inArray(containers.id, uniqueIds));\n\n return new Map(\n rows.flatMap((row) => {\n const displayName = row.name.trim();\n if (!row.id || displayName.length === 0) {\n return [];\n }\n\n return [[row.id, displayName]];\n }),\n );\n}\n\nexport async function saveContainer(\n execSql: ExecSql,\n record: ContainerRecord,\n): Promise<void> {\n const localUpdatedAt = new Date().toISOString();\n\n await getClientSQLitePersistenceRuntime(execSql).transaction(async (tx) => {\n await saveContainerRows({\n record,\n tx,\n localUpdatedAt,\n });\n });\n}\n\n/**\n * The container-row half of the delete cascade, statement-only so callers\n * can compose it inside one atomic transaction.\n */\nexport async function deleteContainerRowsInTransaction(\n tx: ClientSQLiteTransactionScope,\n ids: ReadonlyArray<string>,\n): Promise<void> {\n const uniqueIds = Array.from(new Set(ids));\n if (uniqueIds.length === 0) {\n return;\n }\n await tx.delete(containers).where(inArray(containers.id, uniqueIds)).run();\n}\n"]}
@@ -8,8 +8,32 @@ import { recordContainerHydrationTombstones } from "./containerHydrationPersiste
8
8
  import { repairDocumentsForRemovedContainersInTransaction } from "./containerStructuralRepair.js";
9
9
  import { discardLinkIntentsForRemovedContainers } from "./documentLinkRemovalRepair.js";
10
10
  import { deleteContainerMetadataDocumentRowsInTransaction } from "./dormantContainerMetadata.js";
11
+ const RECOVERY_METADATA_TABLES = [
12
+ [
13
+ "documents",
14
+ "document_id, recovery_document_id, recovery_generation, snapshot_end_version, pending_base_version",
15
+ ],
16
+ ["document_history_checkpoints", "snapshot, end_version_vector"],
17
+ [
18
+ "document_history_updates",
19
+ "id, update_data, origin, update_id, partial_start_version_vector, partial_end_version_vector, source_version_vector",
20
+ ],
21
+ ];
22
+ const CREATE_COLUMNS = "id, subject_id, target_container_id, intent_type, remote_container_id, remote_metadata_document_id, remote_metadata_access_state_hash";
23
+ const MOVE_COLUMNS = "id, subject_id, target_container_id, source_container_id, intent_type";
24
+ const LINK_COLUMNS = "id, local_id, subject_id, target_container_id, source_container_id, replace_linked_containers, intent_type";
25
+ const TARGET_COLUMNS = "intent_id, operation, container_id";
11
26
  // The token covers all local work the user is agreeing to discard, including
12
27
  // shared link intents. A second pane's edit invalidates the confirmation.
28
+ async function summarizeRecoverySnapshot(input) {
29
+ const { creates, dormant, links, metadata, moves, targets } = input;
30
+ const revision = await toFingerprint(new TextEncoder().encode(JSON.stringify([dormant, metadata, creates, moves, links, targets])));
31
+ return {
32
+ revision,
33
+ pendingUpdateCount: (metadata[2] ?? []).filter((row) => Reflect.get(row, "update_id") !== null).length,
34
+ hasStructuralIntent: creates.length + moves.length + links.length > 0,
35
+ };
36
+ }
13
37
  async function recoverySnapshot(execSql, containerId, organizationId) {
14
38
  const [dormant] = await execSql(`SELECT container_id, organization_id FROM dormant_container_metadata
15
39
  WHERE container_id = ? AND organization_id = ?
@@ -17,21 +41,11 @@ async function recoverySnapshot(execSql, containerId, organizationId) {
17
41
  if (!dormant)
18
42
  return null;
19
43
  const metadata = [];
20
- for (const [table, columns] of [
21
- [
22
- "documents",
23
- "document_id, recovery_document_id, recovery_generation, snapshot_end_version, pending_base_version",
24
- ],
25
- ["document_history_checkpoints", "snapshot, end_version_vector"],
26
- [
27
- "document_history_updates",
28
- "id, update_data, origin, update_id, partial_start_version_vector, partial_end_version_vector, source_version_vector",
29
- ],
30
- ]) {
44
+ for (const [table, columns] of RECOVERY_METADATA_TABLES) {
31
45
  metadata.push(await execSql(`SELECT ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id = ? ORDER BY rowid`, [containerId]));
32
46
  }
33
- const creates = await execSql("SELECT id, subject_id, target_container_id, intent_type, remote_container_id, remote_metadata_document_id, remote_metadata_access_state_hash FROM local_intents WHERE lane = ? AND subject_id = ? AND sync_status = 'pending' ORDER BY id", [LOCAL_INTENT_LANES.containerCreate, containerId]);
34
- const moves = await execSql("SELECT id, subject_id, target_container_id, source_container_id, intent_type FROM local_intents WHERE lane = ? AND subject_id = ? ORDER BY id", [LOCAL_INTENT_LANES.containerMove, containerId]);
47
+ const creates = await execSql(`SELECT ${CREATE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id = ? AND sync_status = 'pending' ORDER BY id`, [LOCAL_INTENT_LANES.containerCreate, containerId]);
48
+ const moves = await execSql(`SELECT ${MOVE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id = ? ORDER BY id`, [LOCAL_INTENT_LANES.containerMove, containerId]);
35
49
  const placementWhere = `lane = ? AND (target_container_id = ? OR id IN
36
50
  (SELECT intent_id FROM document_intent_link_targets WHERE container_id = ?))`;
37
51
  const placementBind = [
@@ -39,40 +53,140 @@ async function recoverySnapshot(execSql, containerId, organizationId) {
39
53
  containerId,
40
54
  containerId,
41
55
  ];
42
- const links = await execSql(`SELECT id, local_id, subject_id, target_container_id, source_container_id, replace_linked_containers, intent_type FROM local_intents WHERE ${placementWhere} ORDER BY id`, placementBind);
43
- const targets = await execSql(`SELECT intent_id, operation, container_id FROM document_intent_link_targets WHERE intent_id IN
56
+ const links = await execSql(`SELECT ${LINK_COLUMNS} FROM local_intents WHERE ${placementWhere} ORDER BY id`, placementBind);
57
+ const targets = await execSql(`SELECT ${TARGET_COLUMNS} FROM document_intent_link_targets WHERE intent_id IN
44
58
  (SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`, placementBind);
45
- const revision = await toFingerprint(new TextEncoder().encode(JSON.stringify([dormant, metadata, creates, moves, links, targets])));
59
+ return summarizeRecoverySnapshot({
60
+ creates,
61
+ dormant,
62
+ links,
63
+ metadata,
64
+ moves,
65
+ targets,
66
+ });
67
+ }
68
+ /** Rows grouped by one column's value, dropping that column when asked. */
69
+ function groupRows(rows, column, drop) {
70
+ const groups = new Map();
71
+ for (const row of rows) {
72
+ const key = String(Reflect.get(row, column));
73
+ const kept = drop
74
+ ? Object.fromEntries(Object.entries(row).filter(([name]) => name !== column))
75
+ : row;
76
+ const group = groups.get(key);
77
+ if (group)
78
+ group.push(kept);
79
+ else
80
+ groups.set(key, [kept]);
81
+ }
82
+ return groups;
83
+ }
84
+ /** Keeps SQL `IN` lists, bound twice by the placement reads, under the budget. */
85
+ const RECOVERY_CONTAINER_BATCH_SIZE = 400;
86
+ /** Every row the recovery snapshots of a batch of folders read. */
87
+ async function readRecoveryBatchRows(execSql, ids) {
88
+ const list = ids.map(() => "?").join(", ");
89
+ const metadata = [];
90
+ for (const [table, columns] of RECOVERY_METADATA_TABLES) {
91
+ metadata.push(groupRows(await execSql(`SELECT local_id, ${columns} FROM ${table} WHERE app_kind = 'container-metadata' AND local_id IN (${list}) ORDER BY local_id, rowid`, ids), "local_id", true));
92
+ }
93
+ const creates = groupRows(await execSql(`SELECT ${CREATE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id IN (${list}) AND sync_status = 'pending' ORDER BY subject_id, id`, [LOCAL_INTENT_LANES.containerCreate, ...ids]), "subject_id", false);
94
+ const moves = groupRows(await execSql(`SELECT ${MOVE_COLUMNS} FROM local_intents WHERE lane = ? AND subject_id IN (${list}) ORDER BY subject_id, id`, [LOCAL_INTENT_LANES.containerMove, ...ids]), "subject_id", false);
95
+ const placementWhere = `lane = ? AND (target_container_id IN (${list}) OR id IN
96
+ (SELECT intent_id FROM document_intent_link_targets WHERE container_id IN (${list})))`;
97
+ const placementBind = [LOCAL_INTENT_LANES.documentPlacement, ...ids, ...ids];
98
+ // Every placement intent any folder in the batch reaches, and ALL link
99
+ // targets of those intents: a folder's share is then the intents that
100
+ // target it or carry a link target in it, as the per-folder read selects.
101
+ const links = await execSql(`SELECT ${LINK_COLUMNS} FROM local_intents WHERE ${placementWhere} ORDER BY id`, placementBind);
102
+ const targets = await execSql(`SELECT ${TARGET_COLUMNS} FROM document_intent_link_targets WHERE intent_id IN
103
+ (SELECT id FROM local_intents WHERE ${placementWhere}) ORDER BY intent_id, container_id, operation`, placementBind);
104
+ return { creates, links, metadata, moves, targets };
105
+ }
106
+ /**
107
+ * Each folder's share of a batch's placement rows, indexed in one pass: the
108
+ * intents that target the folder or carry a link target in it (in intent id
109
+ * order), and all link targets of those intents (in the per-folder order).
110
+ */
111
+ function indexPlacementRows(rows) {
112
+ const targetsByIntent = groupRows(rows.targets, "intent_id", false);
113
+ const linksByFolder = new Map();
114
+ for (const link of rows.links) {
115
+ const intentId = String(Reflect.get(link, "id"));
116
+ const folders = new Set([
117
+ String(Reflect.get(link, "target_container_id")),
118
+ ...(targetsByIntent.get(intentId) ?? []).map((target) => String(Reflect.get(target, "container_id"))),
119
+ ]);
120
+ for (const folder of folders) {
121
+ const folderLinks = linksByFolder.get(folder);
122
+ if (folderLinks)
123
+ folderLinks.push(link);
124
+ else
125
+ linksByFolder.set(folder, [link]);
126
+ }
127
+ }
128
+ return { linksByFolder, targetsByIntent };
129
+ }
130
+ /** One folder's snapshot from its batch's rows, in the per-folder shape. */
131
+ async function folderRecoverySnapshot(rows, placements, containerId, dormant) {
132
+ const links = placements.linksByFolder.get(containerId) ?? [];
133
+ // Links are in intent id order and targets in (intent id, container,
134
+ // operation) order, so concatenating per link keeps the per-folder order.
135
+ const targets = links.flatMap((link) => placements.targetsByIntent.get(String(Reflect.get(link, "id"))) ?? []);
136
+ const metadata = rows.metadata.map((byFolder) => byFolder.get(containerId) ?? []);
137
+ // The metadata record's updates are its checkpoint snapshot, read above; a
138
+ // folder with no record has none.
139
+ const snapshotRow = (metadata[1] ?? [])[0];
46
140
  return {
47
- revision,
48
- pendingUpdateCount: (metadata[2] ?? []).filter((row) => Reflect.get(row, "update_id") !== null).length,
49
- hasStructuralIntent: creates.length + moves.length + links.length > 0,
141
+ containerId,
142
+ metadataUpdates: (metadata[0] ?? []).length > 0 && snapshotRow
143
+ ? String(Reflect.get(snapshotRow, "snapshot") ?? "")
144
+ : "",
145
+ snapshot: await summarizeRecoverySnapshot({
146
+ creates: rows.creates.get(containerId) ?? [],
147
+ dormant,
148
+ links,
149
+ metadata,
150
+ moves: rows.moves.get(containerId) ?? [],
151
+ targets,
152
+ }),
50
153
  };
51
154
  }
155
+ /**
156
+ * `recoverySnapshot` for every retained folder of an organization, one read
157
+ * per table for each batch of folders. Each folder's rows are grouped in the
158
+ * per-folder order (`local_id, rowid` for metadata; `subject_id, id` for
159
+ * folder intents), so its revision is byte-identical to `recoverySnapshot`'s,
160
+ * which a later discard compares.
161
+ */
162
+ async function recoverySnapshots(execSql, organizationId) {
163
+ const dormantRows = await execSql(`SELECT container_id, organization_id FROM dormant_container_metadata d
164
+ WHERE organization_id = ?
165
+ AND NOT EXISTS (SELECT 1 FROM containers WHERE id = d.container_id)
166
+ ORDER BY container_id`, [organizationId]);
167
+ const result = [];
168
+ for (let offset = 0; offset < dormantRows.length; offset += RECOVERY_CONTAINER_BATCH_SIZE) {
169
+ const batch = dormantRows.slice(offset, offset + RECOVERY_CONTAINER_BATCH_SIZE);
170
+ const ids = batch.map((row) => String(Reflect.get(row, "container_id")));
171
+ const rows = await readRecoveryBatchRows(execSql, ids);
172
+ const placements = indexPlacementRows(rows);
173
+ for (const [index, dormant] of batch.entries()) {
174
+ result.push(await folderRecoverySnapshot(rows, placements, ids[index] ?? "", dormant));
175
+ }
176
+ }
177
+ return result;
178
+ }
52
179
  export async function listRetainedContainerMetadata(execSql, organizationId) {
53
180
  if (!organizationId)
54
181
  return [];
55
182
  await sqlContainerContentsPersistence.ensureSchema(execSql);
56
183
  await ensureSqlTables(execSql, documentPlacementIntentTables);
57
- return runSerializedSqlMutation(execSql, async (locked) => getClientSQLitePersistenceRuntime(locked).transaction(async () => {
58
- const rows = await locked(`SELECT container_id FROM dormant_container_metadata
59
- WHERE organization_id = ? ORDER BY container_id`, [organizationId]);
60
- const result = [];
61
- for (const row of rows) {
62
- const containerId = String(Reflect.get(row, "container_id"));
63
- const snapshot = await recoverySnapshot(locked, containerId, organizationId);
64
- if (!snapshot)
65
- continue;
66
- const record = await sqlContainerContentsPersistence.loadContainerMetadataRecord(locked, containerId);
67
- result.push({
68
- containerId,
69
- organizationId,
70
- ...snapshot,
71
- metadataUpdates: record?.metadataUpdates ?? "",
72
- });
73
- }
74
- return result;
75
- }));
184
+ return runSerializedSqlMutation(execSql, async (locked) => getClientSQLitePersistenceRuntime(locked).transaction(async () => (await recoverySnapshots(locked, organizationId)).map(({ containerId, metadataUpdates, snapshot }) => ({
185
+ containerId,
186
+ organizationId,
187
+ ...snapshot,
188
+ metadataUpdates,
189
+ }))));
76
190
  }
77
191
  /** Explicit local discard, never called by remote discovery. */
78
192
  export async function discardRetainedContainerMetadata(execSql, input) {
@@ -116,24 +230,26 @@ export async function discardRetainedContainerMetadata(execSql, input) {
116
230
  return true;
117
231
  }, { behavior: "immediate" }));
118
232
  }
119
- export async function hasRetainedContainerMetadata(execSql, organizationId) {
120
- if (!organizationId)
121
- return false;
122
- await sqlContainerContentsPersistence.ensureSchema(execSql);
123
- const rows = await execSql(`SELECT 1 FROM dormant_container_metadata d
124
- WHERE d.organization_id = ? AND NOT EXISTS (SELECT 1 FROM containers c WHERE c.id = d.container_id) LIMIT 1`, [organizationId]);
125
- return rows.length > 0;
126
- }
233
+ /** Retained metadata of an organization's absent folders; binds the org. */
234
+ export const RETAINED_CONTAINER_METADATA_SQL = `SELECT 1 FROM dormant_container_metadata d
235
+ WHERE d.organization_id = ? AND NOT EXISTS (SELECT 1 FROM containers c WHERE c.id = d.container_id)`;
236
+ /**
237
+ * An organization's local folders whose queued move points at an unavailable
238
+ * parent; binds the move lane, then the organization.
239
+ */
240
+ export const RECOVERY_FOLDER_MOVES_SQL = `SELECT c.id FROM containers c JOIN local_intents i
241
+ ON i.lane = ? AND i.subject_id = c.id AND i.target_container_id = c.parent_id
242
+ WHERE c.organization_id = ? AND i.sync_status IN ('pending', 'blocked')
243
+ AND NOT EXISTS (SELECT 1 FROM containers p WHERE p.id = c.parent_id)`;
127
244
  /** Existing local folders whose queued move points at an unavailable parent. */
128
245
  export async function listRecoveryFolderMoveIds(execSql, organizationId) {
129
246
  if (!organizationId)
130
247
  return [];
131
248
  await sqlContainerContentsPersistence.ensureSchema(execSql);
132
- const rows = await execSql(`SELECT c.id FROM containers c JOIN local_intents i
133
- ON i.lane = ? AND i.subject_id = c.id AND i.target_container_id = c.parent_id
134
- WHERE c.organization_id = ? AND i.sync_status IN ('pending', 'blocked')
135
- AND NOT EXISTS (SELECT 1 FROM containers p WHERE p.id = c.parent_id)
136
- ORDER BY c.id`, [LOCAL_INTENT_LANES.containerMove, organizationId]);
249
+ const rows = await execSql(`${RECOVERY_FOLDER_MOVES_SQL} ORDER BY c.id`, [
250
+ LOCAL_INTENT_LANES.containerMove,
251
+ organizationId,
252
+ ]);
137
253
  return rows.map((row) => String(Reflect.get(row, "id")));
138
254
  }
139
255
  //# sourceMappingURL=containerRecoveryPersistence.js.map