@tearleads/client-sdk 0.1.43 → 0.1.44

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 (82) hide show
  1. package/client/containerDocumentDiscovery.js +71 -29
  2. package/client/containerDocumentDiscovery.js.map +1 -1
  3. package/client/deviceFirst.js +4 -5
  4. package/client/deviceFirst.js.map +1 -1
  5. package/data/persistence/containers/containerSyncWatermarkPersistence.d.ts +11 -3
  6. package/data/persistence/containers/containerSyncWatermarkPersistence.js +46 -58
  7. package/data/persistence/containers/containerSyncWatermarkPersistence.js.map +1 -1
  8. package/data/sqlite/schema.d.ts +34 -134
  9. package/data/sqlite/schema.js +22 -35
  10. package/data/sqlite/schema.js.map +1 -1
  11. package/internal/api-client/ApiClient.d.ts +6 -34
  12. package/internal/api-client/ApiClient.js +8 -10
  13. package/internal/api-client/ApiClient.js.map +1 -1
  14. package/internal/api-client/apiRequestRuntime.d.ts +2 -0
  15. package/internal/api-client/apiRequestRuntime.js +4 -0
  16. package/internal/api-client/apiRequestRuntime.js.map +1 -1
  17. package/internal/api-client/requestInternals.js +7 -7
  18. package/internal/api-client/requestInternals.js.map +1 -1
  19. package/internal/api-client/routes/containers/reads.d.ts +13 -7
  20. package/internal/api-client/routes/containers/reads.js +36 -15
  21. package/internal/api-client/routes/containers/reads.js.map +1 -1
  22. package/internal/api-client/types.d.ts +1 -5
  23. package/internal/api-client/types.js.map +1 -1
  24. package/internal/validators/containerReadRefinements.js +15 -5
  25. package/internal/validators/containerReadRefinements.js.map +1 -1
  26. package/internal/validators/containerReadSchemas.d.ts +36 -0
  27. package/internal/validators/containerReadSchemas.js +64 -1
  28. package/internal/validators/containerReadSchemas.js.map +1 -1
  29. package/internal/validators/operation/containerReads.d.ts +118 -182
  30. package/internal/validators/operation/containerReads.js +20 -54
  31. package/internal/validators/operation/containerReads.js.map +1 -1
  32. package/internal/validators/operation/index.d.ts +1 -1
  33. package/internal/validators/operation/index.js +1 -1
  34. package/internal/validators/operation/index.js.map +1 -1
  35. package/internal/validators/operation/registry.d.ts +2 -2
  36. package/internal/validators/operation/registry.js +2 -2
  37. package/internal/validators/operation/registry.js.map +1 -1
  38. package/internal/validators/request/containerDocumentLanes.d.ts +23 -0
  39. package/internal/validators/request/containerDocumentLanes.js +23 -0
  40. package/internal/validators/request/containerDocumentLanes.js.map +1 -0
  41. package/internal/validators/request/containerParentLanes.js +5 -35
  42. package/internal/validators/request/containerParentLanes.js.map +1 -1
  43. package/internal/validators/request/index.d.ts +1 -0
  44. package/internal/validators/request/index.js +1 -0
  45. package/internal/validators/request/index.js.map +1 -1
  46. package/internal/validators/response/containerDocumentLanes.d.ts +82 -0
  47. package/internal/validators/response/containerDocumentLanes.js +35 -0
  48. package/internal/validators/response/containerDocumentLanes.js.map +1 -0
  49. package/internal/validators/response/containerParentLanes.js +3 -15
  50. package/internal/validators/response/containerParentLanes.js.map +1 -1
  51. package/internal/validators/response/index.d.ts +2 -1
  52. package/internal/validators/response/index.js +1 -1
  53. package/internal/validators/response/index.js.map +1 -1
  54. package/package.json +1 -1
  55. package/sync/reconciliation/knownContainerSweep.js +57 -54
  56. package/sync/reconciliation/knownContainerSweep.js.map +1 -1
  57. package/sync/reconciliation/reconcileContainer.js +49 -17
  58. package/sync/reconciliation/reconcileContainer.js.map +1 -1
  59. package/sync/reconciliation/service.js +44 -16
  60. package/sync/reconciliation/service.js.map +1 -1
  61. package/sync/reconciliation/serviceTypes.d.ts +23 -2
  62. package/sync/reconciliation/serviceTypes.js.map +1 -1
  63. package/workflows/container-contents/containerDocumentListing.js +92 -38
  64. package/workflows/container-contents/containerDocumentListing.js.map +1 -1
  65. package/workflows/container-contents/containerPersistence.d.ts +8 -2
  66. package/workflows/container-contents/containerPersistence.js +6 -7
  67. package/workflows/container-contents/containerPersistence.js.map +1 -1
  68. package/workflows/container-contents/documentDiscovery.d.ts +3 -3
  69. package/workflows/container-contents/documentDiscovery.js +24 -96
  70. package/workflows/container-contents/documentDiscovery.js.map +1 -1
  71. package/workflows/container-contents/documentDiscoveryApply.js +65 -0
  72. package/workflows/container-contents/documentDiscoveryApply.js.map +1 -0
  73. package/workflows/container-contents/documentDiscoveryLanes.js +53 -0
  74. package/workflows/container-contents/documentDiscoveryLanes.js.map +1 -0
  75. package/workflows/container-contents/documentDiscoveryTypes.d.ts +25 -19
  76. package/workflows/container-contents/documentDiscoveryTypes.js.map +1 -1
  77. package/workflows/container-contents/remoteHydration/laneFetchMarkers.js +11 -17
  78. package/workflows/container-contents/remoteHydration/laneFetchMarkers.js.map +1 -1
  79. package/workflows/sync/remoteResetCursorScope.js +10 -18
  80. package/workflows/sync/remoteResetCursorScope.js.map +1 -1
  81. package/workflows/sync/remoteResetScope.js +4 -11
  82. package/workflows/sync/remoteResetScope.js.map +1 -1
@@ -1,48 +1,90 @@
1
1
  import { sqlContainerContentsPersistence } from "../data/persistence/container-contents/containerContentsPersistence.js";
2
2
  import { holdContainerDocumentTombstones, listKnownContainerDocumentPlacements, listRetryableHeldContainerDocumentTombstones, loadLocalDocumentAccessEpoch, refuteContainerDocumentTombstoneHolds, } from "../data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js";
3
3
  import { createDocumentDiscoveryEvidenceStore } from "../data/persistence/documents/documentDiscoveryEvidencePersistence.js";
4
- import { discoverContainerDocumentsFromApi } from "../workflows/container-contents/documentDiscovery.js";
4
+ import { listContainerDocumentLanesFromApi } from "../workflows/container-contents/containerDocumentListing.js";
5
5
  import { createDiscoveredDocumentVerifier } from "../workflows/container-contents/documentDiscoveryEvidence.js";
6
+ import { discoverContainerDocumentLanes, } from "../workflows/container-contents/documentDiscoveryLanes.js";
6
7
  import { createContainerDocumentQueriesFromRuntime } from "../workflows/container-contents/documentQueries.js";
7
8
  import { createContainerDocumentTombstoneVerifier, createDocumentHeadLinkSetLoader, } from "../workflows/container-contents/documentTombstoneEvidence.js";
8
- import { createContainerContentsWorkflowRuntime } from "../workflows/container-contents/runtime.js";
9
+ import { createContainerContentsWorkflowRuntime, } from "../workflows/container-contents/runtime.js";
9
10
  import { createRuntimePrincipalPolicyWarmer } from "../workflows/principals/runtimePolicyWarmer.js";
10
- /** Internal adapter shared by explicit discovery and background reconciliation. */
11
- export async function discoverContainerDocumentsForRuntime({ containerId, onFullListing, onPendingDiscovery, runtimeService, }) {
12
- const input = runtimeService.workflowInput();
13
- if (input.infra.dbStatus !== "ready") {
14
- return null;
15
- }
16
- const runtime = createContainerContentsWorkflowRuntime(input);
17
- const evidenceStore = createDocumentDiscoveryEvidenceStore(input.infra.execSql);
18
- // Capture the reset fence before reading scope, so a reset between this read
19
- // and listing cannot publish old-organization evidence into its replacement.
20
- const generation = await evidenceStore.begin();
21
- const stored = await sqlContainerContentsPersistence.loadContainerMetadataState(input.infra.execSql, containerId);
22
- if (!stored)
23
- return null;
24
- const containerOrganizationId = stored.container.organizationId;
11
+ function createDiscoveryLane(input) {
12
+ const { containerOrganizationId, execSql, request, runtime } = input;
25
13
  const warmReferencedPrincipalPolicies = createRuntimePrincipalPolicyWarmer(runtime);
26
14
  const loadHead = createDocumentHeadLinkSetLoader(runtime);
27
- const loadEpoch = (documentId) => loadLocalDocumentAccessEpoch(input.infra.execSql, documentId);
28
- return discoverContainerDocumentsFromApi({
29
- ...createContainerDocumentQueriesFromRuntime(runtime),
30
- apiClient: runtime.apiClient,
31
- beginDocumentDiscovery: async () => generation,
15
+ const loadEpoch = (documentId) => loadLocalDocumentAccessEpoch(execSql, documentId);
16
+ return {
17
+ ...input.queries,
32
18
  cacheReferencedPrincipalPolicies: (references) => containerOrganizationId
33
19
  ? warmReferencedPrincipalPolicies({
34
20
  organizationId: containerOrganizationId,
35
21
  references,
36
22
  })
37
23
  : Promise.resolve(),
38
- containerId,
39
- holdContainerDocumentTombstones: (tombstones) => holdContainerDocumentTombstones(input.infra.execSql, tombstones),
40
- listHeldContainerDocumentTombstones: (containerIds) => listRetryableHeldContainerDocumentTombstones(input.infra.execSql, containerIds),
41
- listKnownContainerDocumentPlacements: (placements) => listKnownContainerDocumentPlacements(input.infra.execSql, placements),
42
- onFullListing,
43
- refuteContainerDocumentTombstoneHolds: (placements) => refuteContainerDocumentTombstoneHolds(input.infra.execSql, placements),
24
+ containerId: request.containerId,
25
+ holdContainerDocumentTombstones: (tombstones) => holdContainerDocumentTombstones(execSql, tombstones),
26
+ listHeldContainerDocumentTombstones: (containerIds) => listRetryableHeldContainerDocumentTombstones(execSql, containerIds),
27
+ listKnownContainerDocumentPlacements: (placements) => listKnownContainerDocumentPlacements(execSql, placements),
28
+ onFullListing: request.onFullListing,
29
+ refuteContainerDocumentTombstoneHolds: (placements) => refuteContainerDocumentTombstoneHolds(execSql, placements),
44
30
  verifyContainerDocumentTombstones: createContainerDocumentTombstoneVerifier(containerOrganizationId, loadHead, loadEpoch),
45
- verifyDiscoveredDocuments: createDiscoveredDocumentVerifier(containerOrganizationId, loadHead, loadEpoch, evidenceStore, onPendingDiscovery),
31
+ verifyDiscoveredDocuments: createDiscoveredDocumentVerifier(containerOrganizationId, loadHead, loadEpoch, input.evidenceStore, request.onPendingDiscovery),
32
+ };
33
+ }
34
+ /**
35
+ * Internal adapter shared by explicit discovery and background reconciliation:
36
+ * one batched listing for every requested container, then per-container
37
+ * verification scoped to each container's durable organization.
38
+ */
39
+ export async function discoverContainerDocumentLanesForRuntime({ onOutcome, requests, runtimeService, }) {
40
+ const unlisted = (containerId) => ({
41
+ containerId,
42
+ status: "fulfilled",
43
+ value: null,
44
+ });
45
+ const input = runtimeService.workflowInput();
46
+ if (input.infra.dbStatus !== "ready") {
47
+ return requests.map(({ containerId }) => unlisted(containerId));
48
+ }
49
+ const runtime = createContainerContentsWorkflowRuntime(input);
50
+ const execSql = input.infra.execSql;
51
+ const evidenceStore = createDocumentDiscoveryEvidenceStore(execSql);
52
+ const queries = createContainerDocumentQueriesFromRuntime(runtime);
53
+ // Capture the reset fence before reading any lane's scope, so a reset
54
+ // between these reads and the listing cannot publish old-organization
55
+ // evidence into its replacement.
56
+ const generation = await evidenceStore.begin();
57
+ const lanes = [];
58
+ for (const request of requests) {
59
+ const stored = await sqlContainerContentsPersistence.loadContainerMetadataState(execSql, request.containerId);
60
+ if (stored)
61
+ lanes.push(createDiscoveryLane({
62
+ containerOrganizationId: stored.container.organizationId,
63
+ evidenceStore,
64
+ execSql,
65
+ queries,
66
+ request,
67
+ runtime,
68
+ }));
69
+ }
70
+ const outcomes = await discoverContainerDocumentLanes({
71
+ generation,
72
+ lanes,
73
+ listContainerDocumentLanes: (listed) => listContainerDocumentLanesFromApi(runtime.apiClient, listed),
74
+ loadContainerDocumentWatermark: queries.loadContainerDocumentWatermark,
75
+ onOutcome,
76
+ });
77
+ const outcomeByContainerId = new Map(outcomes.map((outcome) => [outcome.containerId, outcome]));
78
+ return requests.map(({ containerId }) => outcomeByContainerId.get(containerId) ?? unlisted(containerId));
79
+ }
80
+ /** Discovers one container; a rejected lane rethrows its failure. */
81
+ export async function discoverContainerDocumentsForRuntime({ runtimeService, ...request }) {
82
+ const [outcome] = await discoverContainerDocumentLanesForRuntime({
83
+ requests: [request],
84
+ runtimeService,
46
85
  });
86
+ if (outcome?.status === "rejected")
87
+ throw outcome.reason;
88
+ return outcome?.value ?? null;
47
89
  }
48
90
  //# sourceMappingURL=containerDocumentDiscovery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"containerDocumentDiscovery.js","sourceRoot":"","sources":["containerDocumentDiscovery.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DocumentSummary } from \"../data/documents/documentSummary\";\nimport { sqlContainerContentsPersistence } from \"../data/persistence/container-contents/containerContentsPersistence\";\nimport {\n holdContainerDocumentTombstones,\n listKnownContainerDocumentPlacements,\n listRetryableHeldContainerDocumentTombstones,\n loadLocalDocumentAccessEpoch,\n refuteContainerDocumentTombstoneHolds,\n} from \"../data/persistence/documents/containerDocumentTombstoneHoldsPersistence\";\nimport { createDocumentDiscoveryEvidenceStore } from \"../data/persistence/documents/documentDiscoveryEvidencePersistence\";\nimport { discoverContainerDocumentsFromApi } from \"../workflows/container-contents/documentDiscovery\";\nimport { createDiscoveredDocumentVerifier } from \"../workflows/container-contents/documentDiscoveryEvidence\";\nimport { createContainerDocumentQueriesFromRuntime } from \"../workflows/container-contents/documentQueries\";\nimport {\n createContainerDocumentTombstoneVerifier,\n createDocumentHeadLinkSetLoader,\n} from \"../workflows/container-contents/documentTombstoneEvidence\";\nimport { createContainerContentsWorkflowRuntime } from \"../workflows/container-contents/runtime\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../workflows/principals/runtimePolicyWarmer\";\nimport type { InternalRuntime } from \"./workflowRuntime\";\n\n/** Internal adapter shared by explicit discovery and background reconciliation. */\nexport async function discoverContainerDocumentsForRuntime({\n containerId,\n onFullListing,\n onPendingDiscovery,\n runtimeService,\n}: {\n containerId: string;\n onFullListing?: ((documentIds: ReadonlyArray<string>) => void) | undefined;\n onPendingDiscovery?: ((delayMs: number) => void) | undefined;\n runtimeService: InternalRuntime;\n}): Promise<ReadonlyArray<DocumentSummary> | null> {\n const input = runtimeService.workflowInput();\n if (input.infra.dbStatus !== \"ready\") {\n return null;\n }\n const runtime = createContainerContentsWorkflowRuntime(input);\n const evidenceStore = createDocumentDiscoveryEvidenceStore(\n input.infra.execSql,\n );\n // Capture the reset fence before reading scope, so a reset between this read\n // and listing cannot publish old-organization evidence into its replacement.\n const generation = await evidenceStore.begin();\n const stored =\n await sqlContainerContentsPersistence.loadContainerMetadataState(\n input.infra.execSql,\n containerId,\n );\n if (!stored) return null;\n const containerOrganizationId = stored.container.organizationId;\n const warmReferencedPrincipalPolicies =\n createRuntimePrincipalPolicyWarmer(runtime);\n const loadHead = createDocumentHeadLinkSetLoader(runtime);\n const loadEpoch = (documentId: string) =>\n loadLocalDocumentAccessEpoch(input.infra.execSql, documentId);\n return discoverContainerDocumentsFromApi({\n ...createContainerDocumentQueriesFromRuntime(runtime),\n apiClient: runtime.apiClient,\n beginDocumentDiscovery: async () => generation,\n cacheReferencedPrincipalPolicies: (references) =>\n containerOrganizationId\n ? warmReferencedPrincipalPolicies({\n organizationId: containerOrganizationId,\n references,\n })\n : Promise.resolve(),\n containerId,\n holdContainerDocumentTombstones: (tombstones) =>\n holdContainerDocumentTombstones(input.infra.execSql, tombstones),\n listHeldContainerDocumentTombstones: (containerIds) =>\n listRetryableHeldContainerDocumentTombstones(\n input.infra.execSql,\n containerIds,\n ),\n listKnownContainerDocumentPlacements: (placements) =>\n listKnownContainerDocumentPlacements(input.infra.execSql, placements),\n onFullListing,\n refuteContainerDocumentTombstoneHolds: (placements) =>\n refuteContainerDocumentTombstoneHolds(input.infra.execSql, placements),\n verifyContainerDocumentTombstones: createContainerDocumentTombstoneVerifier(\n containerOrganizationId,\n loadHead,\n loadEpoch,\n ),\n verifyDiscoveredDocuments: createDiscoveredDocumentVerifier(\n containerOrganizationId,\n loadHead,\n loadEpoch,\n evidenceStore,\n onPendingDiscovery,\n ),\n });\n}\n"]}
1
+ {"version":3,"file":"containerDocumentDiscovery.js","sourceRoot":"","sources":["containerDocumentDiscovery.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DocumentSummary } from \"../data/documents/documentSummary\";\nimport { sqlContainerContentsPersistence } from \"../data/persistence/container-contents/containerContentsPersistence\";\nimport {\n holdContainerDocumentTombstones,\n listKnownContainerDocumentPlacements,\n listRetryableHeldContainerDocumentTombstones,\n loadLocalDocumentAccessEpoch,\n refuteContainerDocumentTombstoneHolds,\n} from \"../data/persistence/documents/containerDocumentTombstoneHoldsPersistence\";\nimport { createDocumentDiscoveryEvidenceStore } from \"../data/persistence/documents/documentDiscoveryEvidencePersistence\";\nimport { listContainerDocumentLanesFromApi } from \"../workflows/container-contents/containerDocumentListing\";\nimport { createDiscoveredDocumentVerifier } from \"../workflows/container-contents/documentDiscoveryEvidence\";\nimport {\n type ContainerDocumentDiscoveryLane,\n type ContainerDocumentDiscoveryOutcome,\n type ContainerDocumentDiscoveryOutcomeHandler,\n discoverContainerDocumentLanes,\n} from \"../workflows/container-contents/documentDiscoveryLanes\";\nimport { createContainerDocumentQueriesFromRuntime } from \"../workflows/container-contents/documentQueries\";\nimport {\n createContainerDocumentTombstoneVerifier,\n createDocumentHeadLinkSetLoader,\n} from \"../workflows/container-contents/documentTombstoneEvidence\";\nimport {\n type ContainerContentsWorkflowRuntime,\n createContainerContentsWorkflowRuntime,\n} from \"../workflows/container-contents/runtime\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../workflows/principals/runtimePolicyWarmer\";\nimport type { InternalRuntime } from \"./workflowRuntime\";\n\nexport type { ContainerDocumentDiscoveryOutcome } from \"../workflows/container-contents/documentDiscoveryLanes\";\n\n/** One container to discover, with the reconciler's per-lane callbacks. */\nexport interface ContainerDocumentDiscoveryRequest {\n readonly containerId: string;\n readonly onFullListing?:\n | ((documentIds: ReadonlyArray<string>) => void)\n | undefined;\n readonly onPendingDiscovery?: ((delayMs: number) => void) | undefined;\n}\n\ntype ExecSql = ReturnType<InternalRuntime[\"workflowInput\"]>[\"infra\"][\"execSql\"];\n\nfunction createDiscoveryLane(input: {\n readonly containerOrganizationId: string | undefined;\n readonly evidenceStore: ReturnType<\n typeof createDocumentDiscoveryEvidenceStore\n >;\n readonly execSql: ExecSql;\n readonly queries: ReturnType<\n typeof createContainerDocumentQueriesFromRuntime\n >;\n readonly request: ContainerDocumentDiscoveryRequest;\n readonly runtime: ContainerContentsWorkflowRuntime;\n}): ContainerDocumentDiscoveryLane {\n const { containerOrganizationId, execSql, request, runtime } = input;\n const warmReferencedPrincipalPolicies =\n createRuntimePrincipalPolicyWarmer(runtime);\n const loadHead = createDocumentHeadLinkSetLoader(runtime);\n const loadEpoch = (documentId: string) =>\n loadLocalDocumentAccessEpoch(execSql, documentId);\n return {\n ...input.queries,\n cacheReferencedPrincipalPolicies: (references) =>\n containerOrganizationId\n ? warmReferencedPrincipalPolicies({\n organizationId: containerOrganizationId,\n references,\n })\n : Promise.resolve(),\n containerId: request.containerId,\n holdContainerDocumentTombstones: (tombstones) =>\n holdContainerDocumentTombstones(execSql, tombstones),\n listHeldContainerDocumentTombstones: (containerIds) =>\n listRetryableHeldContainerDocumentTombstones(execSql, containerIds),\n listKnownContainerDocumentPlacements: (placements) =>\n listKnownContainerDocumentPlacements(execSql, placements),\n onFullListing: request.onFullListing,\n refuteContainerDocumentTombstoneHolds: (placements) =>\n refuteContainerDocumentTombstoneHolds(execSql, placements),\n verifyContainerDocumentTombstones: createContainerDocumentTombstoneVerifier(\n containerOrganizationId,\n loadHead,\n loadEpoch,\n ),\n verifyDiscoveredDocuments: createDiscoveredDocumentVerifier(\n containerOrganizationId,\n loadHead,\n loadEpoch,\n input.evidenceStore,\n request.onPendingDiscovery,\n ),\n };\n}\n\n/**\n * Internal adapter shared by explicit discovery and background reconciliation:\n * one batched listing for every requested container, then per-container\n * verification scoped to each container's durable organization.\n */\nexport async function discoverContainerDocumentLanesForRuntime({\n onOutcome,\n requests,\n runtimeService,\n}: {\n readonly onOutcome?: ContainerDocumentDiscoveryOutcomeHandler | undefined;\n readonly requests: ReadonlyArray<ContainerDocumentDiscoveryRequest>;\n readonly runtimeService: InternalRuntime;\n}): Promise<ReadonlyArray<ContainerDocumentDiscoveryOutcome>> {\n const unlisted = (\n containerId: string,\n ): ContainerDocumentDiscoveryOutcome => ({\n containerId,\n status: \"fulfilled\",\n value: null,\n });\n const input = runtimeService.workflowInput();\n if (input.infra.dbStatus !== \"ready\") {\n return requests.map(({ containerId }) => unlisted(containerId));\n }\n const runtime = createContainerContentsWorkflowRuntime(input);\n const execSql = input.infra.execSql;\n const evidenceStore = createDocumentDiscoveryEvidenceStore(execSql);\n const queries = createContainerDocumentQueriesFromRuntime(runtime);\n // Capture the reset fence before reading any lane's scope, so a reset\n // between these reads and the listing cannot publish old-organization\n // evidence into its replacement.\n const generation = await evidenceStore.begin();\n const lanes: ContainerDocumentDiscoveryLane[] = [];\n for (const request of requests) {\n const stored =\n await sqlContainerContentsPersistence.loadContainerMetadataState(\n execSql,\n request.containerId,\n );\n if (stored)\n lanes.push(\n createDiscoveryLane({\n containerOrganizationId: stored.container.organizationId,\n evidenceStore,\n execSql,\n queries,\n request,\n runtime,\n }),\n );\n }\n const outcomes = await discoverContainerDocumentLanes({\n generation,\n lanes,\n listContainerDocumentLanes: (listed) =>\n listContainerDocumentLanesFromApi(runtime.apiClient, listed),\n loadContainerDocumentWatermark: queries.loadContainerDocumentWatermark,\n onOutcome,\n });\n const outcomeByContainerId = new Map(\n outcomes.map((outcome) => [outcome.containerId, outcome]),\n );\n return requests.map(\n ({ containerId }) =>\n outcomeByContainerId.get(containerId) ?? unlisted(containerId),\n );\n}\n\n/** Discovers one container; a rejected lane rethrows its failure. */\nexport async function discoverContainerDocumentsForRuntime({\n runtimeService,\n ...request\n}: ContainerDocumentDiscoveryRequest & {\n readonly runtimeService: InternalRuntime;\n}): Promise<ReadonlyArray<DocumentSummary> | null> {\n const [outcome] = await discoverContainerDocumentLanesForRuntime({\n requests: [request],\n runtimeService,\n });\n if (outcome?.status === \"rejected\") throw outcome.reason;\n return outcome?.value ?? null;\n}\n"]}
@@ -8,7 +8,7 @@ import { createReconciledDocumentContentPuller } from "../sync/reconciliation/do
8
8
  import { listAllContainerDocumentIdsFromApi } from "../workflows/container-contents/containerDocumentListing.js";
9
9
  import { probeUndiscoveredRemoteDocumentBatch } from "../workflows/container-contents/documentHydrationProbe.js";
10
10
  import { createContainerContentsDocumentsRuntime, createContainerContentsStoreWorkflowRuntime, } from "../workflows/container-contents/runtime.js";
11
- import { discoverContainerDocumentsForRuntime } from "./containerDocumentDiscovery.js";
11
+ import { discoverContainerDocumentLanesForRuntime } from "./containerDocumentDiscovery.js";
12
12
  export function createDeviceFirst(runtimeService, containerContents) {
13
13
  return new DeviceFirstService(runtimeService, containerContents);
14
14
  }
@@ -169,10 +169,9 @@ class DeviceFirstService {
169
169
  // retain every regular container. Exclude only this identity's own system
170
170
  // children; cold/auth backfill and explicit full Refresh cover those.
171
171
  listAutomaticRootCatchupContainerIds,
172
- discoverContainerDocuments: (containerId, onFullListing, onPendingDiscovery) => discoverContainerDocumentsForRuntime({
173
- containerId,
174
- onFullListing,
175
- onPendingDiscovery,
172
+ discoverContainerDocuments: (requests, onOutcome) => discoverContainerDocumentLanesForRuntime({
173
+ onOutcome,
174
+ requests,
176
175
  runtimeService,
177
176
  }),
178
177
  loadContainerDelta: (containerId) => store.loadContainerDelta(containerId),
@@ -1 +1 @@
1
- {"version":3,"file":"deviceFirst.js","sourceRoot":"","sources":["deviceFirst.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DomainScope } from \"../data/domainScope\";\nimport {\n disposeDomainSyncCoordinator,\n isDatabaseUnavailableError,\n} from \"../data/sync/syncCoordinator\";\nimport {\n type ContainerContentsStore,\n type ContainerContentsStoreOptions,\n isAutomaticRootCatchupContainerNode,\n isReconcilableContainerNode,\n isRemoteBackedContainerNode,\n} from \"../stores/container-contents\";\nimport { openDocumentStore } from \"../stores/documents\";\nimport {\n requestRegisteredDocumentRemoteSync,\n subscribeToPersistedDocumentDeletions,\n subscribeToPersistedDocuments,\n} from \"../stores/documents/registry\";\nimport {\n getOrCreateLocalProjectionStore,\n type LocalProjectionStore,\n type LocalProjectionView,\n} from \"../stores/local-projection\";\nimport {\n connectReconciliationTriggers,\n createReconciliationService,\n type ReconciliationHost,\n type ReconciliationService,\n} from \"../sync/reconciliation\";\nimport { createReconciledDocumentContentPuller } from \"../sync/reconciliation/documentContentPull\";\nimport { listAllContainerDocumentIdsFromApi } from \"../workflows/container-contents/containerDocumentListing\";\nimport { probeUndiscoveredRemoteDocumentBatch } from \"../workflows/container-contents/documentHydrationProbe\";\nimport {\n type ContainerContentsStoreWorkflowRuntime,\n createContainerContentsDocumentsRuntime,\n createContainerContentsStoreWorkflowRuntime,\n} from \"../workflows/container-contents/runtime\";\nimport type { ContainerContents } from \"./containerContents\";\nimport { discoverContainerDocumentsForRuntime } from \"./containerDocumentDiscovery\";\nimport type { InternalRuntime } from \"./workflowRuntime\";\n\nexport type { LocalProjectionView } from \"../stores/local-projection\";\n\n/** One domain scope's local read, local-write, and reconciliation handles. */\nexport interface DeviceFirstContainerContents {\n /**\n * Locally durable container tree and mutation store. Ordinary tree writes\n * update this store after local persistence and queue remote convergence in\n * the background; security-sensitive remote operations keep their explicit\n * online requirements.\n */\n readonly containerStore: ContainerContentsStore;\n /** Background remote reconciliation; never awaited for first paint. */\n readonly reconciler: ReconciliationService;\n /** Synchronously readable container/document projection. */\n readonly view: LocalProjectionView;\n}\n\n/**\n * Device-first facade. Opens a shared scope that pairs immediate local reads\n * and locally durable writes with background remote convergence.\n */\nexport interface DeviceFirst {\n /** Open all device-first handles for the current domain scope. */\n open(\n options?: ContainerContentsStoreOptions | undefined,\n ): DeviceFirstContainerContents;\n /** Stop the current scope's reconciler (if one was created) on teardown. */\n dispose(): void;\n}\n\nexport function createDeviceFirst(\n runtimeService: InternalRuntime,\n containerContents: ContainerContents,\n): DeviceFirst {\n return new DeviceFirstService(runtimeService, containerContents);\n}\n\ninterface DeviceFirstScopeEntry {\n contents: DeviceFirstContainerContents;\n disconnectReconciliationTriggers: () => void;\n unsubscribePersistedDocumentDeletions: () => void;\n unsubscribePersistedDocuments: () => void;\n}\n\nfunction createInitialDocumentProbeHost(\n runtimeService: InternalRuntime,\n domainScope: ReconciliationHost[\"domainScope\"],\n): Pick<\n ReconciliationHost,\n | \"listContainerDocumentIds\"\n | \"probeUndiscoveredDocumentsBatch\"\n | \"reportInitialDocumentProbeComplete\"\n> {\n return {\n listContainerDocumentIds: (containerId) => {\n const runtime = createDeviceFirstWorkflowRuntime(runtimeService);\n return listAllContainerDocumentIdsFromApi({\n apiClient: runtime.apiClient,\n containerId,\n reportErrors: false,\n });\n },\n probeUndiscoveredDocumentsBatch: async (input) => {\n const runtime = createDeviceFirstWorkflowRuntime(runtimeService);\n return probeUndiscoveredRemoteDocumentBatch({\n ...input,\n host: {\n documentWorkflowRuntime: (containerId) =>\n createContainerContentsDocumentsRuntime(runtime, containerId),\n openDocumentStore: (target) =>\n openDocumentStore(\n domainScope,\n target.localId,\n target.runtime,\n target.documentId,\n ),\n },\n runtime,\n });\n },\n reportInitialDocumentProbeComplete: (requestedCount) => {\n runtimeService\n .workflowInput()\n .util.log(\n `Initial document hydration probe requested ${requestedCount} syncs`,\n );\n },\n };\n}\n\nclass DeviceFirstService implements DeviceFirst {\n // A real Map (not WeakMap) so dispose() can iterate every scope seen this\n // session. A running reconciler holds event subscriptions that keep an idle,\n // stale-scope service (and its scope) reachable anyway, so WeakMap bought no\n // GC here; the entries are released explicitly on dispose().\n private readonly entriesByScope = new Map<\n DomainScope,\n DeviceFirstScopeEntry\n >();\n\n constructor(\n private readonly runtimeService: InternalRuntime,\n private readonly containerContents: ContainerContents,\n ) {}\n\n open(\n options?: ContainerContentsStoreOptions | undefined,\n ): DeviceFirstContainerContents {\n return this.getOrCreateEntry(options).contents;\n }\n\n dispose(): void {\n // Stop every reconciler created this session — not just the current\n // scope's. A scope change (e.g. anonymous -> authenticated) leaves the old\n // scope's reconciler subscribed and its coordinator pump live; tear them\n // all down. Force-stop each scope's coordinator (which runs the reconciler\n // lane) alongside stopping the service.\n for (const [domainScope, entry] of this.entriesByScope) {\n entry.contents.reconciler.stop();\n entry.disconnectReconciliationTriggers();\n entry.unsubscribePersistedDocumentDeletions();\n entry.unsubscribePersistedDocuments();\n disposeDomainSyncCoordinator(domainScope);\n }\n this.entriesByScope.clear();\n }\n\n private workflowRuntime(): ContainerContentsStoreWorkflowRuntime {\n return createDeviceFirstWorkflowRuntime(this.runtimeService);\n }\n\n private getOrCreateEntry(\n options?: ContainerContentsStoreOptions | undefined,\n ): DeviceFirstScopeEntry {\n const runtime = this.workflowRuntime();\n const domainScope = runtime.state.domainScope;\n const existing = this.entriesByScope.get(domainScope);\n if (existing) {\n // Do not call updateRuntime here: facade selectors run during React\n // render, and updateRuntime emits synchronously. Hosts drive runtime\n // updates through view.updateRuntime() from an effect instead.\n return existing;\n }\n\n const store = getOrCreateLocalProjectionStore({\n domainScope,\n runtime,\n options,\n });\n const service = createReconciliationService(\n this.createReconciliationHost(store, domainScope),\n );\n const disconnectReconciliationTriggers = connectReconciliationTriggers({\n service,\n store,\n });\n const unsubscribePersistedDocumentDeletions =\n subscribeToPersistedDocumentDeletions(domainScope, (localId) => {\n store.removePersistedDocument(localId);\n });\n const unsubscribePersistedDocuments = subscribeToPersistedDocuments(\n domainScope,\n (document, change) =>\n store.refreshPersistedDocument(document, change.placementChanged),\n );\n service.start();\n\n const view: LocalProjectionView = {\n getSnapshot: () => store.getSnapshot(),\n subscribe: (listener) => store.subscribe(listener),\n setActiveContainer: (containerId) =>\n store.setActiveContainer(containerId),\n updateRuntime: (nextRuntime) => store.updateRuntime(nextRuntime),\n };\n\n const entry: DeviceFirstScopeEntry = {\n contents: {\n containerStore: store.getContainerStore(),\n reconciler: service,\n view,\n },\n disconnectReconciliationTriggers,\n unsubscribePersistedDocumentDeletions,\n unsubscribePersistedDocuments,\n };\n this.entriesByScope.set(domainScope, entry);\n return entry;\n }\n\n private createReconciliationHost(\n store: LocalProjectionStore,\n domainScope: ReconciliationHost[\"domainScope\"],\n ): ReconciliationHost {\n const runtimeService = this.runtimeService;\n const containerContents = this.containerContents;\n const requestDocumentContentPull = createReconciledDocumentContentPuller({\n getContainer: (containerId) =>\n store\n .getSnapshot()\n .containers.find((container) => container.id === containerId) ?? null,\n pullDocumentContent: (input) =>\n containerContents.pullDocumentContent(input),\n requestRegisteredDocumentRemoteSync: (localId, documentId) =>\n requestRegisteredDocumentRemoteSync(domainScope, localId, documentId),\n });\n const canDiscoverContainerDocuments = (containerId: string) => {\n const container = store\n .getSnapshot()\n .containers.find((node) => node.id === containerId);\n return container ? isRemoteBackedContainerNode(container) : false;\n };\n const listKnownContainerIds = () => {\n // A local-first id is not listable yet and may still be racing its create\n // request. Once remote-backed, it participates normally; system contents\n // are especially important during rematerialization because this device\n // may not have authored them.\n const homeOrganizationId =\n runtimeService.workflowInput().auth.organizationId;\n return store\n .getSnapshot()\n .containers.flatMap((node) =>\n isReconcilableContainerNode(node, homeOrganizationId)\n ? [node.id]\n : [],\n );\n };\n const listAutomaticRootCatchupContainerIds = () => {\n const homeOrganizationId =\n runtimeService.workflowInput().auth.organizationId;\n return store\n .getSnapshot()\n .containers.flatMap((node) =>\n isAutomaticRootCatchupContainerNode(node, homeOrganizationId)\n ? [node.id]\n : [],\n );\n };\n return {\n domainScope,\n canDiscoverContainerDocuments,\n getRuntimeStatus: () => {\n const input = runtimeService.workflowInput();\n return {\n dbStatus: input.infra.dbStatus,\n isAuthenticated: input.auth.isAuthenticated,\n online: input.state.online,\n };\n },\n listKnownContainerIds,\n // Root discovery can surface directly granted non-root containers, so\n // retain every regular container. Exclude only this identity's own system\n // children; cold/auth backfill and explicit full Refresh cover those.\n listAutomaticRootCatchupContainerIds,\n discoverContainerDocuments: (\n containerId,\n onFullListing,\n onPendingDiscovery,\n ) =>\n discoverContainerDocumentsForRuntime({\n containerId,\n onFullListing,\n onPendingDiscovery,\n runtimeService,\n }),\n loadContainerDelta: (containerId) =>\n store.loadContainerDelta(containerId),\n applyReconciled: (delta) => store.applyReconciled(delta),\n requestDocumentContentPull,\n ...createInitialDocumentProbeHost(runtimeService, domainScope),\n refreshTree: async () => {\n await store.getContainerStore().refresh();\n },\n refreshRootTree: async () => {\n await store.getContainerStore().refreshRootLane();\n },\n isIgnorableError: isDatabaseUnavailableError,\n // Resolve the runtime per call: it is rebuilt as the active identity\n // changes, and a reference captured here would report into a dead host.\n logError: (message, error) =>\n logReconciliationFailure(this.workflowRuntime(), message, error),\n };\n }\n}\n\n// Returns the host's result: both callbacks are declared `=> void` but may be\n// async, and the lane reporter only catches a rejection it is handed.\nfunction logReconciliationFailure(\n runtime: ContainerContentsStoreWorkflowRuntime,\n message: string,\n error: unknown,\n): unknown {\n const { log, logError } = runtime.util;\n return logError ? logError(message, error) : log(message);\n}\n\nexport function createDeviceFirstWorkflowRuntime(\n runtimeService: InternalRuntime,\n): ContainerContentsStoreWorkflowRuntime {\n return createContainerContentsStoreWorkflowRuntime(\n runtimeService.workflowInput(),\n runtimeService.adoptRootContainer,\n );\n}\n"]}
1
+ {"version":3,"file":"deviceFirst.js","sourceRoot":"","sources":["deviceFirst.ts"],"names":[],"mappings":"AACA,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAGL,mCAAmC,EACnC,2BAA2B,EAC3B,2BAA2B,GAC5B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,mCAAmC,EACnC,qCAAqC,EACrC,6BAA6B,GAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,+BAA+B,GAGhC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,6BAA6B,EAC7B,2BAA2B,GAG5B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,qCAAqC,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,kCAAkC,EAAE,MAAM,0DAA0D,CAAC;AAC9G,OAAO,EAAE,oCAAoC,EAAE,MAAM,wDAAwD,CAAC;AAC9G,OAAO,EAEL,uCAAuC,EACvC,2CAA2C,GAC5C,MAAM,yCAAyC,CAAC;AAEjD,OAAO,EAAE,wCAAwC,EAAE,MAAM,8BAA8B,CAAC;AAiCxF,MAAM,UAAU,iBAAiB,CAC/B,cAA+B,EAC/B,iBAAoC;IAEpC,OAAO,IAAI,kBAAkB,CAAC,cAAc,EAAE,iBAAiB,CAAC,CAAC;AACnE,CAAC;AASD,SAAS,8BAA8B,CACrC,cAA+B,EAC/B,WAA8C;IAO9C,OAAO;QACL,wBAAwB,EAAE,CAAC,WAAW,EAAE,EAAE;YACxC,MAAM,OAAO,GAAG,gCAAgC,CAAC,cAAc,CAAC,CAAC;YACjE,OAAO,kCAAkC,CAAC;gBACxC,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,WAAW;gBACX,YAAY,EAAE,KAAK;aACpB,CAAC,CAAC;QACL,CAAC;QACD,+BAA+B,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YAC/C,MAAM,OAAO,GAAG,gCAAgC,CAAC,cAAc,CAAC,CAAC;YACjE,OAAO,oCAAoC,CAAC;gBAC1C,GAAG,KAAK;gBACR,IAAI,EAAE;oBACJ,uBAAuB,EAAE,CAAC,WAAW,EAAE,EAAE,CACvC,uCAAuC,CAAC,OAAO,EAAE,WAAW,CAAC;oBAC/D,iBAAiB,EAAE,CAAC,MAAM,EAAE,EAAE,CAC5B,iBAAiB,CACf,WAAW,EACX,MAAM,CAAC,OAAO,EACd,MAAM,CAAC,OAAO,EACd,MAAM,CAAC,UAAU,CAClB;iBACJ;gBACD,OAAO;aACR,CAAC,CAAC;QACL,CAAC;QACD,kCAAkC,EAAE,CAAC,cAAc,EAAE,EAAE;YACrD,cAAc;iBACX,aAAa,EAAE;iBACf,IAAI,CAAC,GAAG,CACP,8CAA8C,cAAc,QAAQ,CACrE,CAAC;QACN,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,kBAAkB;IAWH;IACA;IAXnB,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,6DAA6D;IAC5C,cAAc,GAAG,IAAI,GAAG,EAGtC,CAAC;IAEJ,YACmB,cAA+B,EAC/B,iBAAoC;QADpC,mBAAc,GAAd,cAAc,CAAiB;QAC/B,sBAAiB,GAAjB,iBAAiB,CAAmB;IACpD,CAAC;IAEJ,IAAI,CACF,OAAmD;QAEnD,OAAO,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;IACjD,CAAC;IAED,OAAO;QACL,oEAAoE;QACpE,2EAA2E;QAC3E,yEAAyE;QACzE,2EAA2E;QAC3E,wCAAwC;QACxC,KAAK,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACvD,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;YACjC,KAAK,CAAC,gCAAgC,EAAE,CAAC;YACzC,KAAK,CAAC,qCAAqC,EAAE,CAAC;YAC9C,KAAK,CAAC,6BAA6B,EAAE,CAAC;YACtC,4BAA4B,CAAC,WAAW,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;IAEO,eAAe;QACrB,OAAO,gCAAgC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC/D,CAAC;IAEO,gBAAgB,CACtB,OAAmD;QAEnD,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QACvC,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;QAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACtD,IAAI,QAAQ,EAAE,CAAC;YACb,oEAAoE;YACpE,qEAAqE;YACrE,+DAA+D;YAC/D,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,MAAM,KAAK,GAAG,+BAA+B,CAAC;YAC5C,WAAW;YACX,OAAO;YACP,OAAO;SACR,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,2BAA2B,CACzC,IAAI,CAAC,wBAAwB,CAAC,KAAK,EAAE,WAAW,CAAC,CAClD,CAAC;QACF,MAAM,gCAAgC,GAAG,6BAA6B,CAAC;YACrE,OAAO;YACP,KAAK;SACN,CAAC,CAAC;QACH,MAAM,qCAAqC,GACzC,qCAAqC,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,EAAE;YAC7D,KAAK,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QACL,MAAM,6BAA6B,GAAG,6BAA6B,CACjE,WAAW,EACX,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,CACnB,KAAK,CAAC,wBAAwB,CAAC,QAAQ,EAAE,MAAM,CAAC,gBAAgB,CAAC,CACpE,CAAC;QACF,OAAO,CAAC,KAAK,EAAE,CAAC;QAEhB,MAAM,IAAI,GAAwB;YAChC,WAAW,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE;YACtC,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC;YAClD,kBAAkB,EAAE,CAAC,WAAW,EAAE,EAAE,CAClC,KAAK,CAAC,kBAAkB,CAAC,WAAW,CAAC;YACvC,aAAa,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC;SACjE,CAAC;QAEF,MAAM,KAAK,GAA0B;YACnC,QAAQ,EAAE;gBACR,cAAc,EAAE,KAAK,CAAC,iBAAiB,EAAE;gBACzC,UAAU,EAAE,OAAO;gBACnB,IAAI;aACL;YACD,gCAAgC;YAChC,qCAAqC;YACrC,6BAA6B;SAC9B,CAAC;QACF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QAC5C,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,wBAAwB,CAC9B,KAA2B,EAC3B,WAA8C;QAE9C,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC;QAC3C,MAAM,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,CAAC;QACjD,MAAM,0BAA0B,GAAG,qCAAqC,CAAC;YACvE,YAAY,EAAE,CAAC,WAAW,EAAE,EAAE,CAC5B,KAAK;iBACF,WAAW,EAAE;iBACb,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,WAAW,CAAC,IAAI,IAAI;YACzE,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE,CAC7B,iBAAiB,CAAC,mBAAmB,CAAC,KAAK,CAAC;YAC9C,mCAAmC,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,CAC3D,mCAAmC,CAAC,WAAW,EAAE,OAAO,EAAE,UAAU,CAAC;SACxE,CAAC,CAAC;QACH,MAAM,6BAA6B,GAAG,CAAC,WAAmB,EAAE,EAAE;YAC5D,MAAM,SAAS,GAAG,KAAK;iBACpB,WAAW,EAAE;iBACb,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;YACtD,OAAO,SAAS,CAAC,CAAC,CAAC,2BAA2B,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACpE,CAAC,CAAC;QACF,MAAM,qBAAqB,GAAG,GAAG,EAAE;YACjC,0EAA0E;YAC1E,yEAAyE;YACzE,wEAAwE;YACxE,8BAA8B;YAC9B,MAAM,kBAAkB,GACtB,cAAc,CAAC,aAAa,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC;YACrD,OAAO,KAAK;iBACT,WAAW,EAAE;iBACb,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAC3B,2BAA2B,CAAC,IAAI,EAAE,kBAAkB,CAAC;gBACnD,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBACX,CAAC,CAAC,EAAE,CACP,CAAC;QACN,CAAC,CAAC;QACF,MAAM,oCAAoC,GAAG,GAAG,EAAE;YAChD,MAAM,kBAAkB,GACtB,cAAc,CAAC,aAAa,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC;YACrD,OAAO,KAAK;iBACT,WAAW,EAAE;iBACb,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAC3B,mCAAmC,CAAC,IAAI,EAAE,kBAAkB,CAAC;gBAC3D,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBACX,CAAC,CAAC,EAAE,CACP,CAAC;QACN,CAAC,CAAC;QACF,OAAO;YACL,WAAW;YACX,6BAA6B;YAC7B,gBAAgB,EAAE,GAAG,EAAE;gBACrB,MAAM,KAAK,GAAG,cAAc,CAAC,aAAa,EAAE,CAAC;gBAC7C,OAAO;oBACL,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ;oBAC9B,eAAe,EAAE,KAAK,CAAC,IAAI,CAAC,eAAe;oBAC3C,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM;iBAC3B,CAAC;YACJ,CAAC;YACD,qBAAqB;YACrB,sEAAsE;YACtE,0EAA0E;YAC1E,sEAAsE;YACtE,oCAAoC;YACpC,0BAA0B,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,EAAE,CAClD,wCAAwC,CAAC;gBACvC,SAAS;gBACT,QAAQ;gBACR,cAAc;aACf,CAAC;YACJ,kBAAkB,EAAE,CAAC,WAAW,EAAE,EAAE,CAClC,KAAK,CAAC,kBAAkB,CAAC,WAAW,CAAC;YACvC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,CAAC,KAAK,CAAC;YACxD,0BAA0B;YAC1B,GAAG,8BAA8B,CAAC,cAAc,EAAE,WAAW,CAAC;YAC9D,WAAW,EAAE,KAAK,IAAI,EAAE;gBACtB,MAAM,KAAK,CAAC,iBAAiB,EAAE,CAAC,OAAO,EAAE,CAAC;YAC5C,CAAC;YACD,eAAe,EAAE,KAAK,IAAI,EAAE;gBAC1B,MAAM,KAAK,CAAC,iBAAiB,EAAE,CAAC,eAAe,EAAE,CAAC;YACpD,CAAC;YACD,gBAAgB,EAAE,0BAA0B;YAC5C,qEAAqE;YACrE,wEAAwE;YACxE,QAAQ,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAC3B,wBAAwB,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC;SACnE,CAAC;IACJ,CAAC;CACF;AAED,8EAA8E;AAC9E,sEAAsE;AACtE,SAAS,wBAAwB,CAC/B,OAA8C,EAC9C,OAAe,EACf,KAAc;IAEd,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IACvC,OAAO,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC9C,cAA+B;IAE/B,OAAO,2CAA2C,CAChD,cAAc,CAAC,aAAa,EAAE,EAC9B,cAAc,CAAC,kBAAkB,CAClC,CAAC;AACJ,CAAC","sourcesContent":["import type { DomainScope } from \"../data/domainScope\";\nimport {\n disposeDomainSyncCoordinator,\n isDatabaseUnavailableError,\n} from \"../data/sync/syncCoordinator\";\nimport {\n type ContainerContentsStore,\n type ContainerContentsStoreOptions,\n isAutomaticRootCatchupContainerNode,\n isReconcilableContainerNode,\n isRemoteBackedContainerNode,\n} from \"../stores/container-contents\";\nimport { openDocumentStore } from \"../stores/documents\";\nimport {\n requestRegisteredDocumentRemoteSync,\n subscribeToPersistedDocumentDeletions,\n subscribeToPersistedDocuments,\n} from \"../stores/documents/registry\";\nimport {\n getOrCreateLocalProjectionStore,\n type LocalProjectionStore,\n type LocalProjectionView,\n} from \"../stores/local-projection\";\nimport {\n connectReconciliationTriggers,\n createReconciliationService,\n type ReconciliationHost,\n type ReconciliationService,\n} from \"../sync/reconciliation\";\nimport { createReconciledDocumentContentPuller } from \"../sync/reconciliation/documentContentPull\";\nimport { listAllContainerDocumentIdsFromApi } from \"../workflows/container-contents/containerDocumentListing\";\nimport { probeUndiscoveredRemoteDocumentBatch } from \"../workflows/container-contents/documentHydrationProbe\";\nimport {\n type ContainerContentsStoreWorkflowRuntime,\n createContainerContentsDocumentsRuntime,\n createContainerContentsStoreWorkflowRuntime,\n} from \"../workflows/container-contents/runtime\";\nimport type { ContainerContents } from \"./containerContents\";\nimport { discoverContainerDocumentLanesForRuntime } from \"./containerDocumentDiscovery\";\nimport type { InternalRuntime } from \"./workflowRuntime\";\n\nexport type { LocalProjectionView } from \"../stores/local-projection\";\n\n/** One domain scope's local read, local-write, and reconciliation handles. */\nexport interface DeviceFirstContainerContents {\n /**\n * Locally durable container tree and mutation store. Ordinary tree writes\n * update this store after local persistence and queue remote convergence in\n * the background; security-sensitive remote operations keep their explicit\n * online requirements.\n */\n readonly containerStore: ContainerContentsStore;\n /** Background remote reconciliation; never awaited for first paint. */\n readonly reconciler: ReconciliationService;\n /** Synchronously readable container/document projection. */\n readonly view: LocalProjectionView;\n}\n\n/**\n * Device-first facade. Opens a shared scope that pairs immediate local reads\n * and locally durable writes with background remote convergence.\n */\nexport interface DeviceFirst {\n /** Open all device-first handles for the current domain scope. */\n open(\n options?: ContainerContentsStoreOptions | undefined,\n ): DeviceFirstContainerContents;\n /** Stop the current scope's reconciler (if one was created) on teardown. */\n dispose(): void;\n}\n\nexport function createDeviceFirst(\n runtimeService: InternalRuntime,\n containerContents: ContainerContents,\n): DeviceFirst {\n return new DeviceFirstService(runtimeService, containerContents);\n}\n\ninterface DeviceFirstScopeEntry {\n contents: DeviceFirstContainerContents;\n disconnectReconciliationTriggers: () => void;\n unsubscribePersistedDocumentDeletions: () => void;\n unsubscribePersistedDocuments: () => void;\n}\n\nfunction createInitialDocumentProbeHost(\n runtimeService: InternalRuntime,\n domainScope: ReconciliationHost[\"domainScope\"],\n): Pick<\n ReconciliationHost,\n | \"listContainerDocumentIds\"\n | \"probeUndiscoveredDocumentsBatch\"\n | \"reportInitialDocumentProbeComplete\"\n> {\n return {\n listContainerDocumentIds: (containerId) => {\n const runtime = createDeviceFirstWorkflowRuntime(runtimeService);\n return listAllContainerDocumentIdsFromApi({\n apiClient: runtime.apiClient,\n containerId,\n reportErrors: false,\n });\n },\n probeUndiscoveredDocumentsBatch: async (input) => {\n const runtime = createDeviceFirstWorkflowRuntime(runtimeService);\n return probeUndiscoveredRemoteDocumentBatch({\n ...input,\n host: {\n documentWorkflowRuntime: (containerId) =>\n createContainerContentsDocumentsRuntime(runtime, containerId),\n openDocumentStore: (target) =>\n openDocumentStore(\n domainScope,\n target.localId,\n target.runtime,\n target.documentId,\n ),\n },\n runtime,\n });\n },\n reportInitialDocumentProbeComplete: (requestedCount) => {\n runtimeService\n .workflowInput()\n .util.log(\n `Initial document hydration probe requested ${requestedCount} syncs`,\n );\n },\n };\n}\n\nclass DeviceFirstService implements DeviceFirst {\n // A real Map (not WeakMap) so dispose() can iterate every scope seen this\n // session. A running reconciler holds event subscriptions that keep an idle,\n // stale-scope service (and its scope) reachable anyway, so WeakMap bought no\n // GC here; the entries are released explicitly on dispose().\n private readonly entriesByScope = new Map<\n DomainScope,\n DeviceFirstScopeEntry\n >();\n\n constructor(\n private readonly runtimeService: InternalRuntime,\n private readonly containerContents: ContainerContents,\n ) {}\n\n open(\n options?: ContainerContentsStoreOptions | undefined,\n ): DeviceFirstContainerContents {\n return this.getOrCreateEntry(options).contents;\n }\n\n dispose(): void {\n // Stop every reconciler created this session — not just the current\n // scope's. A scope change (e.g. anonymous -> authenticated) leaves the old\n // scope's reconciler subscribed and its coordinator pump live; tear them\n // all down. Force-stop each scope's coordinator (which runs the reconciler\n // lane) alongside stopping the service.\n for (const [domainScope, entry] of this.entriesByScope) {\n entry.contents.reconciler.stop();\n entry.disconnectReconciliationTriggers();\n entry.unsubscribePersistedDocumentDeletions();\n entry.unsubscribePersistedDocuments();\n disposeDomainSyncCoordinator(domainScope);\n }\n this.entriesByScope.clear();\n }\n\n private workflowRuntime(): ContainerContentsStoreWorkflowRuntime {\n return createDeviceFirstWorkflowRuntime(this.runtimeService);\n }\n\n private getOrCreateEntry(\n options?: ContainerContentsStoreOptions | undefined,\n ): DeviceFirstScopeEntry {\n const runtime = this.workflowRuntime();\n const domainScope = runtime.state.domainScope;\n const existing = this.entriesByScope.get(domainScope);\n if (existing) {\n // Do not call updateRuntime here: facade selectors run during React\n // render, and updateRuntime emits synchronously. Hosts drive runtime\n // updates through view.updateRuntime() from an effect instead.\n return existing;\n }\n\n const store = getOrCreateLocalProjectionStore({\n domainScope,\n runtime,\n options,\n });\n const service = createReconciliationService(\n this.createReconciliationHost(store, domainScope),\n );\n const disconnectReconciliationTriggers = connectReconciliationTriggers({\n service,\n store,\n });\n const unsubscribePersistedDocumentDeletions =\n subscribeToPersistedDocumentDeletions(domainScope, (localId) => {\n store.removePersistedDocument(localId);\n });\n const unsubscribePersistedDocuments = subscribeToPersistedDocuments(\n domainScope,\n (document, change) =>\n store.refreshPersistedDocument(document, change.placementChanged),\n );\n service.start();\n\n const view: LocalProjectionView = {\n getSnapshot: () => store.getSnapshot(),\n subscribe: (listener) => store.subscribe(listener),\n setActiveContainer: (containerId) =>\n store.setActiveContainer(containerId),\n updateRuntime: (nextRuntime) => store.updateRuntime(nextRuntime),\n };\n\n const entry: DeviceFirstScopeEntry = {\n contents: {\n containerStore: store.getContainerStore(),\n reconciler: service,\n view,\n },\n disconnectReconciliationTriggers,\n unsubscribePersistedDocumentDeletions,\n unsubscribePersistedDocuments,\n };\n this.entriesByScope.set(domainScope, entry);\n return entry;\n }\n\n private createReconciliationHost(\n store: LocalProjectionStore,\n domainScope: ReconciliationHost[\"domainScope\"],\n ): ReconciliationHost {\n const runtimeService = this.runtimeService;\n const containerContents = this.containerContents;\n const requestDocumentContentPull = createReconciledDocumentContentPuller({\n getContainer: (containerId) =>\n store\n .getSnapshot()\n .containers.find((container) => container.id === containerId) ?? null,\n pullDocumentContent: (input) =>\n containerContents.pullDocumentContent(input),\n requestRegisteredDocumentRemoteSync: (localId, documentId) =>\n requestRegisteredDocumentRemoteSync(domainScope, localId, documentId),\n });\n const canDiscoverContainerDocuments = (containerId: string) => {\n const container = store\n .getSnapshot()\n .containers.find((node) => node.id === containerId);\n return container ? isRemoteBackedContainerNode(container) : false;\n };\n const listKnownContainerIds = () => {\n // A local-first id is not listable yet and may still be racing its create\n // request. Once remote-backed, it participates normally; system contents\n // are especially important during rematerialization because this device\n // may not have authored them.\n const homeOrganizationId =\n runtimeService.workflowInput().auth.organizationId;\n return store\n .getSnapshot()\n .containers.flatMap((node) =>\n isReconcilableContainerNode(node, homeOrganizationId)\n ? [node.id]\n : [],\n );\n };\n const listAutomaticRootCatchupContainerIds = () => {\n const homeOrganizationId =\n runtimeService.workflowInput().auth.organizationId;\n return store\n .getSnapshot()\n .containers.flatMap((node) =>\n isAutomaticRootCatchupContainerNode(node, homeOrganizationId)\n ? [node.id]\n : [],\n );\n };\n return {\n domainScope,\n canDiscoverContainerDocuments,\n getRuntimeStatus: () => {\n const input = runtimeService.workflowInput();\n return {\n dbStatus: input.infra.dbStatus,\n isAuthenticated: input.auth.isAuthenticated,\n online: input.state.online,\n };\n },\n listKnownContainerIds,\n // Root discovery can surface directly granted non-root containers, so\n // retain every regular container. Exclude only this identity's own system\n // children; cold/auth backfill and explicit full Refresh cover those.\n listAutomaticRootCatchupContainerIds,\n discoverContainerDocuments: (requests, onOutcome) =>\n discoverContainerDocumentLanesForRuntime({\n onOutcome,\n requests,\n runtimeService,\n }),\n loadContainerDelta: (containerId) =>\n store.loadContainerDelta(containerId),\n applyReconciled: (delta) => store.applyReconciled(delta),\n requestDocumentContentPull,\n ...createInitialDocumentProbeHost(runtimeService, domainScope),\n refreshTree: async () => {\n await store.getContainerStore().refresh();\n },\n refreshRootTree: async () => {\n await store.getContainerStore().refreshRootLane();\n },\n isIgnorableError: isDatabaseUnavailableError,\n // Resolve the runtime per call: it is rebuilt as the active identity\n // changes, and a reference captured here would report into a dead host.\n logError: (message, error) =>\n logReconciliationFailure(this.workflowRuntime(), message, error),\n };\n }\n}\n\n// Returns the host's result: both callbacks are declared `=> void` but may be\n// async, and the lane reporter only catches a rejection it is handed.\nfunction logReconciliationFailure(\n runtime: ContainerContentsStoreWorkflowRuntime,\n message: string,\n error: unknown,\n): unknown {\n const { log, logError } = runtime.util;\n return logError ? logError(message, error) : log(message);\n}\n\nexport function createDeviceFirstWorkflowRuntime(\n runtimeService: InternalRuntime,\n): ContainerContentsStoreWorkflowRuntime {\n return createContainerContentsStoreWorkflowRuntime(\n runtimeService.workflowInput(),\n runtimeService.adoptRootContainer,\n );\n}\n"]}
@@ -31,9 +31,9 @@ export declare function containerSyncWatermarkLaneKey(lane: ContainerSyncWaterma
31
31
  laneKind: string;
32
32
  };
33
33
  /**
34
- * The watermark half of the container-delete cascade, statement-only so the
35
- * caller can run it inside one atomic transaction alongside the rest of the
36
- * cascade.
34
+ * The lane-state half of the container-delete cascade (cursor and freshness
35
+ * marker share one row), statement-only so the caller can run it inside one
36
+ * atomic transaction alongside the rest of the cascade.
37
37
  */
38
38
  export declare function deleteContainerWatermarksInTransaction(tx: ClientSQLiteDatabase | ClientSQLiteTransactionScope, containerIds: ReadonlyArray<string>): Promise<void>;
39
39
  export declare const sqlContainerSyncWatermarkPersistence: {
@@ -43,6 +43,14 @@ export declare const sqlContainerSyncWatermarkPersistence: {
43
43
  loadCheckRecords(execSql: ExecSql, lanes: ReadonlyArray<ContainerSyncWatermarkLane>): Promise<Array<ContainerSyncLaneCheckRecord | null>>;
44
44
  markChecked(execSql: ExecSql, lane: ContainerSyncWatermarkLane, options?: ContainerSyncMarkerMutationOptions): Promise<boolean>;
45
45
  saveWatermark(execSql: ExecSql, lane: ContainerSyncWatermarkLane, watermark: SyncWatermark, options?: ContainerSyncMarkerMutationOptions): Promise<boolean>;
46
+ /**
47
+ * Upserts one lane row in a single statement: the cursor when `watermark`
48
+ * is set and the freshness marker when `checked`; the other half is kept.
49
+ */
50
+ recordLaneFetch(execSql: ExecSql, lane: ContainerSyncWatermarkLane, fetch: {
51
+ readonly checked: boolean;
52
+ readonly watermark: SyncWatermark | null;
53
+ }, options?: ContainerSyncMarkerMutationOptions): Promise<boolean>;
46
54
  deleteWatermarksForContainers(execSql: ExecSql, containerIds: ReadonlyArray<string>): Promise<void>;
47
55
  };
48
56
  export {};
@@ -1,5 +1,5 @@
1
- import { and, eq, inArray, or } from "drizzle-orm";
2
- import { containerSyncLaneChecks, containerSyncWatermarks, containerSyncWatermarkTables, } from "../../sqlite/schema.js";
1
+ import { and, eq, inArray, isNotNull, or } from "drizzle-orm";
2
+ import { containerSyncWatermarks, containerSyncWatermarkTables, } from "../../sqlite/schema.js";
3
3
  import { getClientSQLitePersistenceRuntime, } from "../../sqlite/sqlitePersistenceRuntime.js";
4
4
  import { ensureSqlTables } from "../../sqlite/sqlSchema.js";
5
5
  const CONTAINER_PARENT_LANE = "container_parent";
@@ -37,9 +37,9 @@ function containerParentLaneId(parentId) {
37
37
  : `${CONTAINER_PARENT_LANE_ID_PREFIX}${parentId}`;
38
38
  }
39
39
  /**
40
- * The watermark half of the container-delete cascade, statement-only so the
41
- * caller can run it inside one atomic transaction alongside the rest of the
42
- * cascade.
40
+ * The lane-state half of the container-delete cascade (cursor and freshness
41
+ * marker share one row), statement-only so the caller can run it inside one
42
+ * atomic transaction alongside the rest of the cascade.
43
43
  */
44
44
  export async function deleteContainerWatermarksInTransaction(tx, containerIds) {
45
45
  const uniqueContainerIds = Array.from(new Set(containerIds));
@@ -52,12 +52,14 @@ export async function deleteContainerWatermarksInTransaction(tx, containerIds) {
52
52
  .delete(containerSyncWatermarks)
53
53
  .where(or(and(eq(containerSyncWatermarks.laneKind, CONTAINER_PARENT_LANE), inArray(containerSyncWatermarks.laneId, parentLaneIds)), and(eq(containerSyncWatermarks.laneKind, CONTAINER_DOCUMENTS_LANE), inArray(containerSyncWatermarks.laneId, documentLaneIds))))
54
54
  .run();
55
- await tx
56
- .delete(containerSyncLaneChecks)
57
- .where(or(and(eq(containerSyncLaneChecks.laneKind, CONTAINER_PARENT_LANE), inArray(containerSyncLaneChecks.laneId, parentLaneIds)), and(eq(containerSyncLaneChecks.laneKind, CONTAINER_DOCUMENTS_LANE), inArray(containerSyncLaneChecks.laneId, documentLaneIds))))
58
- .run();
59
55
  }
60
56
  function mapSelectedWatermarkRecord(row) {
57
+ // A lane checked before any cursor was saved has no watermark yet.
58
+ if (row.watermarkId === null ||
59
+ row.watermarkUpdatedAt === null ||
60
+ row.updatedAt === null) {
61
+ return null;
62
+ }
61
63
  return {
62
64
  laneId: row.laneId,
63
65
  laneKind: row.laneKind,
@@ -121,14 +123,14 @@ async function selectWatermarkRows(execSql, lanes) {
121
123
  async function selectCheckRows(execSql, lanes) {
122
124
  await ensureSqlTables(execSql, containerSyncWatermarkTables);
123
125
  const { db } = getClientSQLitePersistenceRuntime(execSql);
124
- return selectLaneRowsInBatches(lanes, ({ laneId, laneKind }) => and(eq(containerSyncLaneChecks.laneKind, laneKind), eq(containerSyncLaneChecks.laneId, laneId)), (whereClause) => db
126
+ return selectLaneRowsInBatches(lanes, ({ laneId, laneKind }) => and(eq(containerSyncWatermarks.laneKind, laneKind), eq(containerSyncWatermarks.laneId, laneId)), async (whereClause) => (await db
125
127
  .select({
126
- checkedAt: containerSyncLaneChecks.checkedAt,
127
- laneId: containerSyncLaneChecks.laneId,
128
- laneKind: containerSyncLaneChecks.laneKind,
128
+ checkedAt: containerSyncWatermarks.checkedAt,
129
+ laneId: containerSyncWatermarks.laneId,
130
+ laneKind: containerSyncWatermarks.laneKind,
129
131
  })
130
- .from(containerSyncLaneChecks)
131
- .where(whereClause));
132
+ .from(containerSyncWatermarks)
133
+ .where(and(whereClause, isNotNull(containerSyncWatermarks.checkedAt)))).flatMap(({ checkedAt, laneId, laneKind }) => checkedAt === null ? [] : [{ checkedAt, laneId, laneKind }]));
132
134
  }
133
135
  export const sqlContainerSyncWatermarkPersistence = {
134
136
  async ensureSchema(execSql) {
@@ -145,7 +147,8 @@ export const sqlContainerSyncWatermarkPersistence = {
145
147
  const recordsByKey = new Map();
146
148
  for (const row of rows) {
147
149
  const record = mapSelectedWatermarkRecord(row);
148
- recordsByKey.set(laneKeyString(record), record);
150
+ if (record)
151
+ recordsByKey.set(laneKeyString(record), record);
149
152
  }
150
153
  return lanes.map((lane) => {
151
154
  const laneKey = containerSyncWatermarkLaneKey(lane);
@@ -163,65 +166,50 @@ export const sqlContainerSyncWatermarkPersistence = {
163
166
  return recordsByKey.get(laneKeyString(laneKey)) ?? null;
164
167
  });
165
168
  },
166
- async markChecked(execSql, lane, options) {
167
- await sqlContainerSyncWatermarkPersistence.ensureSchema(execSql);
168
- const { laneId, laneKind } = containerSyncWatermarkLaneKey(lane);
169
- const row = {
170
- checkedAt: new Date().toISOString(),
171
- laneId,
172
- laneKind,
173
- };
174
- const mark = async (db) => {
175
- await db
176
- .insert(containerSyncLaneChecks)
177
- .values(row)
178
- .onConflictDoUpdate({
179
- target: [
180
- containerSyncLaneChecks.laneKind,
181
- containerSyncLaneChecks.laneId,
182
- ],
183
- set: row,
184
- })
185
- .run();
186
- };
187
- const runtime = getClientSQLitePersistenceRuntime(execSql);
188
- if (!options?.stillCurrent) {
189
- await runtime.runMutation(mark);
190
- return true;
191
- }
192
- return (await runtime.guardedTransaction(mark, options.stillCurrent))
193
- .committed;
169
+ markChecked(execSql, lane, options) {
170
+ return sqlContainerSyncWatermarkPersistence.recordLaneFetch(execSql, lane, { checked: true, watermark: null }, options);
194
171
  },
195
- async saveWatermark(execSql, lane, watermark, options) {
172
+ saveWatermark(execSql, lane, watermark, options) {
173
+ return sqlContainerSyncWatermarkPersistence.recordLaneFetch(execSql, lane, { checked: false, watermark }, options);
174
+ },
175
+ /**
176
+ * Upserts one lane row in a single statement: the cursor when `watermark`
177
+ * is set and the freshness marker when `checked`; the other half is kept.
178
+ */
179
+ async recordLaneFetch(execSql, lane, fetch, options) {
180
+ if (!fetch.checked && !fetch.watermark)
181
+ return true;
196
182
  await sqlContainerSyncWatermarkPersistence.ensureSchema(execSql);
197
- const { laneId, laneKind } = containerSyncWatermarkLaneKey(lane);
198
- const updatedAt = new Date().toISOString();
199
- const row = {
200
- laneId,
201
- laneKind,
202
- watermarkId: watermark.id,
203
- watermarkUpdatedAt: watermark.updatedAt,
204
- updatedAt,
183
+ const now = new Date().toISOString();
184
+ const changed = {
185
+ ...(fetch.watermark
186
+ ? {
187
+ updatedAt: now,
188
+ watermarkId: fetch.watermark.id,
189
+ watermarkUpdatedAt: fetch.watermark.updatedAt,
190
+ }
191
+ : {}),
192
+ ...(fetch.checked ? { checkedAt: now } : {}),
205
193
  };
206
- const save = async (db) => {
194
+ const record = async (db) => {
207
195
  await db
208
196
  .insert(containerSyncWatermarks)
209
- .values(row)
197
+ .values({ ...containerSyncWatermarkLaneKey(lane), ...changed })
210
198
  .onConflictDoUpdate({
211
199
  target: [
212
200
  containerSyncWatermarks.laneKind,
213
201
  containerSyncWatermarks.laneId,
214
202
  ],
215
- set: row,
203
+ set: changed,
216
204
  })
217
205
  .run();
218
206
  };
219
207
  const runtime = getClientSQLitePersistenceRuntime(execSql);
220
208
  if (!options?.stillCurrent) {
221
- await runtime.runMutation(save);
209
+ await runtime.runMutation(record);
222
210
  return true;
223
211
  }
224
- return (await runtime.guardedTransaction(save, options.stillCurrent))
212
+ return (await runtime.guardedTransaction(record, options.stillCurrent))
225
213
  .committed;
226
214
  },
227
215
  async deleteWatermarksForContainers(execSql, containerIds) {
@@ -1 +1 @@
1
- {"version":3,"file":"containerSyncWatermarkPersistence.js","sourceRoot":"","sources":["containerSyncWatermarkPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SyncWatermark } from \"@tearleads/validators/response\";\nimport { and, eq, inArray, or, type SQL } from \"drizzle-orm\";\nimport {\n containerSyncLaneChecks,\n containerSyncWatermarks,\n containerSyncWatermarkTables,\n} from \"../../sqlite/schema\";\nimport {\n type ClientSQLiteDatabase,\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\n\nconst CONTAINER_PARENT_LANE = \"container_parent\";\nconst CONTAINER_DOCUMENTS_LANE = \"container_documents\";\nconst ROOT_CONTAINER_PARENT_LANE_ID = \"root\";\nconst CONTAINER_PARENT_LANE_ID_PREFIX = \"parent:\";\n// Each lane predicate binds two values, so keep batches comfortably under\n// SQLite's variable limit when a caller reads many lanes at once.\nconst LANE_SELECT_BATCH_SIZE = 100;\n\nexport type ContainerSyncWatermarkLane =\n | {\n kind: typeof CONTAINER_PARENT_LANE;\n parentId: string | null;\n }\n | {\n kind: typeof CONTAINER_DOCUMENTS_LANE;\n containerId: string;\n };\n\ninterface SelectedContainerSyncWatermark {\n laneId: string;\n laneKind: string;\n updatedAt: string;\n watermarkId: string;\n watermarkUpdatedAt: string;\n}\n\ninterface ContainerSyncWatermarkRecord {\n laneId: string;\n laneKind: string;\n updatedAt: string;\n watermark: SyncWatermark;\n}\n\nexport interface ContainerSyncLaneCheckRecord {\n checkedAt: string;\n laneId: string;\n laneKind: string;\n}\n\ninterface ContainerSyncMarkerMutationOptions {\n readonly stillCurrent?: (() => boolean) | undefined;\n}\n\nexport const containerParentSyncLane = (\n parentId: string | null,\n): ContainerSyncWatermarkLane => ({\n kind: CONTAINER_PARENT_LANE,\n parentId,\n});\n\nexport const containerContentsSyncLane = (\n containerId: string,\n): ContainerSyncWatermarkLane => ({\n kind: CONTAINER_DOCUMENTS_LANE,\n containerId,\n});\n\nexport function containerSyncWatermarkLaneKey(\n lane: ContainerSyncWatermarkLane,\n): {\n laneId: string;\n laneKind: string;\n} {\n switch (lane.kind) {\n case CONTAINER_PARENT_LANE:\n return {\n laneKind: lane.kind,\n laneId: containerParentLaneId(lane.parentId),\n };\n case CONTAINER_DOCUMENTS_LANE:\n return {\n laneKind: lane.kind,\n laneId: lane.containerId,\n };\n }\n}\n\nfunction containerParentLaneId(parentId: string | null): string {\n return parentId === null\n ? ROOT_CONTAINER_PARENT_LANE_ID\n : `${CONTAINER_PARENT_LANE_ID_PREFIX}${parentId}`;\n}\n\n/**\n * The watermark half of the container-delete cascade, statement-only so the\n * caller can run it inside one atomic transaction alongside the rest of the\n * cascade.\n */\nexport async function deleteContainerWatermarksInTransaction(\n tx: ClientSQLiteDatabase | ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n const uniqueContainerIds = Array.from(new Set(containerIds));\n if (uniqueContainerIds.length === 0) {\n return;\n }\n const parentLaneIds = uniqueContainerIds.map(containerParentLaneId);\n const documentLaneIds = uniqueContainerIds;\n await tx\n .delete(containerSyncWatermarks)\n .where(\n or(\n and(\n eq(containerSyncWatermarks.laneKind, CONTAINER_PARENT_LANE),\n inArray(containerSyncWatermarks.laneId, parentLaneIds),\n ),\n and(\n eq(containerSyncWatermarks.laneKind, CONTAINER_DOCUMENTS_LANE),\n inArray(containerSyncWatermarks.laneId, documentLaneIds),\n ),\n ),\n )\n .run();\n await tx\n .delete(containerSyncLaneChecks)\n .where(\n or(\n and(\n eq(containerSyncLaneChecks.laneKind, CONTAINER_PARENT_LANE),\n inArray(containerSyncLaneChecks.laneId, parentLaneIds),\n ),\n and(\n eq(containerSyncLaneChecks.laneKind, CONTAINER_DOCUMENTS_LANE),\n inArray(containerSyncLaneChecks.laneId, documentLaneIds),\n ),\n ),\n )\n .run();\n}\n\nfunction mapSelectedWatermarkRecord(\n row: SelectedContainerSyncWatermark,\n): ContainerSyncWatermarkRecord {\n return {\n laneId: row.laneId,\n laneKind: row.laneKind,\n updatedAt: row.updatedAt,\n watermark: {\n id: row.watermarkId,\n updatedAt: row.watermarkUpdatedAt,\n },\n };\n}\n\nfunction laneKeyString(input: { laneId: string; laneKind: string }): string {\n return `${input.laneKind}\\0${input.laneId}`;\n}\n\nfunction uniqueLaneKeys(\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n): Array<{ laneId: string; laneKind: string }> {\n const seen = new Set<string>();\n const laneKeys: Array<{ laneId: string; laneKind: string }> = [];\n\n for (const lane of lanes) {\n const laneKey = containerSyncWatermarkLaneKey(lane);\n const key = laneKeyString(laneKey);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n laneKeys.push(laneKey);\n }\n\n return laneKeys;\n}\n\n// The ONE batched lane-select shape for this file: both row readers must\n// chunk, because a caller may pass unboundedly many lanes and each lane\n// predicate binds two variables.\nasync function selectLaneRowsInBatches<TRow>(\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n buildLanePredicate: (laneKey: {\n laneId: string;\n laneKind: string;\n }) => SQL | undefined,\n selectBatch: (whereClause: SQL) => Promise<TRow[]>,\n): Promise<TRow[]> {\n const laneKeys = uniqueLaneKeys(lanes);\n const rows: TRow[] = [];\n\n for (\n let index = 0;\n index < laneKeys.length;\n index += LANE_SELECT_BATCH_SIZE\n ) {\n const lanePredicates = laneKeys\n .slice(index, index + LANE_SELECT_BATCH_SIZE)\n .map(buildLanePredicate)\n .filter((predicate): predicate is SQL => predicate !== undefined);\n const whereClause =\n lanePredicates.length === 1 ? lanePredicates[0] : or(...lanePredicates);\n if (!whereClause) {\n continue;\n }\n\n rows.push(...(await selectBatch(whereClause)));\n }\n\n return rows;\n}\n\nasync function selectWatermarkRows(\n execSql: ExecSql,\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n): Promise<SelectedContainerSyncWatermark[]> {\n await ensureSqlTables(execSql, containerSyncWatermarkTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n\n return selectLaneRowsInBatches(\n lanes,\n ({ laneId, laneKind }) =>\n and(\n eq(containerSyncWatermarks.laneKind, laneKind),\n eq(containerSyncWatermarks.laneId, laneId),\n ),\n (whereClause) =>\n db\n .select({\n laneId: containerSyncWatermarks.laneId,\n laneKind: containerSyncWatermarks.laneKind,\n updatedAt: containerSyncWatermarks.updatedAt,\n watermarkId: containerSyncWatermarks.watermarkId,\n watermarkUpdatedAt: containerSyncWatermarks.watermarkUpdatedAt,\n })\n .from(containerSyncWatermarks)\n .where(whereClause),\n );\n}\n\nasync function selectCheckRows(\n execSql: ExecSql,\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n): Promise<ContainerSyncLaneCheckRecord[]> {\n await ensureSqlTables(execSql, containerSyncWatermarkTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n\n return selectLaneRowsInBatches(\n lanes,\n ({ laneId, laneKind }) =>\n and(\n eq(containerSyncLaneChecks.laneKind, laneKind),\n eq(containerSyncLaneChecks.laneId, laneId),\n ),\n (whereClause) =>\n db\n .select({\n checkedAt: containerSyncLaneChecks.checkedAt,\n laneId: containerSyncLaneChecks.laneId,\n laneKind: containerSyncLaneChecks.laneKind,\n })\n .from(containerSyncLaneChecks)\n .where(whereClause),\n );\n}\n\nexport const sqlContainerSyncWatermarkPersistence = {\n async ensureSchema(execSql: ExecSql): Promise<void> {\n await ensureSqlTables(execSql, containerSyncWatermarkTables);\n },\n\n async loadWatermark(\n execSql: ExecSql,\n lane: ContainerSyncWatermarkLane,\n ): Promise<SyncWatermark | null> {\n const [record] =\n await sqlContainerSyncWatermarkPersistence.loadWatermarkRecords(execSql, [\n lane,\n ]);\n return record?.watermark ?? null;\n },\n\n async loadWatermarkRecords(\n execSql: ExecSql,\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n ): Promise<Array<ContainerSyncWatermarkRecord | null>> {\n const rows = await selectWatermarkRows(execSql, lanes);\n const recordsByKey = new Map<string, ContainerSyncWatermarkRecord>();\n\n for (const row of rows) {\n const record = mapSelectedWatermarkRecord(row);\n recordsByKey.set(laneKeyString(record), record);\n }\n\n return lanes.map((lane) => {\n const laneKey = containerSyncWatermarkLaneKey(lane);\n return recordsByKey.get(laneKeyString(laneKey)) ?? null;\n });\n },\n\n async loadCheckRecords(\n execSql: ExecSql,\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n ): Promise<Array<ContainerSyncLaneCheckRecord | null>> {\n const rows = await selectCheckRows(execSql, lanes);\n const recordsByKey = new Map<string, ContainerSyncLaneCheckRecord>();\n\n for (const row of rows) {\n recordsByKey.set(laneKeyString(row), row);\n }\n\n return lanes.map((lane) => {\n const laneKey = containerSyncWatermarkLaneKey(lane);\n return recordsByKey.get(laneKeyString(laneKey)) ?? null;\n });\n },\n\n async markChecked(\n execSql: ExecSql,\n lane: ContainerSyncWatermarkLane,\n options?: ContainerSyncMarkerMutationOptions,\n ): Promise<boolean> {\n await sqlContainerSyncWatermarkPersistence.ensureSchema(execSql);\n const { laneId, laneKind } = containerSyncWatermarkLaneKey(lane);\n const row = {\n checkedAt: new Date().toISOString(),\n laneId,\n laneKind,\n };\n\n const mark = async (\n db: ClientSQLiteDatabase | ClientSQLiteTransactionScope,\n ) => {\n await db\n .insert(containerSyncLaneChecks)\n .values(row)\n .onConflictDoUpdate({\n target: [\n containerSyncLaneChecks.laneKind,\n containerSyncLaneChecks.laneId,\n ],\n set: row,\n })\n .run();\n };\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n if (!options?.stillCurrent) {\n await runtime.runMutation(mark);\n return true;\n }\n return (await runtime.guardedTransaction(mark, options.stillCurrent))\n .committed;\n },\n\n async saveWatermark(\n execSql: ExecSql,\n lane: ContainerSyncWatermarkLane,\n watermark: SyncWatermark,\n options?: ContainerSyncMarkerMutationOptions,\n ): Promise<boolean> {\n await sqlContainerSyncWatermarkPersistence.ensureSchema(execSql);\n const { laneId, laneKind } = containerSyncWatermarkLaneKey(lane);\n const updatedAt = new Date().toISOString();\n const row = {\n laneId,\n laneKind,\n watermarkId: watermark.id,\n watermarkUpdatedAt: watermark.updatedAt,\n updatedAt,\n };\n\n const save = async (\n db: ClientSQLiteDatabase | ClientSQLiteTransactionScope,\n ) => {\n await db\n .insert(containerSyncWatermarks)\n .values(row)\n .onConflictDoUpdate({\n target: [\n containerSyncWatermarks.laneKind,\n containerSyncWatermarks.laneId,\n ],\n set: row,\n })\n .run();\n };\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n if (!options?.stillCurrent) {\n await runtime.runMutation(save);\n return true;\n }\n return (await runtime.guardedTransaction(save, options.stillCurrent))\n .committed;\n },\n\n async deleteWatermarksForContainers(\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ): Promise<void> {\n const uniqueContainerIds = Array.from(new Set(containerIds));\n if (uniqueContainerIds.length === 0) {\n return;\n }\n\n await sqlContainerSyncWatermarkPersistence.ensureSchema(execSql);\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {\n await deleteContainerWatermarksInTransaction(db, uniqueContainerIds);\n });\n },\n};\n"]}
1
+ {"version":3,"file":"containerSyncWatermarkPersistence.js","sourceRoot":"","sources":["containerSyncWatermarkPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SyncWatermark } from \"@tearleads/validators/response\";\nimport { and, eq, inArray, isNotNull, or, type SQL } from \"drizzle-orm\";\nimport {\n containerSyncWatermarks,\n containerSyncWatermarkTables,\n} from \"../../sqlite/schema\";\nimport {\n type ClientSQLiteDatabase,\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\n\nconst CONTAINER_PARENT_LANE = \"container_parent\";\nconst CONTAINER_DOCUMENTS_LANE = \"container_documents\";\nconst ROOT_CONTAINER_PARENT_LANE_ID = \"root\";\nconst CONTAINER_PARENT_LANE_ID_PREFIX = \"parent:\";\n// Each lane predicate binds two values, so keep batches comfortably under\n// SQLite's variable limit when a caller reads many lanes at once.\nconst LANE_SELECT_BATCH_SIZE = 100;\n\nexport type ContainerSyncWatermarkLane =\n | {\n kind: typeof CONTAINER_PARENT_LANE;\n parentId: string | null;\n }\n | {\n kind: typeof CONTAINER_DOCUMENTS_LANE;\n containerId: string;\n };\n\ninterface SelectedContainerSyncWatermark {\n laneId: string;\n laneKind: string;\n updatedAt: string | null;\n watermarkId: string | null;\n watermarkUpdatedAt: string | null;\n}\n\ninterface ContainerSyncWatermarkRecord {\n laneId: string;\n laneKind: string;\n updatedAt: string;\n watermark: SyncWatermark;\n}\n\nexport interface ContainerSyncLaneCheckRecord {\n checkedAt: string;\n laneId: string;\n laneKind: string;\n}\n\ninterface ContainerSyncMarkerMutationOptions {\n readonly stillCurrent?: (() => boolean) | undefined;\n}\n\nexport const containerParentSyncLane = (\n parentId: string | null,\n): ContainerSyncWatermarkLane => ({\n kind: CONTAINER_PARENT_LANE,\n parentId,\n});\n\nexport const containerContentsSyncLane = (\n containerId: string,\n): ContainerSyncWatermarkLane => ({\n kind: CONTAINER_DOCUMENTS_LANE,\n containerId,\n});\n\nexport function containerSyncWatermarkLaneKey(\n lane: ContainerSyncWatermarkLane,\n): {\n laneId: string;\n laneKind: string;\n} {\n switch (lane.kind) {\n case CONTAINER_PARENT_LANE:\n return {\n laneKind: lane.kind,\n laneId: containerParentLaneId(lane.parentId),\n };\n case CONTAINER_DOCUMENTS_LANE:\n return {\n laneKind: lane.kind,\n laneId: lane.containerId,\n };\n }\n}\n\nfunction containerParentLaneId(parentId: string | null): string {\n return parentId === null\n ? ROOT_CONTAINER_PARENT_LANE_ID\n : `${CONTAINER_PARENT_LANE_ID_PREFIX}${parentId}`;\n}\n\n/**\n * The lane-state half of the container-delete cascade (cursor and freshness\n * marker share one row), statement-only so the caller can run it inside one\n * atomic transaction alongside the rest of the cascade.\n */\nexport async function deleteContainerWatermarksInTransaction(\n tx: ClientSQLiteDatabase | ClientSQLiteTransactionScope,\n containerIds: ReadonlyArray<string>,\n): Promise<void> {\n const uniqueContainerIds = Array.from(new Set(containerIds));\n if (uniqueContainerIds.length === 0) {\n return;\n }\n const parentLaneIds = uniqueContainerIds.map(containerParentLaneId);\n const documentLaneIds = uniqueContainerIds;\n await tx\n .delete(containerSyncWatermarks)\n .where(\n or(\n and(\n eq(containerSyncWatermarks.laneKind, CONTAINER_PARENT_LANE),\n inArray(containerSyncWatermarks.laneId, parentLaneIds),\n ),\n and(\n eq(containerSyncWatermarks.laneKind, CONTAINER_DOCUMENTS_LANE),\n inArray(containerSyncWatermarks.laneId, documentLaneIds),\n ),\n ),\n )\n .run();\n}\n\nfunction mapSelectedWatermarkRecord(\n row: SelectedContainerSyncWatermark,\n): ContainerSyncWatermarkRecord | null {\n // A lane checked before any cursor was saved has no watermark yet.\n if (\n row.watermarkId === null ||\n row.watermarkUpdatedAt === null ||\n row.updatedAt === null\n ) {\n return null;\n }\n return {\n laneId: row.laneId,\n laneKind: row.laneKind,\n updatedAt: row.updatedAt,\n watermark: {\n id: row.watermarkId,\n updatedAt: row.watermarkUpdatedAt,\n },\n };\n}\n\nfunction laneKeyString(input: { laneId: string; laneKind: string }): string {\n return `${input.laneKind}\\0${input.laneId}`;\n}\n\nfunction uniqueLaneKeys(\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n): Array<{ laneId: string; laneKind: string }> {\n const seen = new Set<string>();\n const laneKeys: Array<{ laneId: string; laneKind: string }> = [];\n\n for (const lane of lanes) {\n const laneKey = containerSyncWatermarkLaneKey(lane);\n const key = laneKeyString(laneKey);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n laneKeys.push(laneKey);\n }\n\n return laneKeys;\n}\n\n// The ONE batched lane-select shape for this file: both row readers must\n// chunk, because a caller may pass unboundedly many lanes and each lane\n// predicate binds two variables.\nasync function selectLaneRowsInBatches<TRow>(\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n buildLanePredicate: (laneKey: {\n laneId: string;\n laneKind: string;\n }) => SQL | undefined,\n selectBatch: (whereClause: SQL) => Promise<TRow[]>,\n): Promise<TRow[]> {\n const laneKeys = uniqueLaneKeys(lanes);\n const rows: TRow[] = [];\n\n for (\n let index = 0;\n index < laneKeys.length;\n index += LANE_SELECT_BATCH_SIZE\n ) {\n const lanePredicates = laneKeys\n .slice(index, index + LANE_SELECT_BATCH_SIZE)\n .map(buildLanePredicate)\n .filter((predicate): predicate is SQL => predicate !== undefined);\n const whereClause =\n lanePredicates.length === 1 ? lanePredicates[0] : or(...lanePredicates);\n if (!whereClause) {\n continue;\n }\n\n rows.push(...(await selectBatch(whereClause)));\n }\n\n return rows;\n}\n\nasync function selectWatermarkRows(\n execSql: ExecSql,\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n): Promise<SelectedContainerSyncWatermark[]> {\n await ensureSqlTables(execSql, containerSyncWatermarkTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n\n return selectLaneRowsInBatches(\n lanes,\n ({ laneId, laneKind }) =>\n and(\n eq(containerSyncWatermarks.laneKind, laneKind),\n eq(containerSyncWatermarks.laneId, laneId),\n ),\n (whereClause) =>\n db\n .select({\n laneId: containerSyncWatermarks.laneId,\n laneKind: containerSyncWatermarks.laneKind,\n updatedAt: containerSyncWatermarks.updatedAt,\n watermarkId: containerSyncWatermarks.watermarkId,\n watermarkUpdatedAt: containerSyncWatermarks.watermarkUpdatedAt,\n })\n .from(containerSyncWatermarks)\n .where(whereClause),\n );\n}\n\nasync function selectCheckRows(\n execSql: ExecSql,\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n): Promise<ContainerSyncLaneCheckRecord[]> {\n await ensureSqlTables(execSql, containerSyncWatermarkTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n\n return selectLaneRowsInBatches(\n lanes,\n ({ laneId, laneKind }) =>\n and(\n eq(containerSyncWatermarks.laneKind, laneKind),\n eq(containerSyncWatermarks.laneId, laneId),\n ),\n async (whereClause) =>\n (\n await db\n .select({\n checkedAt: containerSyncWatermarks.checkedAt,\n laneId: containerSyncWatermarks.laneId,\n laneKind: containerSyncWatermarks.laneKind,\n })\n .from(containerSyncWatermarks)\n .where(and(whereClause, isNotNull(containerSyncWatermarks.checkedAt)))\n ).flatMap(({ checkedAt, laneId, laneKind }) =>\n checkedAt === null ? [] : [{ checkedAt, laneId, laneKind }],\n ),\n );\n}\n\nexport const sqlContainerSyncWatermarkPersistence = {\n async ensureSchema(execSql: ExecSql): Promise<void> {\n await ensureSqlTables(execSql, containerSyncWatermarkTables);\n },\n\n async loadWatermark(\n execSql: ExecSql,\n lane: ContainerSyncWatermarkLane,\n ): Promise<SyncWatermark | null> {\n const [record] =\n await sqlContainerSyncWatermarkPersistence.loadWatermarkRecords(execSql, [\n lane,\n ]);\n return record?.watermark ?? null;\n },\n\n async loadWatermarkRecords(\n execSql: ExecSql,\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n ): Promise<Array<ContainerSyncWatermarkRecord | null>> {\n const rows = await selectWatermarkRows(execSql, lanes);\n const recordsByKey = new Map<string, ContainerSyncWatermarkRecord>();\n\n for (const row of rows) {\n const record = mapSelectedWatermarkRecord(row);\n if (record) recordsByKey.set(laneKeyString(record), record);\n }\n\n return lanes.map((lane) => {\n const laneKey = containerSyncWatermarkLaneKey(lane);\n return recordsByKey.get(laneKeyString(laneKey)) ?? null;\n });\n },\n\n async loadCheckRecords(\n execSql: ExecSql,\n lanes: ReadonlyArray<ContainerSyncWatermarkLane>,\n ): Promise<Array<ContainerSyncLaneCheckRecord | null>> {\n const rows = await selectCheckRows(execSql, lanes);\n const recordsByKey = new Map<string, ContainerSyncLaneCheckRecord>();\n\n for (const row of rows) {\n recordsByKey.set(laneKeyString(row), row);\n }\n\n return lanes.map((lane) => {\n const laneKey = containerSyncWatermarkLaneKey(lane);\n return recordsByKey.get(laneKeyString(laneKey)) ?? null;\n });\n },\n\n markChecked(\n execSql: ExecSql,\n lane: ContainerSyncWatermarkLane,\n options?: ContainerSyncMarkerMutationOptions,\n ): Promise<boolean> {\n return sqlContainerSyncWatermarkPersistence.recordLaneFetch(\n execSql,\n lane,\n { checked: true, watermark: null },\n options,\n );\n },\n\n saveWatermark(\n execSql: ExecSql,\n lane: ContainerSyncWatermarkLane,\n watermark: SyncWatermark,\n options?: ContainerSyncMarkerMutationOptions,\n ): Promise<boolean> {\n return sqlContainerSyncWatermarkPersistence.recordLaneFetch(\n execSql,\n lane,\n { checked: false, watermark },\n options,\n );\n },\n\n /**\n * Upserts one lane row in a single statement: the cursor when `watermark`\n * is set and the freshness marker when `checked`; the other half is kept.\n */\n async recordLaneFetch(\n execSql: ExecSql,\n lane: ContainerSyncWatermarkLane,\n fetch: {\n readonly checked: boolean;\n readonly watermark: SyncWatermark | null;\n },\n options?: ContainerSyncMarkerMutationOptions,\n ): Promise<boolean> {\n if (!fetch.checked && !fetch.watermark) return true;\n await sqlContainerSyncWatermarkPersistence.ensureSchema(execSql);\n const now = new Date().toISOString();\n const changed = {\n ...(fetch.watermark\n ? {\n updatedAt: now,\n watermarkId: fetch.watermark.id,\n watermarkUpdatedAt: fetch.watermark.updatedAt,\n }\n : {}),\n ...(fetch.checked ? { checkedAt: now } : {}),\n };\n const record = async (\n db: ClientSQLiteDatabase | ClientSQLiteTransactionScope,\n ) => {\n await db\n .insert(containerSyncWatermarks)\n .values({ ...containerSyncWatermarkLaneKey(lane), ...changed })\n .onConflictDoUpdate({\n target: [\n containerSyncWatermarks.laneKind,\n containerSyncWatermarks.laneId,\n ],\n set: changed,\n })\n .run();\n };\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n if (!options?.stillCurrent) {\n await runtime.runMutation(record);\n return true;\n }\n return (await runtime.guardedTransaction(record, options.stillCurrent))\n .committed;\n },\n\n async deleteWatermarksForContainers(\n execSql: ExecSql,\n containerIds: ReadonlyArray<string>,\n ): Promise<void> {\n const uniqueContainerIds = Array.from(new Set(containerIds));\n if (uniqueContainerIds.length === 0) {\n return;\n }\n\n await sqlContainerSyncWatermarkPersistence.ensureSchema(execSql);\n await getClientSQLitePersistenceRuntime(execSql).runMutation(async (db) => {\n await deleteContainerWatermarksInTransaction(db, uniqueContainerIds);\n });\n },\n};\n"]}