@cleocode/core 2026.9.26 → 2026.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-provenance.json +2 -2
- package/dist/cloud/journal.d.ts +9 -5
- package/dist/cloud/journal.d.ts.map +1 -1
- package/dist/cloud/journal.js +29 -8
- package/dist/cloud/journal.js.map +1 -1
- package/dist/cloud/manifest-check.d.ts +140 -4
- package/dist/cloud/manifest-check.d.ts.map +1 -1
- package/dist/cloud/manifest-check.js +309 -10
- package/dist/cloud/manifest-check.js.map +1 -1
- package/dist/cloud/nexus-attach.d.ts +119 -0
- package/dist/cloud/nexus-attach.d.ts.map +1 -0
- package/dist/cloud/nexus-attach.js +267 -0
- package/dist/cloud/nexus-attach.js.map +1 -0
- package/dist/cloud/nexus-auth.d.ts +15 -3
- package/dist/cloud/nexus-auth.d.ts.map +1 -1
- package/dist/cloud/nexus-auth.js +90 -6
- package/dist/cloud/nexus-auth.js.map +1 -1
- package/dist/cloud/nexus-cloud-activity.d.ts +37 -0
- package/dist/cloud/nexus-cloud-activity.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud-activity.js +79 -0
- package/dist/cloud/nexus-cloud-activity.js.map +1 -0
- package/dist/cloud/nexus-cloud-status.d.ts +132 -0
- package/dist/cloud/nexus-cloud-status.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud-status.js +476 -0
- package/dist/cloud/nexus-cloud-status.js.map +1 -0
- package/dist/cloud/nexus-cloud.d.ts +179 -0
- package/dist/cloud/nexus-cloud.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud.js +296 -0
- package/dist/cloud/nexus-cloud.js.map +1 -0
- package/dist/cloud/nexus-device.d.ts +32 -7
- package/dist/cloud/nexus-device.d.ts.map +1 -1
- package/dist/cloud/nexus-device.js +54 -7
- package/dist/cloud/nexus-device.js.map +1 -1
- package/dist/cloud/nexus-enrol.d.ts +35 -4
- package/dist/cloud/nexus-enrol.d.ts.map +1 -1
- package/dist/cloud/nexus-enrol.js +192 -47
- package/dist/cloud/nexus-enrol.js.map +1 -1
- package/dist/cloud/nexus-home.d.ts +74 -0
- package/dist/cloud/nexus-home.d.ts.map +1 -0
- package/dist/cloud/nexus-home.js +97 -0
- package/dist/cloud/nexus-home.js.map +1 -0
- package/dist/cloud/nexus-link.d.ts +8 -1
- package/dist/cloud/nexus-link.d.ts.map +1 -1
- package/dist/cloud/nexus-link.js +138 -11
- package/dist/cloud/nexus-link.js.map +1 -1
- package/dist/cloud/nexus-logout.d.ts +114 -0
- package/dist/cloud/nexus-logout.d.ts.map +1 -0
- package/dist/cloud/nexus-logout.js +429 -0
- package/dist/cloud/nexus-logout.js.map +1 -0
- package/dist/cloud/nexus-vault-keys.d.ts +129 -0
- package/dist/cloud/nexus-vault-keys.d.ts.map +1 -0
- package/dist/cloud/nexus-vault-keys.js +277 -0
- package/dist/cloud/nexus-vault-keys.js.map +1 -0
- package/dist/cloud/nexus-vault-state.d.ts +121 -0
- package/dist/cloud/nexus-vault-state.d.ts.map +1 -0
- package/dist/cloud/nexus-vault-state.js +274 -0
- package/dist/cloud/nexus-vault-state.js.map +1 -0
- package/dist/cloud/nexus-vault.d.ts +160 -0
- package/dist/cloud/nexus-vault.d.ts.map +1 -0
- package/dist/cloud/nexus-vault.js +1733 -0
- package/dist/cloud/nexus-vault.js.map +1 -0
- package/dist/cloud/signing.d.ts +24 -8
- package/dist/cloud/signing.d.ts.map +1 -1
- package/dist/cloud/signing.js +61 -15
- package/dist/cloud/signing.js.map +1 -1
- package/dist/docs/export-document.js +8341 -3317
- package/dist/docs/export-document.js.map +3 -3
- package/dist/doctor/migrations.d.ts +75 -0
- package/dist/doctor/migrations.d.ts.map +1 -0
- package/dist/doctor/migrations.js +136 -0
- package/dist/doctor/migrations.js.map +1 -0
- package/dist/doctor/sync-sealer.d.ts +48 -0
- package/dist/doctor/sync-sealer.d.ts.map +1 -0
- package/dist/doctor/sync-sealer.js +109 -0
- package/dist/doctor/sync-sealer.js.map +1 -0
- package/dist/doctor/sync-triggers.d.ts +53 -0
- package/dist/doctor/sync-triggers.d.ts.map +1 -0
- package/dist/doctor/sync-triggers.js +157 -0
- package/dist/doctor/sync-triggers.js.map +1 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +5 -1
- package/dist/init.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/llm/plugin-facade.js +8668 -3644
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/nexus/projects-clean.d.ts +3 -0
- package/dist/nexus/projects-clean.d.ts.map +1 -1
- package/dist/nexus/projects-clean.js +9 -3
- package/dist/nexus/projects-clean.js.map +1 -1
- package/dist/nexus/registry-roots.d.ts +3 -1
- package/dist/nexus/registry-roots.d.ts.map +1 -1
- package/dist/nexus/registry-roots.js +5 -2
- package/dist/nexus/registry-roots.js.map +1 -1
- package/dist/nexus/registry.d.ts +8 -1
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +24 -10
- package/dist/nexus/registry.js.map +1 -1
- package/dist/nexus/workspace.d.ts +4 -1
- package/dist/nexus/workspace.d.ts.map +1 -1
- package/dist/nexus/workspace.js +10 -4
- package/dist/nexus/workspace.js.map +1 -1
- package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
- package/dist/orchestrate/spawn-ops.js +7 -2
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/orchestrate/worker-verify.d.ts +121 -12
- package/dist/orchestrate/worker-verify.d.ts.map +1 -1
- package/dist/orchestrate/worker-verify.js +172 -21
- package/dist/orchestrate/worker-verify.js.map +1 -1
- package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
- package/dist/orchestration/spawn-prompt.js +15 -9
- package/dist/orchestration/spawn-prompt.js.map +1 -1
- package/dist/release/ci-evidence.d.ts +9 -0
- package/dist/release/ci-evidence.d.ts.map +1 -1
- package/dist/release/ci-evidence.js +9 -2
- package/dist/release/ci-evidence.js.map +1 -1
- package/dist/resources/backend.d.ts +15 -3
- package/dist/resources/backend.d.ts.map +1 -1
- package/dist/resources/backend.js +7 -3
- package/dist/resources/backend.js.map +1 -1
- package/dist/resources/darwin-backend.d.ts +142 -0
- package/dist/resources/darwin-backend.d.ts.map +1 -0
- package/dist/resources/darwin-backend.js +288 -0
- package/dist/resources/darwin-backend.js.map +1 -0
- package/dist/resources/governor.d.ts +44 -1
- package/dist/resources/governor.d.ts.map +1 -1
- package/dist/resources/governor.js +100 -36
- package/dist/resources/governor.js.map +1 -1
- package/dist/resources/heavy-command.d.ts +132 -0
- package/dist/resources/heavy-command.d.ts.map +1 -0
- package/dist/resources/heavy-command.js +768 -0
- package/dist/resources/heavy-command.js.map +1 -0
- package/dist/resources/monitor.d.ts +50 -1
- package/dist/resources/monitor.d.ts.map +1 -1
- package/dist/resources/monitor.js +83 -1
- package/dist/resources/monitor.js.map +1 -1
- package/dist/resources/run-admission.d.ts +303 -0
- package/dist/resources/run-admission.d.ts.map +1 -0
- package/dist/resources/run-admission.js +648 -0
- package/dist/resources/run-admission.js.map +1 -0
- package/dist/resources/run-class.d.ts +111 -0
- package/dist/resources/run-class.d.ts.map +1 -0
- package/dist/resources/run-class.js +658 -0
- package/dist/resources/run-class.js.map +1 -0
- package/dist/resources/run-governed.d.ts +158 -0
- package/dist/resources/run-governed.d.ts.map +1 -0
- package/dist/resources/run-governed.js +406 -0
- package/dist/resources/run-governed.js.map +1 -0
- package/dist/resources/slot-holder.d.ts +267 -0
- package/dist/resources/slot-holder.d.ts.map +1 -0
- package/dist/resources/slot-holder.js +505 -0
- package/dist/resources/slot-holder.js.map +1 -0
- package/dist/resources/tool-groups.d.ts +74 -0
- package/dist/resources/tool-groups.d.ts.map +1 -0
- package/dist/resources/tool-groups.js +118 -0
- package/dist/resources/tool-groups.js.map +1 -0
- package/dist/routing/build-command-groups.d.ts.map +1 -1
- package/dist/routing/build-command-groups.js +1 -0
- package/dist/routing/build-command-groups.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +8676 -3652
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/sentient/cross-project-hygiene.d.ts +3 -2
- package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
- package/dist/sentient/cross-project-hygiene.js +5 -2
- package/dist/sentient/cross-project-hygiene.js.map +1 -1
- package/dist/sentient/state.d.ts +7 -0
- package/dist/sentient/state.d.ts.map +1 -1
- package/dist/sentient/state.js.map +1 -1
- package/dist/sentient/tick.d.ts +14 -2
- package/dist/sentient/tick.d.ts.map +1 -1
- package/dist/sentient/tick.js +63 -4
- package/dist/sentient/tick.js.map +1 -1
- package/dist/setup/sections/nexus-account.d.ts +8 -6
- package/dist/setup/sections/nexus-account.d.ts.map +1 -1
- package/dist/setup/sections/nexus-account.js +53 -9
- package/dist/setup/sections/nexus-account.js.map +1 -1
- package/dist/store/backup-recover.d.ts.map +1 -1
- package/dist/store/backup-recover.js +5 -0
- package/dist/store/backup-recover.js.map +1 -1
- package/dist/store/data-accessor.d.ts +1 -0
- package/dist/store/data-accessor.d.ts.map +1 -1
- package/dist/store/data-accessor.js +7 -0
- package/dist/store/data-accessor.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts +23 -1
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +126 -36
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/recovery.d.ts.map +1 -1
- package/dist/store/exodus/recovery.js +15 -0
- package/dist/store/exodus/recovery.js.map +1 -1
- package/dist/store/legacy-tasks-lineage.d.ts.map +1 -1
- package/dist/store/legacy-tasks-lineage.js +5 -0
- package/dist/store/legacy-tasks-lineage.js.map +1 -1
- package/dist/store/lock.d.ts +8 -1
- package/dist/store/lock.d.ts.map +1 -1
- package/dist/store/lock.js +10 -2
- package/dist/store/lock.js.map +1 -1
- package/dist/store/migration-manager.d.ts +5 -0
- package/dist/store/migration-manager.d.ts.map +1 -1
- package/dist/store/migration-manager.js +11 -0
- package/dist/store/migration-manager.js.map +1 -1
- package/dist/store/migration-runner.d.ts +158 -0
- package/dist/store/migration-runner.d.ts.map +1 -0
- package/dist/store/migration-runner.js +493 -0
- package/dist/store/migration-runner.js.map +1 -0
- package/dist/store/portable-bundle-import.d.ts +15 -1
- package/dist/store/portable-bundle-import.d.ts.map +1 -1
- package/dist/store/portable-bundle-import.js +24 -4
- package/dist/store/portable-bundle-import.js.map +1 -1
- package/dist/store/portable-bundle-relocate.d.ts +43 -0
- package/dist/store/portable-bundle-relocate.d.ts.map +1 -1
- package/dist/store/portable-bundle-relocate.js +79 -5
- package/dist/store/portable-bundle-relocate.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts +20 -0
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +38 -3
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/portable-bundle.d.ts +33 -0
- package/dist/store/portable-bundle.d.ts.map +1 -1
- package/dist/store/portable-bundle.js +34 -7
- package/dist/store/portable-bundle.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts +137 -7
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +364 -9
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/row-identity.d.ts.map +1 -1
- package/dist/store/row-identity.js +9 -10
- package/dist/store/row-identity.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +10 -0
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sqlite.d.ts +31 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +5 -1
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sync/capture.d.ts +203 -0
- package/dist/store/sync/capture.d.ts.map +1 -0
- package/dist/store/sync/capture.js +562 -0
- package/dist/store/sync/capture.js.map +1 -0
- package/dist/store/sync/clock-store.d.ts.map +1 -1
- package/dist/store/sync/clock-store.js +2 -0
- package/dist/store/sync/clock-store.js.map +1 -1
- package/dist/store/sync/fk-orphans.d.ts +133 -0
- package/dist/store/sync/fk-orphans.d.ts.map +1 -0
- package/dist/store/sync/fk-orphans.js +265 -0
- package/dist/store/sync/fk-orphans.js.map +1 -0
- package/dist/store/sync/flags.d.ts +7 -0
- package/dist/store/sync/flags.d.ts.map +1 -1
- package/dist/store/sync/flags.js +16 -0
- package/dist/store/sync/flags.js.map +1 -1
- package/dist/store/sync/hlc.d.ts.map +1 -1
- package/dist/store/sync/hlc.js +4 -0
- package/dist/store/sync/hlc.js.map +1 -1
- package/dist/store/sync/identity-fill.d.ts +45 -0
- package/dist/store/sync/identity-fill.d.ts.map +1 -0
- package/dist/store/sync/identity-fill.js +85 -0
- package/dist/store/sync/identity-fill.js.map +1 -0
- package/dist/store/sync/machinery.d.ts +27 -0
- package/dist/store/sync/machinery.d.ts.map +1 -0
- package/dist/store/sync/machinery.js +44 -0
- package/dist/store/sync/machinery.js.map +1 -0
- package/dist/store/sync/netting.d.ts +120 -0
- package/dist/store/sync/netting.d.ts.map +1 -0
- package/dist/store/sync/netting.js +336 -0
- package/dist/store/sync/netting.js.map +1 -0
- package/dist/store/sync/replica.d.ts +59 -3
- package/dist/store/sync/replica.d.ts.map +1 -1
- package/dist/store/sync/replica.js +84 -3
- package/dist/store/sync/replica.js.map +1 -1
- package/dist/store/sync/schema.d.ts +18 -3
- package/dist/store/sync/schema.d.ts.map +1 -1
- package/dist/store/sync/schema.js +27 -3
- package/dist/store/sync/schema.js.map +1 -1
- package/dist/store/sync/seal-backlog.d.ts +21 -0
- package/dist/store/sync/seal-backlog.d.ts.map +1 -0
- package/dist/store/sync/seal-backlog.js +24 -0
- package/dist/store/sync/seal-backlog.js.map +1 -0
- package/dist/store/sync/sealer-values.d.ts +34 -0
- package/dist/store/sync/sealer-values.d.ts.map +1 -0
- package/dist/store/sync/sealer-values.js +53 -0
- package/dist/store/sync/sealer-values.js.map +1 -0
- package/dist/store/sync/sealer.d.ts +145 -0
- package/dist/store/sync/sealer.d.ts.map +1 -0
- package/dist/store/sync/sealer.js +906 -0
- package/dist/store/sync/sealer.js.map +1 -0
- package/dist/store/sync/structural.d.ts +63 -0
- package/dist/store/sync/structural.d.ts.map +1 -0
- package/dist/store/sync/structural.js +180 -0
- package/dist/store/sync/structural.js.map +1 -0
- package/dist/store/sync/timestamps.d.ts +92 -0
- package/dist/store/sync/timestamps.d.ts.map +1 -0
- package/dist/store/sync/timestamps.js +228 -0
- package/dist/store/sync/timestamps.js.map +1 -0
- package/dist/store/sync/tombstones.d.ts +75 -0
- package/dist/store/sync/tombstones.d.ts.map +1 -0
- package/dist/store/sync/tombstones.js +103 -0
- package/dist/store/sync/tombstones.js.map +1 -0
- package/dist/store/sync/trigger-classes.d.ts +221 -0
- package/dist/store/sync/trigger-classes.d.ts.map +1 -0
- package/dist/store/sync/trigger-classes.js +441 -0
- package/dist/store/sync/trigger-classes.js.map +1 -0
- package/dist/store/sync/trigger-suspend-at-path.d.ts +23 -0
- package/dist/store/sync/trigger-suspend-at-path.d.ts.map +1 -0
- package/dist/store/sync/trigger-suspend-at-path.js +44 -0
- package/dist/store/sync/trigger-suspend-at-path.js.map +1 -0
- package/dist/store/sync/writer-version.d.ts +43 -0
- package/dist/store/sync/writer-version.d.ts.map +1 -0
- package/dist/store/sync/writer-version.js +90 -0
- package/dist/store/sync/writer-version.js.map +1 -0
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +102 -0
- package/dist/store/table-classification.js.map +1 -1
- package/dist/store/trigger-suspend-schema.d.ts +44 -0
- package/dist/store/trigger-suspend-schema.d.ts.map +1 -0
- package/dist/store/trigger-suspend-schema.js +25 -0
- package/dist/store/trigger-suspend-schema.js.map +1 -0
- package/dist/store/vault-manifest.d.ts +365 -0
- package/dist/store/vault-manifest.d.ts.map +1 -0
- package/dist/store/vault-manifest.js +812 -0
- package/dist/store/vault-manifest.js.map +1 -0
- package/dist/store/writer-lease.d.ts +31 -0
- package/dist/store/writer-lease.d.ts.map +1 -1
- package/dist/store/writer-lease.js +65 -2
- package/dist/store/writer-lease.js.map +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +21 -0
- package/dist/system/health.js.map +1 -1
- package/dist/system/project-health.d.ts.map +1 -1
- package/dist/system/project-health.js +3 -1
- package/dist/system/project-health.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +58 -0
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +75 -6
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/affected-scope.d.ts +150 -23
- package/dist/tasks/affected-scope.d.ts.map +1 -1
- package/dist/tasks/affected-scope.js +320 -32
- package/dist/tasks/affected-scope.js.map +1 -1
- package/dist/tasks/change-set.d.ts +96 -0
- package/dist/tasks/change-set.d.ts.map +1 -1
- package/dist/tasks/change-set.js +169 -4
- package/dist/tasks/change-set.js.map +1 -1
- package/dist/tasks/complete-ci.d.ts +115 -0
- package/dist/tasks/complete-ci.d.ts.map +1 -0
- package/dist/tasks/complete-ci.js +171 -0
- package/dist/tasks/complete-ci.js.map +1 -0
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +50 -17
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/compute-task-view.d.ts.map +1 -1
- package/dist/tasks/compute-task-view.js +7 -0
- package/dist/tasks/compute-task-view.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +51 -30
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/done-record.d.ts +6 -0
- package/dist/tasks/done-record.d.ts.map +1 -1
- package/dist/tasks/done-record.js +9 -3
- package/dist/tasks/done-record.js.map +1 -1
- package/dist/tasks/evidence.d.ts +3 -2
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +143 -12
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-notes.d.ts +22 -0
- package/dist/tasks/gate-notes.d.ts.map +1 -0
- package/dist/tasks/gate-notes.js +33 -0
- package/dist/tasks/gate-notes.js.map +1 -0
- package/dist/tasks/index.d.ts +2 -1
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +2 -1
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/test-run-binding.d.ts +117 -0
- package/dist/tasks/test-run-binding.d.ts.map +1 -0
- package/dist/tasks/test-run-binding.js +365 -0
- package/dist/tasks/test-run-binding.js.map +1 -0
- package/dist/tasks/tool-cache-env.d.ts +123 -0
- package/dist/tasks/tool-cache-env.d.ts.map +1 -0
- package/dist/tasks/tool-cache-env.js +287 -0
- package/dist/tasks/tool-cache-env.js.map +1 -0
- package/dist/tasks/tool-cache-failed-first.d.ts +155 -0
- package/dist/tasks/tool-cache-failed-first.d.ts.map +1 -0
- package/dist/tasks/tool-cache-failed-first.js +285 -0
- package/dist/tasks/tool-cache-failed-first.js.map +1 -0
- package/dist/tasks/tool-cache.d.ts +375 -55
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +912 -303
- package/dist/tasks/tool-cache.js.map +1 -1
- package/dist/tasks/tool-resolver.d.ts +4 -3
- package/dist/tasks/tool-resolver.d.ts.map +1 -1
- package/dist/tasks/tool-resolver.js +15 -4
- package/dist/tasks/tool-resolver.js.map +1 -1
- package/dist/tasks/tool-semaphore.d.ts +67 -7
- package/dist/tasks/tool-semaphore.d.ts.map +1 -1
- package/dist/tasks/tool-semaphore.js +134 -56
- package/dist/tasks/tool-semaphore.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +4 -0
- package/dist/upgrade.js.map +1 -1
- package/dist/validation/engine-ops.d.ts +6 -0
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +8 -0
- package/dist/validation/engine-ops.js.map +1 -1
- package/migrations/drizzle-cleo-project/20260930170000_t12819-trigger-suspend-clause/migration.sql +367 -0
- package/migrations/sync-journal/20260930120000_t12343-capture/migration.sql +47 -0
- package/migrations/sync-journal/20261001170000_t12984-sealer/migration.sql +101 -0
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +5 -5
- package/templates/CLEO-REFERENCE.md +8 -4
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sealer, S3a + S3b (journal spec §2.5 phase B, §2.4, §1.6, §2.6, §4.3;
|
|
3
|
+
* T12984, T12985).
|
|
4
|
+
*
|
|
5
|
+
* `sealPending` turns live `_sync_capture` rows into sealed transactions:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Group (§2.4).** A validated frame (its `_sync_frame` row exists) is
|
|
8
|
+
* one transaction. Every capture with no validated frame is a singleton
|
|
9
|
+
* transaction flagged `unframed`, provenance `foreign` (ruling (c)): the
|
|
10
|
+
* sealer never claims atomicity it did not observe. A batch ends on a
|
|
11
|
+
* group boundary. Apply and rebase frames wait for S5's intent subtraction.
|
|
12
|
+
* 2. **Draft ops (§2.6).** Each capture becomes one draft. Values are decoded
|
|
13
|
+
* from the capture's `enc()` text to typed wire values; references become
|
|
14
|
+
* uids (resolved from the local key when the capture saw none); secret
|
|
15
|
+
* columns and the local columns a stored ref uid is derived from (`ac_id`)
|
|
16
|
+
* are removed. Natural rows carry `k`, their key with references as uids.
|
|
17
|
+
* An unreadable capture is quarantined (`_sync_quarantine`, its table
|
|
18
|
+
* suspect) and never stalls the outbox (T13036).
|
|
19
|
+
* 3. **Net (§2.4, {@link netTransaction}).** Per (table, uid) inside the
|
|
20
|
+
* transaction. Across transactions, the dead incarnation of a row that
|
|
21
|
+
* never got a uid (its captures up to its delete) is dropped whole
|
|
22
|
+
* (T13036). A minted row with no uid or `birth_fp` keeps its group
|
|
23
|
+
* pending, and nothing after it seals first (§2.9).
|
|
24
|
+
* 4. **HLC.** `tick(at_ms)` per op in order, at its last capture's time; the
|
|
25
|
+
* transaction's HLC is its maximum.
|
|
26
|
+
* 5. **Write.** `_sync_txn`, `_sync_op`; `_sync_row_meta` upserted per op
|
|
27
|
+
* (version, origin, actor, per-field `fhlc`, tombstone on D, key on
|
|
28
|
+
* natural rows) and MOVED on K (hlc, fhlc, version and chash kept);
|
|
29
|
+
* `chash` recomputed from the live row once no live capture of that row
|
|
30
|
+
* remains; `_sync_ledger` per table; the `sealer.local_seq` counter.
|
|
31
|
+
* 6. **Consume.** Sealed, dropped and quarantined captures are deleted, and
|
|
32
|
+
* frames no live capture references.
|
|
33
|
+
*
|
|
34
|
+
* S3c: timestamp columns are canonical on the wire and in `chash`
|
|
35
|
+
* ({@link canonicalStoreTimestamp}; local rows are never rewritten here), and
|
|
36
|
+
* a delete in an append-only table leaves no per-row tombstone (§1.7 R5-5).
|
|
37
|
+
* The step-0 fill and the repair diff of suspect tables are S3d. `sync.seal`
|
|
38
|
+
* stays unreleased until those land: the sealer refuses a persisted flag too.
|
|
39
|
+
*
|
|
40
|
+
* Phase B is synchronous: one `BEGIN IMMEDIATE` … `COMMIT` with no await, so
|
|
41
|
+
* a caller on the accessor's transaction queue serializes it with every
|
|
42
|
+
* writer on the handle.
|
|
43
|
+
*
|
|
44
|
+
* @module store/sync/sealer
|
|
45
|
+
* @task T12984
|
|
46
|
+
* @epic T12323
|
|
47
|
+
*/
|
|
48
|
+
import { createHash } from 'node:crypto';
|
|
49
|
+
import { naturalRowUid } from '../row-identity.js';
|
|
50
|
+
import { BIRTH_FP_COLUMN, rowIdentitySpec, UID_COLUMN } from '../row-identity-registry.js';
|
|
51
|
+
import { captureTableDef, chunkedObject, enc, SECRET_MARKER, } from './capture.js';
|
|
52
|
+
import { tickClock, withImmediateTransaction } from './clock-store.js';
|
|
53
|
+
import { isSyncFlagOn, UNRELEASED_FLAGS } from './flags.js';
|
|
54
|
+
import { netTransaction } from './netting.js';
|
|
55
|
+
import { activeReplica } from './replica.js';
|
|
56
|
+
import { hasTable } from './schema.js';
|
|
57
|
+
import { canonicalJson, decodeEnc } from './sealer-values.js';
|
|
58
|
+
import { markSuspect } from './structural.js';
|
|
59
|
+
import { canonicalStoreTimestamp, timestampColumns } from './timestamps.js';
|
|
60
|
+
export { sealBacklog } from './seal-backlog.js';
|
|
61
|
+
export { canonicalJson, decodeEnc } from './sealer-values.js';
|
|
62
|
+
const emptyReport = (refused) => ({
|
|
63
|
+
txns: 0,
|
|
64
|
+
ops: 0,
|
|
65
|
+
captures: 0,
|
|
66
|
+
unframed: 0,
|
|
67
|
+
pending: [],
|
|
68
|
+
dropped: 0,
|
|
69
|
+
quarantined: [],
|
|
70
|
+
refused,
|
|
71
|
+
});
|
|
72
|
+
/** Ledger transaction kinds a frame kind maps to (§2.6). */
|
|
73
|
+
const TXN_KINDS = new Set(['write', 'remint', 'rekey', 'repair', 'exodus', 'import']);
|
|
74
|
+
const q = (id) => `"${id.replaceAll('"', '""')}"`;
|
|
75
|
+
/** The two identity columns an op carries as `u` / `bfp` instead of in its images. */
|
|
76
|
+
const OP_IDENTITY = new Set([UID_COLUMN, BIRTH_FP_COLUMN]);
|
|
77
|
+
/** A capture the sealer cannot read (a table left the sync set, a malformed image). */
|
|
78
|
+
class SealInputError extends Error {
|
|
79
|
+
}
|
|
80
|
+
class TableContext {
|
|
81
|
+
db;
|
|
82
|
+
scope;
|
|
83
|
+
defs = new Map();
|
|
84
|
+
stmts = new Map();
|
|
85
|
+
sources = new Map();
|
|
86
|
+
stamps = new Map();
|
|
87
|
+
constructor(db, scope) {
|
|
88
|
+
this.db = db;
|
|
89
|
+
this.scope = scope;
|
|
90
|
+
}
|
|
91
|
+
/** A statement prepared once per sealing pass (T13032). */
|
|
92
|
+
stmt(sql) {
|
|
93
|
+
let st = this.stmts.get(sql);
|
|
94
|
+
if (!st) {
|
|
95
|
+
st = this.db.prepare(sql);
|
|
96
|
+
this.stmts.set(sql, st);
|
|
97
|
+
}
|
|
98
|
+
return st;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The local columns a stored ref uid is derived from (`ac_id` for
|
|
102
|
+
* `ac_uid`): another replica's local ids, so they never travel or hash;
|
|
103
|
+
* the stored uid carries the reference (T13037).
|
|
104
|
+
*/
|
|
105
|
+
refSources(table) {
|
|
106
|
+
let out = this.sources.get(table);
|
|
107
|
+
if (!out) {
|
|
108
|
+
const spec = rowIdentitySpec(this.scope, table);
|
|
109
|
+
out = new Set((spec?.storedRefUids ?? []).map((r) => r.from));
|
|
110
|
+
this.sources.set(table, out);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** The table's key columns: a D-then-I U never carries them (§2.4). */
|
|
115
|
+
keyColumns(table) {
|
|
116
|
+
return rowIdentitySpec(this.scope, table)?.key ?? [];
|
|
117
|
+
}
|
|
118
|
+
/** The birth_fp row meta recorded for a uid (survives a delete or a re-key). */
|
|
119
|
+
metaBirthFp(table, uid) {
|
|
120
|
+
const row = this.stmt('SELECT bfp AS f FROM _sync_row_meta WHERE tbl = ? AND uid = ?').get(table, uid);
|
|
121
|
+
return row?.f ?? undefined;
|
|
122
|
+
}
|
|
123
|
+
/** The live row's uid by its local key (the capture's `rk`). */
|
|
124
|
+
uidByKey(table, rk) {
|
|
125
|
+
const def = this.def(table);
|
|
126
|
+
if (!def.identity.includes(UID_COLUMN))
|
|
127
|
+
return null;
|
|
128
|
+
const parts = JSON.parse(rk).map((p) => decodeEnc(p));
|
|
129
|
+
if (parts.some((v) => v !== null && typeof v === 'object'))
|
|
130
|
+
return null;
|
|
131
|
+
const where = def.key.map((k) => `${q(k)} = ?`).join(' AND ');
|
|
132
|
+
const row = this.stmt(`SELECT ${q(UID_COLUMN)} AS u FROM ${q(table)} WHERE ${where}`).get(...parts);
|
|
133
|
+
return row?.u ?? null;
|
|
134
|
+
}
|
|
135
|
+
/** A non-symmetric natural row's uid from its key (refs as uids), or null. */
|
|
136
|
+
naturalUid(table, key) {
|
|
137
|
+
const spec = rowIdentitySpec(this.scope, table);
|
|
138
|
+
if (!spec || spec.kind === 'minted' || spec.symmetric)
|
|
139
|
+
return null;
|
|
140
|
+
const parts = spec.key.map((c) => key[c]);
|
|
141
|
+
if (parts.some((v) => v === null || v === undefined || typeof v === 'object'))
|
|
142
|
+
return null;
|
|
143
|
+
return naturalRowUid(this.scope, table, parts);
|
|
144
|
+
}
|
|
145
|
+
/** The live row's birth_fp, so U ops of minted rows carry `bfp` (H5). */
|
|
146
|
+
liveBirthFp(table, uid) {
|
|
147
|
+
const row = this.stmt(`SELECT ${q(BIRTH_FP_COLUMN)} AS f FROM ${q(table)} WHERE ${q(UID_COLUMN)} = ?`).get(uid);
|
|
148
|
+
return row?.f ?? undefined;
|
|
149
|
+
}
|
|
150
|
+
def(table) {
|
|
151
|
+
let d = this.defs.get(table);
|
|
152
|
+
if (d === undefined) {
|
|
153
|
+
d = captureTableDef(this.db, this.scope, table) ?? null;
|
|
154
|
+
this.defs.set(table, d);
|
|
155
|
+
}
|
|
156
|
+
// §2.10: every op's table is in the sync set. A capture of any other table
|
|
157
|
+
// holds its group (reported), never aborts the batch (T13029).
|
|
158
|
+
if (d === null)
|
|
159
|
+
throw new SealInputError(`capture for ${table}, which is not in the sync set`);
|
|
160
|
+
return d;
|
|
161
|
+
}
|
|
162
|
+
minted(table) {
|
|
163
|
+
return rowIdentitySpec(this.scope, table)?.kind === 'minted';
|
|
164
|
+
}
|
|
165
|
+
/** The table's captured timestamp columns (§1.8). */
|
|
166
|
+
timestamps(table) {
|
|
167
|
+
let t = this.stamps.get(table);
|
|
168
|
+
if (!t) {
|
|
169
|
+
t = timestampColumns(this.scope, table);
|
|
170
|
+
this.stamps.set(table, t);
|
|
171
|
+
}
|
|
172
|
+
return t;
|
|
173
|
+
}
|
|
174
|
+
/** The uid of `table`'s row whose first key column is `key`. */
|
|
175
|
+
uidOf(table, keyColumn, key) {
|
|
176
|
+
if (key === null || typeof key === 'object')
|
|
177
|
+
return null;
|
|
178
|
+
const row = this.stmt(`SELECT ${q(UID_COLUMN)} AS u FROM ${q(table)} WHERE ${q(keyColumn)} = ?`).get(key);
|
|
179
|
+
return row?.u ?? null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The first live delete of (table, rk) after `seq`, if any (T13041). The
|
|
183
|
+
* live row under that local key is then a later incarnation, so it must
|
|
184
|
+
* not lend its uid to an earlier capture.
|
|
185
|
+
*/
|
|
186
|
+
laterDelete(table, rk, seq) {
|
|
187
|
+
return this.stmt("SELECT uid FROM _sync_capture WHERE state = 'live' AND tbl = ? AND rk = ? AND op = 'D' AND seq > ? ORDER BY seq LIMIT 1").get(table, rk, seq);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A timestamp column's wire value: canonical when the canonicalizer accepts
|
|
192
|
+
* it, else the raw text (`timestamp_ambiguous`, §1.8).
|
|
193
|
+
*/
|
|
194
|
+
function wireTimestamp(stamps, col, v) {
|
|
195
|
+
if (typeof v !== 'string' || !stamps.has(col))
|
|
196
|
+
return v;
|
|
197
|
+
return canonicalStoreTimestamp(v) ?? v;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A decoded column value: references become uids, secrets disappear,
|
|
201
|
+
* timestamps are canonical. Decoded by the value's shape (T13029): a
|
|
202
|
+
* `[local key, uid]` pair is a reference however the column is declared now,
|
|
203
|
+
* a string is an `enc()` value.
|
|
204
|
+
*/
|
|
205
|
+
function columnValue(ctx, def, col, raw) {
|
|
206
|
+
if (def.secret.has(col))
|
|
207
|
+
return undefined;
|
|
208
|
+
if (Array.isArray(raw)) {
|
|
209
|
+
const [localKey, uid] = raw;
|
|
210
|
+
if (typeof uid === 'string')
|
|
211
|
+
return uid;
|
|
212
|
+
const local = typeof localKey === 'string' ? decodeEnc(localKey) : null;
|
|
213
|
+
if (local === null)
|
|
214
|
+
return null;
|
|
215
|
+
const ref = def.refs.get(col);
|
|
216
|
+
return ref ? ctx.uidOf(ref.table, ref.key, local) : local;
|
|
217
|
+
}
|
|
218
|
+
if (typeof raw !== 'string')
|
|
219
|
+
throw new SealInputError(`${def.table}.${col}: unexpected image value`);
|
|
220
|
+
if (raw === SECRET_MARKER)
|
|
221
|
+
return undefined;
|
|
222
|
+
let v;
|
|
223
|
+
try {
|
|
224
|
+
v = decodeEnc(raw);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
throw new SealInputError(`${def.table}.${col}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
return wireTimestamp(ctx.timestamps(def.table), col, v);
|
|
230
|
+
}
|
|
231
|
+
function imageValues(ctx, def, img, skipIdentity) {
|
|
232
|
+
const out = {};
|
|
233
|
+
for (const [col, raw] of Object.entries(img)) {
|
|
234
|
+
// Only uid and birth_fp move to `u` / `bfp`; stored ref uids (ac_uid,
|
|
235
|
+
// ac_text_hash) are row content and stay in the image (T13030).
|
|
236
|
+
if (skipIdentity && OP_IDENTITY.has(col))
|
|
237
|
+
continue;
|
|
238
|
+
if (ctx.refSources(def.table).has(col))
|
|
239
|
+
continue;
|
|
240
|
+
const v = columnValue(ctx, def, col, raw);
|
|
241
|
+
if (v !== undefined)
|
|
242
|
+
out[col] = v;
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/** Natural rows: the key, with references as uids, from the capture's `rk`. */
|
|
247
|
+
function naturalKey(ctx, def, rk) {
|
|
248
|
+
const parts = JSON.parse(rk);
|
|
249
|
+
const out = {};
|
|
250
|
+
def.key.forEach((col, i) => {
|
|
251
|
+
const local = decodeEnc(parts[i]);
|
|
252
|
+
const ref = def.refs.get(col);
|
|
253
|
+
out[col] = ref ? ctx.uidOf(ref.table, ref.key, local) : local;
|
|
254
|
+
});
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
function str(v) {
|
|
258
|
+
return typeof v === 'string' ? v : undefined;
|
|
259
|
+
}
|
|
260
|
+
const rowKey = (t, u) => `${t}\u0000${u}`;
|
|
261
|
+
/**
|
|
262
|
+
* The birth_fp every capture of the batch records: I and D images, and both
|
|
263
|
+
* sides of a K. A later op on a row that was since deleted or re-keyed then
|
|
264
|
+
* still carries `bfp` (T13030). Unreadable captures are skipped here; their
|
|
265
|
+
* group reports them.
|
|
266
|
+
*/
|
|
267
|
+
function batchBirths(batch) {
|
|
268
|
+
const out = new Map();
|
|
269
|
+
for (const c of batch) {
|
|
270
|
+
try {
|
|
271
|
+
const img = JSON.parse(c.img);
|
|
272
|
+
if (c.op === 'K') {
|
|
273
|
+
const u = img[UID_COLUMN];
|
|
274
|
+
const f = img[BIRTH_FP_COLUMN];
|
|
275
|
+
if (f) {
|
|
276
|
+
// A K that changes only birth_fp keeps its uid (the capture's).
|
|
277
|
+
const [ou, nu] = u
|
|
278
|
+
? [str(decodeEnc(u[0])), str(decodeEnc(u[1]))]
|
|
279
|
+
: [c.uid ?? undefined, c.uid ?? undefined];
|
|
280
|
+
const [of, nf] = [str(decodeEnc(f[0])), str(decodeEnc(f[1]))];
|
|
281
|
+
if (ou && of && ou !== nu)
|
|
282
|
+
out.set(rowKey(c.tbl, ou), of);
|
|
283
|
+
if (nu && nf)
|
|
284
|
+
out.set(rowKey(c.tbl, nu), nf);
|
|
285
|
+
}
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const uid = c.uid ??
|
|
289
|
+
(typeof img[UID_COLUMN] === 'string'
|
|
290
|
+
? str(decodeEnc(img[UID_COLUMN]))
|
|
291
|
+
: undefined);
|
|
292
|
+
const bfp = typeof img[BIRTH_FP_COLUMN] === 'string'
|
|
293
|
+
? str(decodeEnc(img[BIRTH_FP_COLUMN]))
|
|
294
|
+
: undefined;
|
|
295
|
+
if (uid && bfp)
|
|
296
|
+
out.set(rowKey(c.tbl, uid), bfp);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// reported by the capture's group
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* A minted row's birth_fp when its own image lacks it (T13029, T13030): the
|
|
306
|
+
* batch first, then row meta (kept across a delete or re-key), then the live
|
|
307
|
+
* row.
|
|
308
|
+
*/
|
|
309
|
+
function resolveBirthFp(ctx, births, table, uid) {
|
|
310
|
+
if (uid === null)
|
|
311
|
+
return undefined;
|
|
312
|
+
return (births.get(rowKey(table, uid)) ?? ctx.metaBirthFp(table, uid) ?? ctx.liveBirthFp(table, uid));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Build one draft op from a capture (S3b: netting runs on drafts). The uid
|
|
316
|
+
* may stay NULL here; the netted op is checked afterwards, so a clear-and-
|
|
317
|
+
* refill pair (K x → NULL, K NULL → y) can net to one K first (N8).
|
|
318
|
+
*/
|
|
319
|
+
function buildDraft(ctx, c, births) {
|
|
320
|
+
const def = ctx.def(c.tbl);
|
|
321
|
+
const img = JSON.parse(c.img);
|
|
322
|
+
const minted = ctx.minted(c.tbl);
|
|
323
|
+
const natural = !minted ? { k: naturalKey(ctx, def, c.rk) } : {};
|
|
324
|
+
const base = { t: c.tbl, rk: c.rk, seq: c.seq };
|
|
325
|
+
if (c.op === 'K') {
|
|
326
|
+
const pair = (col) => {
|
|
327
|
+
const p = img[col];
|
|
328
|
+
return p ? [str(decodeEnc(p[0])), str(decodeEnc(p[1]))] : [undefined, undefined];
|
|
329
|
+
};
|
|
330
|
+
const [, nu] = pair(UID_COLUMN);
|
|
331
|
+
const [obfp, bfp] = pair(BIRTH_FP_COLUMN);
|
|
332
|
+
return {
|
|
333
|
+
...base,
|
|
334
|
+
o: 'K',
|
|
335
|
+
u: c.uid,
|
|
336
|
+
nu: nu ?? null,
|
|
337
|
+
...(bfp ? { bfp } : {}),
|
|
338
|
+
...(obfp ? { obfp } : {}),
|
|
339
|
+
...natural,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
let uid = c.uid;
|
|
343
|
+
if (uid === null && def.identity.includes(UID_COLUMN)) {
|
|
344
|
+
const raw = img[UID_COLUMN];
|
|
345
|
+
if (typeof raw === 'string')
|
|
346
|
+
uid = str(decodeEnc(raw)) ?? null;
|
|
347
|
+
}
|
|
348
|
+
// Resolve a missing uid from the live row by its local key (§2.5 step 3),
|
|
349
|
+
// unless a later capture deletes that local row: the live row is then a
|
|
350
|
+
// later incarnation, and this one is resolved by the netting or dropped as
|
|
351
|
+
// dead (T13041).
|
|
352
|
+
if (uid === null && !ctx.laterDelete(c.tbl, c.rk, c.seq))
|
|
353
|
+
uid = ctx.uidByKey(c.tbl, c.rk);
|
|
354
|
+
// A natural row's uid is a function of its key with references as uids
|
|
355
|
+
// (T12341 §5.3), so a capture taken before the fill still seals. Symmetric
|
|
356
|
+
// edges need the fill's twin rule and wait for it (the step-0 fill, S3d).
|
|
357
|
+
if (uid === null && !minted && natural.k)
|
|
358
|
+
uid = ctx.naturalUid(c.tbl, natural.k);
|
|
359
|
+
switch (c.op) {
|
|
360
|
+
case 'I': {
|
|
361
|
+
// The uid may have come from the live row (the capture predates the
|
|
362
|
+
// fill): take birth_fp the same way, so the I never waits forever (T13029).
|
|
363
|
+
const bfp = str(columnValue(ctx, def, BIRTH_FP_COLUMN, img[BIRTH_FP_COLUMN] ?? 'NULL')) ??
|
|
364
|
+
(minted ? resolveBirthFp(ctx, births, c.tbl, uid) : undefined);
|
|
365
|
+
return {
|
|
366
|
+
...base,
|
|
367
|
+
o: 'I',
|
|
368
|
+
u: uid,
|
|
369
|
+
...(bfp ? { bfp } : {}),
|
|
370
|
+
...natural,
|
|
371
|
+
a: imageValues(ctx, def, img, true),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
case 'U': {
|
|
375
|
+
const a = {};
|
|
376
|
+
const b = {};
|
|
377
|
+
for (const [col, pair] of Object.entries(img)) {
|
|
378
|
+
if (ctx.refSources(def.table).has(col))
|
|
379
|
+
continue;
|
|
380
|
+
const [before, after] = pair;
|
|
381
|
+
const nv = columnValue(ctx, def, col, after);
|
|
382
|
+
const ov = columnValue(ctx, def, col, before);
|
|
383
|
+
if (nv !== undefined)
|
|
384
|
+
a[col] = nv;
|
|
385
|
+
if (ov !== undefined)
|
|
386
|
+
b[col] = ov;
|
|
387
|
+
}
|
|
388
|
+
const bfp = minted ? resolveBirthFp(ctx, births, c.tbl, uid) : undefined;
|
|
389
|
+
return { ...base, o: 'U', u: uid, ...(bfp ? { bfp } : {}), ...natural, a, b };
|
|
390
|
+
}
|
|
391
|
+
case 'D': {
|
|
392
|
+
const bfp = str(columnValue(ctx, def, BIRTH_FP_COLUMN, img[BIRTH_FP_COLUMN] ?? 'NULL')) ??
|
|
393
|
+
(minted ? resolveBirthFp(ctx, births, c.tbl, uid) : undefined);
|
|
394
|
+
return {
|
|
395
|
+
...base,
|
|
396
|
+
o: 'D',
|
|
397
|
+
u: uid,
|
|
398
|
+
...(bfp ? { bfp } : {}),
|
|
399
|
+
...natural,
|
|
400
|
+
b: imageValues(ctx, def, img, true),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/** Why a netted op cannot be sealed yet, or null. */
|
|
406
|
+
function unsealable(ctx, op) {
|
|
407
|
+
if (op.u === null)
|
|
408
|
+
return `${op.t} seq ${op.seq}: row has no uid`;
|
|
409
|
+
if (op.o === 'K' && (op.nu ?? null) === null)
|
|
410
|
+
return `${op.t} seq ${op.seq}: re-key to a NULL uid`;
|
|
411
|
+
if (op.o === 'I' &&
|
|
412
|
+
ctx.minted(op.t) &&
|
|
413
|
+
ctx.def(op.t).identity.includes(BIRTH_FP_COLUMN) &&
|
|
414
|
+
op.bfp === undefined) {
|
|
415
|
+
return `${op.t} seq ${op.seq}: minted row has no birth_fp`;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* sha256 of a row's canonical WIRE image (§2.7, M6; T13031): the values an I
|
|
421
|
+
* op of the row would carry, so every replica hashes identical bytes. Secret
|
|
422
|
+
* columns, uid and birth_fp, and a minted table's local key (a display id or
|
|
423
|
+
* an autoincrement id, which differ per replica) are left out; references are
|
|
424
|
+
* their uids; NULL columns are omitted, as in an I image; and timestamp
|
|
425
|
+
* columns hash their canonical form (§1.8), so a replica holding legacy text
|
|
426
|
+
* hashes like one that received the canonical value.
|
|
427
|
+
*/
|
|
428
|
+
export function rowChash(db, scope, def, uid) {
|
|
429
|
+
return chashOf(new TableContext(db, scope), def, uid);
|
|
430
|
+
}
|
|
431
|
+
function chashOf(ctx, def, uid) {
|
|
432
|
+
const spec = rowIdentitySpec(ctx.scope, def.table);
|
|
433
|
+
const localKey = new Set(spec?.kind === 'minted' ? spec.key : []);
|
|
434
|
+
const sources = ctx.refSources(def.table);
|
|
435
|
+
const cols = def.columns.filter((c) => !def.secret.has(c) && !OP_IDENTITY.has(c) && !localKey.has(c) && !sources.has(c));
|
|
436
|
+
if (cols.length === 0)
|
|
437
|
+
return null;
|
|
438
|
+
const stamps = ctx.timestamps(def.table);
|
|
439
|
+
const row = ctx
|
|
440
|
+
.stmt(`SELECT ${chunkedObject(cols.map((c) => [c, enc(q(c))]))} AS img FROM ${q(def.table)} WHERE ${q(UID_COLUMN)} = ?`)
|
|
441
|
+
.get(uid);
|
|
442
|
+
if (!row)
|
|
443
|
+
return null;
|
|
444
|
+
const wire = {};
|
|
445
|
+
for (const [col, raw] of Object.entries(JSON.parse(row.img))) {
|
|
446
|
+
const local = decodeEnc(raw);
|
|
447
|
+
const ref = def.refs.get(col);
|
|
448
|
+
const v = ref && local !== null
|
|
449
|
+
? ctx.uidOf(ref.table, ref.key, local)
|
|
450
|
+
: wireTimestamp(stamps, col, local);
|
|
451
|
+
if (v !== null)
|
|
452
|
+
wire[col] = v;
|
|
453
|
+
}
|
|
454
|
+
return createHash('sha256').update(canonicalJson(wire)).digest('hex');
|
|
455
|
+
}
|
|
456
|
+
function nextFhlc(prev, def, changed, h) {
|
|
457
|
+
if (!prev)
|
|
458
|
+
return null;
|
|
459
|
+
const old = prev.fhlc ? JSON.parse(prev.fhlc) : {};
|
|
460
|
+
const out = {};
|
|
461
|
+
for (const col of def.columns) {
|
|
462
|
+
if (def.identity.includes(col) || changed.includes(col))
|
|
463
|
+
continue;
|
|
464
|
+
const at = old[col] ?? prev.hlc;
|
|
465
|
+
if (at < h)
|
|
466
|
+
out[col] = at;
|
|
467
|
+
}
|
|
468
|
+
return Object.keys(out).length > 0 ? canonicalJson(out) : null;
|
|
469
|
+
}
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// sealPending
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
/**
|
|
474
|
+
* Why the sealer refuses to run, or `null` when it may. A persisted
|
|
475
|
+
* `sync.seal` is refused while the flag is unreleased (T13037), whatever
|
|
476
|
+
* wrote it.
|
|
477
|
+
*/
|
|
478
|
+
export function sealPreconditions(db, env, allowUnreleased = false) {
|
|
479
|
+
if (!hasTable(db, '_sync_capture') || !hasTable(db, '_sync_txn')) {
|
|
480
|
+
return 'sync schema not installed';
|
|
481
|
+
}
|
|
482
|
+
if (!isSyncFlagOn(db, 'sync.seal', env))
|
|
483
|
+
return 'sync.seal is off';
|
|
484
|
+
if (UNRELEASED_FLAGS.has('sync.seal') && !allowUnreleased) {
|
|
485
|
+
return 'sync.seal is unreleased until S3b–S3d land (T13032)';
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
/** Where the sealer's transaction counter is persisted (T13033). */
|
|
490
|
+
export const SEAL_COUNTER_KEY = 'sealer.local_seq';
|
|
491
|
+
/**
|
|
492
|
+
* Seal up to `budget` live captures (ending on a group boundary) into
|
|
493
|
+
* transactions, in one synchronous `BEGIN IMMEDIATE` transaction.
|
|
494
|
+
*
|
|
495
|
+
* Groups seal strictly in capture order (§2.9): the first group that cannot
|
|
496
|
+
* seal yet (a row without uid or birth_fp, an unreadable capture) stops the
|
|
497
|
+
* batch, and is reported in `pending` until it can. Nothing after it gets an
|
|
498
|
+
* HLC first.
|
|
499
|
+
*
|
|
500
|
+
* @returns what was sealed, what stays pending, or why nothing ran.
|
|
501
|
+
*/
|
|
502
|
+
export function sealPending(db, opts) {
|
|
503
|
+
const refused = sealPreconditions(db, opts.env ?? process.env, opts.allowUnreleased === true);
|
|
504
|
+
if (refused)
|
|
505
|
+
return emptyReport(refused);
|
|
506
|
+
const replica = opts.replica ?? activeReplica(db, opts.scope)?.replicaId;
|
|
507
|
+
if (!replica)
|
|
508
|
+
return emptyReport('no bound replica');
|
|
509
|
+
const now = opts.now ?? Date.now;
|
|
510
|
+
const budget = Math.max(1, opts.budget ?? 5000);
|
|
511
|
+
return withImmediateTransaction(db, () => sealInTransaction(db, { ...opts, replica }, budget, now));
|
|
512
|
+
}
|
|
513
|
+
const CAPTURE_COLS = 'seq, tbl, op, rk, uid, img, at_ms, frame';
|
|
514
|
+
/** The uid pair a K capture's image records, decoded; null when unreadable. */
|
|
515
|
+
function kUids(img) {
|
|
516
|
+
try {
|
|
517
|
+
const pair = JSON.parse(img)[UID_COLUMN];
|
|
518
|
+
if (!Array.isArray(pair) || typeof pair[0] !== 'string' || typeof pair[1] !== 'string') {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
return [str(decodeEnc(pair[0])) ?? null, str(decodeEnc(pair[1])) ?? null];
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* A clear K(x → NULL) and what follows it on the same local row (T13041).
|
|
529
|
+
* Until the row's next delete or its refill K(NULL → y), the row is still x:
|
|
530
|
+
* those captures are pointed at x (in `_sync_capture`, so a later pass sees
|
|
531
|
+
* it too) and the clear is consumed. A cleared-then-deleted row then seals as
|
|
532
|
+
* D(x), and a clear and refill in different transactions as K(x → y) (N8).
|
|
533
|
+
* With neither yet, the clear waits.
|
|
534
|
+
*
|
|
535
|
+
* @returns x and the seqs pointed at it, or null when `c` is not a clear or
|
|
536
|
+
* must wait.
|
|
537
|
+
*/
|
|
538
|
+
function resolveClear(ctx, c) {
|
|
539
|
+
if (c.op !== 'K' || c.uid === null)
|
|
540
|
+
return null;
|
|
541
|
+
const pair = kUids(c.img);
|
|
542
|
+
if (pair === null || pair[1] !== null)
|
|
543
|
+
return null;
|
|
544
|
+
const end = ctx
|
|
545
|
+
.stmt("SELECT seq, op, img FROM _sync_capture WHERE state = 'live' AND tbl = ? AND rk = ? AND seq > ? AND op IN ('D', 'K') ORDER BY seq LIMIT 1")
|
|
546
|
+
.get(c.tbl, c.rk, c.seq);
|
|
547
|
+
if (!end)
|
|
548
|
+
return null;
|
|
549
|
+
// Only a refill (a K from NULL) continues the cleared row.
|
|
550
|
+
if (end.op === 'K' && kUids(end.img)?.[0] !== null)
|
|
551
|
+
return null;
|
|
552
|
+
const seqs = ctx
|
|
553
|
+
.stmt("SELECT seq FROM _sync_capture WHERE state = 'live' AND tbl = ? AND rk = ? AND seq > ? AND seq <= ? AND uid IS NULL ORDER BY seq")
|
|
554
|
+
.all(c.tbl, c.rk, c.seq, end.seq).map((r) => r.seq);
|
|
555
|
+
ctx
|
|
556
|
+
.stmt("UPDATE _sync_capture SET uid = ? WHERE state = 'live' AND tbl = ? AND rk = ? AND seq > ? AND seq <= ? AND uid IS NULL")
|
|
557
|
+
.run(c.uid, c.tbl, c.rk, c.seq, end.seq);
|
|
558
|
+
return { uid: c.uid, seqs };
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* The seqs of the dead incarnation that starts at draft `d` (T13036), or
|
|
562
|
+
* null. `d` has no uid; its row's live captures from `d` on run to a delete
|
|
563
|
+
* of the same local row; none of them names a uid row meta knows as live;
|
|
564
|
+
* and no earlier capture of this incarnation (back to the previous delete of
|
|
565
|
+
* the local row) names a uid some sealed op carried (T13041). Otherwise some
|
|
566
|
+
* replica knew the row, and it waits.
|
|
567
|
+
*/
|
|
568
|
+
function deadIncarnation(ctx, facts, d) {
|
|
569
|
+
const earlier = ctx
|
|
570
|
+
.stmt("SELECT op, uid, img FROM _sync_capture WHERE state = 'live' AND tbl = ? AND rk = ? AND seq < ? ORDER BY seq DESC")
|
|
571
|
+
.all(d.t, d.rk, d.seq);
|
|
572
|
+
for (const r of earlier) {
|
|
573
|
+
if (r.op === 'D')
|
|
574
|
+
break;
|
|
575
|
+
const named = r.op === 'K' ? [r.uid, ...(kUids(r.img) ?? [])] : [r.uid];
|
|
576
|
+
if (named.some((u) => u !== null && u !== undefined && facts.known(d.t, u)))
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const rows = ctx
|
|
580
|
+
.stmt("SELECT seq, op, uid, img FROM _sync_capture WHERE state = 'live' AND tbl = ? AND rk = ? AND seq >= ? ORDER BY seq")
|
|
581
|
+
.all(d.t, d.rk, d.seq);
|
|
582
|
+
const out = [];
|
|
583
|
+
for (const r of rows) {
|
|
584
|
+
out.push(r.seq);
|
|
585
|
+
const uids = [r.uid];
|
|
586
|
+
if (r.op === 'K') {
|
|
587
|
+
try {
|
|
588
|
+
const pair = JSON.parse(r.img)[UID_COLUMN];
|
|
589
|
+
if (Array.isArray(pair) && typeof pair[1] === 'string')
|
|
590
|
+
uids.push(str(decodeEnc(pair[1])) ?? null);
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (uids.some((u) => u !== null && u !== undefined && facts.live(d.t, u)))
|
|
597
|
+
return null;
|
|
598
|
+
if (r.op === 'D')
|
|
599
|
+
return out;
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* The net row-count effect of a table's live captures, for the ledger's
|
|
605
|
+
* first sight (§4.3; T13037), mirroring how they will seal: per local row,
|
|
606
|
+
* `(exists after the last capture) − (existed before the first)`. A first I
|
|
607
|
+
* counts as a new row unless its uid is one row meta knows live: that is a
|
|
608
|
+
* foreign REPLACE, which seals as a U and adds nothing.
|
|
609
|
+
*/
|
|
610
|
+
function waitingEffect(db, tbl, facts) {
|
|
611
|
+
const rows = db
|
|
612
|
+
.prepare("SELECT rk, op, uid FROM _sync_capture WHERE tbl = ? AND state = 'live' ORDER BY seq")
|
|
613
|
+
.all(tbl);
|
|
614
|
+
const span = new Map();
|
|
615
|
+
for (const r of rows) {
|
|
616
|
+
const s = span.get(r.rk);
|
|
617
|
+
if (s)
|
|
618
|
+
s.last = r;
|
|
619
|
+
else
|
|
620
|
+
span.set(r.rk, { first: r, last: r });
|
|
621
|
+
}
|
|
622
|
+
let n = 0;
|
|
623
|
+
for (const { first, last } of span.values()) {
|
|
624
|
+
const before = first.op === 'I' && !(first.uid !== null && facts.live(tbl, first.uid)) ? 0 : 1;
|
|
625
|
+
const after = last.op === 'D' ? 0 : 1;
|
|
626
|
+
n += after - before;
|
|
627
|
+
}
|
|
628
|
+
return n;
|
|
629
|
+
}
|
|
630
|
+
function sealInTransaction(db, opts, budget, now) {
|
|
631
|
+
const ctx = new TableContext(db, opts.scope);
|
|
632
|
+
const started = performance.now();
|
|
633
|
+
const maxMs = opts.maxMs ?? 200;
|
|
634
|
+
const replica = opts.replica;
|
|
635
|
+
// 1. Read a batch. A frame cut by the budget is completed only when it is
|
|
636
|
+
// the batch's first group (it must seal whole); otherwise it is left for
|
|
637
|
+
// the next batch, so the extension stays bounded (T13032).
|
|
638
|
+
let batch = db
|
|
639
|
+
.prepare(`SELECT ${CAPTURE_COLS} FROM _sync_capture WHERE state = 'live' ORDER BY seq LIMIT ?`)
|
|
640
|
+
.all(budget);
|
|
641
|
+
if (batch.length === 0)
|
|
642
|
+
return emptyReport(null);
|
|
643
|
+
const lastFrame = batch[batch.length - 1]?.frame ?? null;
|
|
644
|
+
if (lastFrame !== null && batch.length === budget) {
|
|
645
|
+
if (batch[0]?.frame === lastFrame) {
|
|
646
|
+
const maxSeq = batch[batch.length - 1]?.seq ?? 0;
|
|
647
|
+
batch.push(...db
|
|
648
|
+
.prepare(`SELECT ${CAPTURE_COLS} FROM _sync_capture WHERE state = 'live' AND frame = ? AND seq > ? ORDER BY seq`)
|
|
649
|
+
.all(lastFrame, maxSeq));
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
batch = batch.filter((c) => c.frame !== lastFrame);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const births = batchBirths(batch);
|
|
656
|
+
// 2. Group: a validated frame is one transaction; anything else a singleton.
|
|
657
|
+
const frames = new Map();
|
|
658
|
+
for (const f of db.prepare('SELECT frame, kind, actor FROM _sync_frame').all()) {
|
|
659
|
+
frames.set(f.frame, { kind: f.kind, actor: f.actor });
|
|
660
|
+
}
|
|
661
|
+
const groups = [];
|
|
662
|
+
const byFrame = new Map();
|
|
663
|
+
for (const c of batch) {
|
|
664
|
+
const f = c.frame !== null ? frames.get(c.frame) : undefined;
|
|
665
|
+
if (c.frame !== null && f) {
|
|
666
|
+
let g = byFrame.get(c.frame);
|
|
667
|
+
if (!g) {
|
|
668
|
+
g = { frame: c.frame, kind: f.kind, actor: f.actor, captures: [] };
|
|
669
|
+
byFrame.set(c.frame, g);
|
|
670
|
+
groups.push(g);
|
|
671
|
+
}
|
|
672
|
+
g.captures.push(c);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
groups.push({ frame: null, kind: 'write', actor: null, captures: [c] });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// 3–5. Seal each group, in order.
|
|
679
|
+
const meta = {
|
|
680
|
+
get: db.prepare('SELECT hlc, fhlc, version, deleted, key_json, chash, bfp FROM _sync_row_meta WHERE tbl = ? AND uid = ?'),
|
|
681
|
+
upsert: db.prepare(`INSERT INTO _sync_row_meta (tbl, uid, hlc, fhlc, origin, actor, version, deleted, key_json, chash, bfp)
|
|
682
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
683
|
+
ON CONFLICT (tbl, uid) DO UPDATE SET hlc = excluded.hlc, fhlc = excluded.fhlc,
|
|
684
|
+
origin = excluded.origin, actor = excluded.actor, version = excluded.version,
|
|
685
|
+
deleted = excluded.deleted, key_json = coalesce(excluded.key_json, key_json),
|
|
686
|
+
chash = excluded.chash, bfp = coalesce(excluded.bfp, bfp)`),
|
|
687
|
+
remove: db.prepare('DELETE FROM _sync_row_meta WHERE tbl = ? AND uid = ?'),
|
|
688
|
+
// A re-key moves the row's meta and keeps hlc, fhlc, version and chash
|
|
689
|
+
// (§2.5 step 5, T13031); only the key and birth_fp follow the new uid.
|
|
690
|
+
move: db.prepare(`UPDATE _sync_row_meta SET uid = ?, key_json = coalesce(?, key_json), bfp = coalesce(?, bfp)
|
|
691
|
+
WHERE tbl = ? AND uid = ?`),
|
|
692
|
+
flags: db.prepare('SELECT sent, deleted FROM _sync_row_meta WHERE tbl = ? AND uid = ?'),
|
|
693
|
+
};
|
|
694
|
+
const insTxn = db.prepare(`INSERT INTO _sync_txn (txn, local_seq, replica, hlc, scope, via, kind, actor, frame, unframed, partial, op_count, sealed_at_ms)
|
|
695
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
696
|
+
const insOp = db.prepare('INSERT INTO _sync_op (txn, idx, tbl, uid, o, hlc, body) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
697
|
+
// The counter only rises: a txn id is never reused after old rows are
|
|
698
|
+
// collected (T13033).
|
|
699
|
+
const stored = db.prepare('SELECT value FROM _sync_meta WHERE key = ?').get(SEAL_COUNTER_KEY);
|
|
700
|
+
const maxRow = db.prepare('SELECT coalesce(max(local_seq), 0) AS n FROM _sync_txn').get().n;
|
|
701
|
+
let localSeq = Math.max(Number(stored?.value ?? 0) || 0, maxRow);
|
|
702
|
+
const firstSeq = localSeq;
|
|
703
|
+
let txns = 0;
|
|
704
|
+
let ops = 0;
|
|
705
|
+
let unframed = 0;
|
|
706
|
+
const consumed = new Set();
|
|
707
|
+
const pending = [];
|
|
708
|
+
const touched = new Map();
|
|
709
|
+
const ledgerDelta = new Map();
|
|
710
|
+
const metaFacts = {
|
|
711
|
+
sent: (t, u) => meta.flags.get(t, u)?.sent === 1,
|
|
712
|
+
live: (t, u) => meta.flags.get(t, u)?.deleted === 0,
|
|
713
|
+
known: (t, u) => meta.flags.get(t, u) !== undefined,
|
|
714
|
+
};
|
|
715
|
+
const quarantine = db.prepare(`INSERT INTO _sync_quarantine
|
|
716
|
+
(seq, tbl, op, rk, uid, img, at_ms, frame, reason, quarantined_at_ms)
|
|
717
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (seq) DO NOTHING`);
|
|
718
|
+
const dead = new Set();
|
|
719
|
+
// Captures pointed at a cleared row's uid in this pass (T13041).
|
|
720
|
+
const pointedAt = new Map();
|
|
721
|
+
let dropped = 0;
|
|
722
|
+
const quarantined = [];
|
|
723
|
+
for (const g of groups) {
|
|
724
|
+
// Bound the time this transaction holds the write lock (T13032).
|
|
725
|
+
if (txns > 0 && performance.now() - started > maxMs)
|
|
726
|
+
break;
|
|
727
|
+
// T13041: a clear whose row is deleted or refilled later is consumed, and
|
|
728
|
+
// what follows it on the row is pointed at the cleared uid.
|
|
729
|
+
for (const c of g.captures) {
|
|
730
|
+
if (dead.has(c.seq))
|
|
731
|
+
continue;
|
|
732
|
+
const cleared = resolveClear(ctx, c);
|
|
733
|
+
if (cleared === null)
|
|
734
|
+
continue;
|
|
735
|
+
for (const seq of cleared.seqs)
|
|
736
|
+
pointedAt.set(seq, cleared.uid);
|
|
737
|
+
dead.add(c.seq);
|
|
738
|
+
consumed.add(c.seq);
|
|
739
|
+
}
|
|
740
|
+
const captures = g.captures
|
|
741
|
+
.filter((c) => !dead.has(c.seq))
|
|
742
|
+
.map((c) => (pointedAt.has(c.seq) ? { ...c, uid: pointedAt.get(c.seq) ?? null } : c));
|
|
743
|
+
if (captures.length === 0)
|
|
744
|
+
continue;
|
|
745
|
+
const head = captures[0]?.seq ?? 0;
|
|
746
|
+
if (g.frame !== null && (g.kind === 'apply' || g.kind === 'rebase')) {
|
|
747
|
+
// §3.3: an apply or rebase frame is sealed only after its apply intents
|
|
748
|
+
// are subtracted, which is S5. Until then it waits (T13037).
|
|
749
|
+
pending.push({ firstSeq: head, reason: `${g.kind} frames wait for S5 intent subtraction` });
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
const capOf = new Map(captures.map((c) => [c.seq, c]));
|
|
753
|
+
let drafts = [];
|
|
754
|
+
let partial = false;
|
|
755
|
+
for (const c of captures) {
|
|
756
|
+
try {
|
|
757
|
+
drafts.push(buildDraft(ctx, c, births));
|
|
758
|
+
}
|
|
759
|
+
catch (err) {
|
|
760
|
+
if (!(err instanceof SealInputError))
|
|
761
|
+
throw err;
|
|
762
|
+
// T13036: an unreadable capture never stalls the outbox. It moves to
|
|
763
|
+
// _sync_quarantine, which marks its table suspect for S3d's repair
|
|
764
|
+
// diff, and the rest of the group seals.
|
|
765
|
+
quarantine.run(c.seq, c.tbl, c.op, c.rk, c.uid, c.img, c.at_ms, c.frame, err.message, now());
|
|
766
|
+
markSuspect(db, opts.scope, [c.tbl]);
|
|
767
|
+
quarantined.push({ seq: c.seq, tbl: c.tbl, reason: err.message });
|
|
768
|
+
consumed.add(c.seq);
|
|
769
|
+
partial = true;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// T13036: a row that never had a uid and is gone again is a dead
|
|
773
|
+
// incarnation no replica ever knew (§2.4: an I and a later D of a uid
|
|
774
|
+
// that never left the device drop both). Its captures, up to its delete,
|
|
775
|
+
// are dropped wherever they sit, so it cannot hold the outbox forever.
|
|
776
|
+
for (const d of drafts) {
|
|
777
|
+
if (d.u !== null || d.o === 'K' || dead.has(d.seq))
|
|
778
|
+
continue;
|
|
779
|
+
for (const seq of deadIncarnation(ctx, metaFacts, d) ?? []) {
|
|
780
|
+
if (dead.has(seq))
|
|
781
|
+
continue;
|
|
782
|
+
dead.add(seq);
|
|
783
|
+
consumed.add(seq);
|
|
784
|
+
dropped += 1;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
drafts = drafts.filter((d) => !dead.has(d.seq));
|
|
788
|
+
const netted = netTransaction(drafts, metaFacts, { keyColumns: (t) => ctx.keyColumns(t) });
|
|
789
|
+
const wait = netted.ops.map((op) => unsealable(ctx, op)).find((r) => r !== null) ?? null;
|
|
790
|
+
if (wait !== null) {
|
|
791
|
+
// §2.9: stop here. Nothing after a waiting group seals before it.
|
|
792
|
+
pending.push({ firstSeq: head, reason: wait });
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
// Dropped re-keys (no sealed op carried the old uid): the meta follows the row.
|
|
796
|
+
for (const r of netted.renames) {
|
|
797
|
+
meta.remove.run(r.t, r.to);
|
|
798
|
+
meta.move.run(r.to, null, null, r.t, r.from);
|
|
799
|
+
}
|
|
800
|
+
for (const c of captures)
|
|
801
|
+
consumed.add(c.seq);
|
|
802
|
+
if (netted.ops.length === 0)
|
|
803
|
+
continue; // everything netted away
|
|
804
|
+
localSeq += 1;
|
|
805
|
+
const txn = `${replica}:${localSeq}`;
|
|
806
|
+
// An op's time and local key are its LAST capture's (T13037).
|
|
807
|
+
const at = (op) => capOf.get(op.last) ?? capOf.get(op.seq);
|
|
808
|
+
const sealedOps = netted.ops.map((op) => {
|
|
809
|
+
const { seq, last, ...rest } = op;
|
|
810
|
+
return {
|
|
811
|
+
...rest,
|
|
812
|
+
seq,
|
|
813
|
+
last,
|
|
814
|
+
h: tickClock(db, replica, at(op)?.at_ms ?? now()),
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
const txnHlc = sealedOps.reduce((m, o) => (o.h > m ? o.h : m), sealedOps[0]?.h ?? '');
|
|
818
|
+
const kind = g.frame !== null && TXN_KINDS.has(g.kind) ? g.kind : 'write';
|
|
819
|
+
insTxn.run(txn, localSeq, replica, txnHlc, opts.scope, g.frame === null ? 'foreign' : 'accessor', kind, g.actor, g.frame, g.frame === null ? 1 : 0, partial ? 1 : 0, sealedOps.length, now());
|
|
820
|
+
sealedOps.forEach(({ seq, last, ...op }, i) => {
|
|
821
|
+
const rk = at({ seq, last })?.rk ?? '';
|
|
822
|
+
insOp.run(txn, i, op.t, op.u, op.o, op.h, canonicalJson(op));
|
|
823
|
+
const def = ctx.def(op.t);
|
|
824
|
+
const prev = meta.get.get(op.t, op.u);
|
|
825
|
+
const keyJson = op.k ? canonicalJson(op.k) : null;
|
|
826
|
+
if (op.o === 'K' && op.nu !== undefined) {
|
|
827
|
+
// A K that keeps its uid changes only birth_fp: its meta stays put.
|
|
828
|
+
if (op.nu !== op.u)
|
|
829
|
+
meta.remove.run(op.t, op.nu);
|
|
830
|
+
if (prev) {
|
|
831
|
+
meta.move.run(op.nu, keyJson, op.bfp ?? null, op.t, op.u);
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
meta.upsert.run(op.t, op.nu, op.h, null, replica, g.actor, 1, 0, keyJson, null, op.bfp ?? null);
|
|
835
|
+
}
|
|
836
|
+
touched.delete(rowKey(op.t, op.u));
|
|
837
|
+
touched.set(rowKey(op.t, op.nu), { tbl: op.t, uid: op.nu, rk });
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (op.o === 'D' && def.appendOnly) {
|
|
841
|
+
// §1.7 R5-5: append-only rows get no per-row tombstone; deleted vs
|
|
842
|
+
// never seen is decided by the prune cutoff. The D op still travels.
|
|
843
|
+
meta.remove.run(op.t, op.u);
|
|
844
|
+
ledgerDelta.set(op.t, (ledgerDelta.get(op.t) ?? 0) - 1);
|
|
845
|
+
touched.delete(rowKey(op.t, op.u));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const changed = op.o === 'U' ? Object.keys(op.a ?? {}) : [];
|
|
849
|
+
const fhlc = op.o === 'U' ? nextFhlc(prev, def, changed, op.h) : null;
|
|
850
|
+
meta.upsert.run(op.t, op.u, op.h, fhlc, replica, g.actor, (prev?.version ?? 0) + 1, op.o === 'D' ? 1 : 0, keyJson, op.o === 'D' ? (prev?.chash ?? null) : null, op.bfp ?? null);
|
|
851
|
+
if (op.o === 'I')
|
|
852
|
+
ledgerDelta.set(op.t, (ledgerDelta.get(op.t) ?? 0) + 1);
|
|
853
|
+
if (op.o === 'D')
|
|
854
|
+
ledgerDelta.set(op.t, (ledgerDelta.get(op.t) ?? 0) - 1);
|
|
855
|
+
if (op.o === 'D')
|
|
856
|
+
touched.delete(rowKey(op.t, op.u));
|
|
857
|
+
else
|
|
858
|
+
touched.set(rowKey(op.t, op.u), { tbl: op.t, uid: op.u, rk });
|
|
859
|
+
});
|
|
860
|
+
txns += 1;
|
|
861
|
+
ops += sealedOps.length;
|
|
862
|
+
if (g.frame === null)
|
|
863
|
+
unframed += 1;
|
|
864
|
+
}
|
|
865
|
+
if (localSeq !== firstSeq) {
|
|
866
|
+
db.prepare('INSERT INTO _sync_meta (key, value, updated_at) VALUES (?, ?, ?) ' +
|
|
867
|
+
'ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at').run(SEAL_COUNTER_KEY, String(localSeq), new Date(now()).toISOString());
|
|
868
|
+
}
|
|
869
|
+
// Consume the sealed captures (and frames nothing references any more).
|
|
870
|
+
const del = db.prepare('DELETE FROM _sync_capture WHERE seq = ?');
|
|
871
|
+
for (const seq of consumed)
|
|
872
|
+
del.run(seq);
|
|
873
|
+
db.exec("DELETE FROM _sync_frame WHERE NOT EXISTS (SELECT 1 FROM _sync_capture c WHERE c.frame = _sync_frame.frame AND c.state = 'live')");
|
|
874
|
+
// chash from the live row, once no live capture of that row remains.
|
|
875
|
+
const stillLive = db.prepare("SELECT 1 FROM _sync_capture WHERE tbl = ? AND rk = ? AND state = 'live' LIMIT 1");
|
|
876
|
+
const setChash = db.prepare('UPDATE _sync_row_meta SET chash = ? WHERE tbl = ? AND uid = ?');
|
|
877
|
+
for (const { tbl, uid, rk } of touched.values()) {
|
|
878
|
+
if (stillLive.get(tbl, rk))
|
|
879
|
+
continue;
|
|
880
|
+
setChash.run(chashOf(ctx, ctx.def(tbl), uid), tbl, uid);
|
|
881
|
+
}
|
|
882
|
+
// Ledger: sealed live rows per table. First sight: count(*) minus the net
|
|
883
|
+
// effect of captures still waiting to be sealed.
|
|
884
|
+
const ledgerGet = db.prepare('SELECT live FROM _sync_ledger WHERE tbl = ?');
|
|
885
|
+
const ledgerSet = db.prepare('INSERT INTO _sync_ledger (tbl, live) VALUES (?, ?) ON CONFLICT (tbl) DO UPDATE SET live = excluded.live');
|
|
886
|
+
for (const [tbl, delta] of ledgerDelta) {
|
|
887
|
+
const row = ledgerGet.get(tbl);
|
|
888
|
+
if (row) {
|
|
889
|
+
ledgerSet.run(tbl, row.live + delta);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const count = db.prepare(`SELECT count(*) AS n FROM ${q(tbl)}`).get().n;
|
|
893
|
+
ledgerSet.run(tbl, count - waitingEffect(db, tbl, metaFacts));
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
txns,
|
|
897
|
+
ops,
|
|
898
|
+
captures: consumed.size,
|
|
899
|
+
unframed,
|
|
900
|
+
pending,
|
|
901
|
+
dropped,
|
|
902
|
+
quarantined,
|
|
903
|
+
refused: null,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
//# sourceMappingURL=sealer.js.map
|