@murphai/murph 1.3.1 → 1.3.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/CHANGELOG.md +808 -0
- package/config.schema.json +91 -40
- package/dist/bin.js +6 -4
- package/dist/bin.js.map +1 -1
- package/dist/cli-entry.d.ts +8 -1
- package/dist/cli-entry.d.ts.map +1 -1
- package/dist/cli-entry.js +79 -3
- package/dist/cli-entry.js.map +1 -1
- package/dist/commands/audit-command-helpers.d.ts.map +1 -1
- package/dist/commands/audit-command-helpers.js +7 -5
- package/dist/commands/audit-command-helpers.js.map +1 -1
- package/dist/commands/batch.d.ts +5 -5
- package/dist/commands/batch.d.ts.map +1 -1
- package/dist/commands/batch.js +2 -21
- package/dist/commands/batch.js.map +1 -1
- package/dist/commands/commons.d.ts.map +1 -1
- package/dist/commands/commons.js +2 -4
- package/dist/commands/commons.js.map +1 -1
- package/dist/commands/exercise.d.ts.map +1 -1
- package/dist/commands/exercise.js +16 -3
- package/dist/commands/exercise.js.map +1 -1
- package/dist/commands/experiment-progress-card-image.d.ts.map +1 -1
- package/dist/commands/experiment-progress-card-image.js +63 -12
- package/dist/commands/experiment-progress-card-image.js.map +1 -1
- package/dist/commands/experiment.d.ts.map +1 -1
- package/dist/commands/experiment.js +45 -5
- package/dist/commands/experiment.js.map +1 -1
- package/dist/commands/export-intake-read-helpers.d.ts.map +1 -1
- package/dist/commands/export-intake-read-helpers.js +2 -3
- package/dist/commands/export-intake-read-helpers.js.map +1 -1
- package/dist/commands/food.d.ts.map +1 -1
- package/dist/commands/food.js +1 -7
- package/dist/commands/food.js.map +1 -1
- package/dist/commands/habitat.d.ts.map +1 -1
- package/dist/commands/habitat.js +120 -9
- package/dist/commands/habitat.js.map +1 -1
- package/dist/commands/health-blood-test-save.d.ts.map +1 -1
- package/dist/commands/health-blood-test-save.js +42 -8
- package/dist/commands/health-blood-test-save.js.map +1 -1
- package/dist/commands/meal.d.ts.map +1 -1
- package/dist/commands/meal.js +4 -1
- package/dist/commands/meal.js.map +1 -1
- package/dist/commands/measurement.d.ts.map +1 -1
- package/dist/commands/measurement.js +3 -0
- package/dist/commands/measurement.js.map +1 -1
- package/dist/commands/murph-age.d.ts +9 -1
- package/dist/commands/murph-age.d.ts.map +1 -1
- package/dist/commands/murph-age.js +132 -7
- package/dist/commands/murph-age.js.map +1 -1
- package/dist/commands/public-validation-issue.d.ts +7 -0
- package/dist/commands/public-validation-issue.d.ts.map +1 -0
- package/dist/commands/public-validation-issue.js +7 -0
- package/dist/commands/public-validation-issue.js.map +1 -0
- package/dist/commands/scheduled-log.d.ts +12 -0
- package/dist/commands/scheduled-log.d.ts.map +1 -1
- package/dist/commands/scheduled-log.js +456 -106
- package/dist/commands/scheduled-log.js.map +1 -1
- package/dist/commands/wearables.d.ts +286 -0
- package/dist/commands/wearables.d.ts.map +1 -1
- package/dist/commands/wearables.js +33 -1
- package/dist/commands/wearables.js.map +1 -1
- package/dist/commands/workout-live.d.ts.map +1 -1
- package/dist/commands/workout-live.js +123 -42
- package/dist/commands/workout-live.js.map +1 -1
- package/dist/commands/workout-typed-options.d.ts +38 -0
- package/dist/commands/workout-typed-options.d.ts.map +1 -0
- package/dist/commands/workout-typed-options.js +90 -0
- package/dist/commands/workout-typed-options.js.map +1 -0
- package/dist/commands/workout.d.ts.map +1 -1
- package/dist/commands/workout.js +80 -73
- package/dist/commands/workout.js.map +1 -1
- package/dist/hosted-data-api-labels.d.ts.map +1 -1
- package/dist/hosted-data-api-labels.js +161 -30
- package/dist/hosted-data-api-labels.js.map +1 -1
- package/dist/incur-error-bridge.d.ts.map +1 -1
- package/dist/incur-error-bridge.js +7 -15
- package/dist/incur-error-bridge.js.map +1 -1
- package/dist/incur.generated.d.ts +22 -13
- package/dist/incur.generated.d.ts.map +1 -1
- package/dist/mapbox-address.d.ts.map +1 -1
- package/dist/mapbox-address.js +3 -4
- package/dist/mapbox-address.js.map +1 -1
- package/dist/mapbox-route-client.d.ts +2 -2
- package/dist/mapbox-route-client.d.ts.map +1 -1
- package/dist/mapbox-route-client.js +49 -12
- package/dist/mapbox-route-client.js.map +1 -1
- package/dist/mapbox-route-directions.d.ts.map +1 -1
- package/dist/mapbox-route-directions.js +0 -1
- package/dist/mapbox-route-directions.js.map +1 -1
- package/dist/mapbox-route-elevation.js +0 -1
- package/dist/mapbox-route-elevation.js.map +1 -1
- package/dist/mapbox-route-points.js +0 -2
- package/dist/mapbox-route-points.js.map +1 -1
- package/dist/mapbox-route.d.ts.map +1 -1
- package/dist/mapbox-route.js +1 -1
- package/dist/mapbox-route.js.map +1 -1
- package/dist/vault-cli-command-manifest.d.ts +304 -6
- package/dist/vault-cli-command-manifest.d.ts.map +1 -1
- package/dist/vault-cli-skill-hash.generated.d.ts +1 -1
- package/dist/vault-cli-skill-hash.generated.js +1 -1
- package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.js +6 -2
- package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/README.md +4 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.d.ts +15 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.js +121 -24
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-journal.js +2 -2
- package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-journal.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.js +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.js +55 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.js +9 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cli-surface-contract.generated.json +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.js +7 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.js +123 -22
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.js +48 -19
- package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/context-snapshot.js +3 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/context-snapshot.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.d.ts +5 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.js +24 -10
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.d.ts +2 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.js +12 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.d.ts +5 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.js +9 -10
- package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.d.ts +41 -9
- package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.js +9 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.js +2 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.d.ts +3 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.js +8 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.d.ts +3 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.js +26 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.js +687 -370
- package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.js +54 -18
- package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.d.ts +2 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.js +10 -9
- package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.js +146 -18
- package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.d.ts +25 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.js +72 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.d.ts +4 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.js +14 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.js +8 -5
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.d.ts +4 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.js +8 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/private-completion-continuity.js +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/private-completion-continuity.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.js +25 -2
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.d.ts +11 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.js +67 -176
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.d.ts +17 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.js +10 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.d.ts +19 -4
- package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.js +76 -40
- package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-contracts.d.ts +3 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-contracts.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.d.ts +4 -2
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.js +8 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/shared.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/shared.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/shared.js +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/shared.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/store/types.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/store/types.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/store.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/store.js +6 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/store.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.d.ts +3 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.js +57 -29
- package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.js +17 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.d.ts +4 -7
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.js +21 -6
- package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.js +6 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-input.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/turn-input.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-ask.js +8 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-ask.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.js +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.d.ts +49 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.js +387 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.js +12 -13
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.d.ts +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.js +16 -5
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.js +2 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.d.ts +4 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.js +26 -13
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.d.ts +1024 -1091
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.js +354 -330
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.d.ts +50 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.js +79 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.d.ts +4 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.js +153 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.d.ts +4 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.js +29 -35
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.d.ts +4 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.js +43 -9
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.d.ts +6 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.d.ts +60 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.js +91 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.d.ts +41 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.js +85 -9
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.d.ts +25 -2
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.js +280 -89
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/failures.js +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.js +1 -3
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.d.ts +19 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.d.ts.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.js +203 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.js.map +1 -0
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex.d.ts +9 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex.js +220 -130
- package/node_modules/@murphai/assistant-engine/dist/assistant-codex.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.js +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.d.ts +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.js +1 -1
- package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/index.d.ts +2 -1
- package/node_modules/@murphai/assistant-engine/dist/index.d.ts.map +1 -1
- package/node_modules/@murphai/assistant-engine/dist/index.js +1 -0
- package/node_modules/@murphai/assistant-engine/dist/index.js.map +1 -1
- package/node_modules/@murphai/assistant-engine/skills/automatic-meal-capture/SKILL.md +111 -68
- package/node_modules/@murphai/assistant-engine/skills/behavior-followthrough/SKILL.md +0 -2
- package/node_modules/@murphai/assistant-engine/skills/daily-activity/SKILL.md +11 -4
- package/node_modules/@murphai/assistant-engine/skills/food-journal/SKILL.md +83 -22
- package/node_modules/@murphai/assistant-engine/skills/hosted-low-usage/SKILL.md +3 -3
- package/node_modules/@murphai/assistant-engine/skills/murph-family/SKILL.md +1 -1
- package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/SKILL.md +17 -11
- package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/references/daily-nutrition-card-goals.md +62 -34
- package/node_modules/@murphai/assistant-engine/skills/physical-notes/SKILL.md +47 -0
- package/node_modules/@murphai/assistant-engine/skills/shared/exercise-catalog-runtime.md +6 -0
- package/node_modules/@murphai/assistant-engine/skills/signup-link/SKILL.md +1 -1
- package/node_modules/@murphai/assistant-engine/skills/strength-training/SKILL.md +12 -5
- package/node_modules/@murphai/assistant-engine/skills/tracked-table/SKILL.md +156 -66
- package/node_modules/@murphai/core/dist/automation.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/automation.js +17 -9
- package/node_modules/@murphai/core/dist/automation.js.map +1 -1
- package/node_modules/@murphai/core/dist/bank/habitat.d.ts +2 -0
- package/node_modules/@murphai/core/dist/bank/habitat.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/bank/habitat.js +27 -0
- package/node_modules/@murphai/core/dist/bank/habitat.js.map +1 -1
- package/node_modules/@murphai/core/dist/bank/regimens.js +1 -1
- package/node_modules/@murphai/core/dist/bank/regimens.js.map +1 -1
- package/node_modules/@murphai/core/dist/domains/events/ledger.d.ts +21 -0
- package/node_modules/@murphai/core/dist/domains/events/ledger.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/domains/events/ledger.js +102 -8
- package/node_modules/@murphai/core/dist/domains/events/ledger.js.map +1 -1
- package/node_modules/@murphai/core/dist/domains/events.d.ts +2 -2
- package/node_modules/@murphai/core/dist/domains/events.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/domains/events.js +1 -1
- package/node_modules/@murphai/core/dist/domains/events.js.map +1 -1
- package/node_modules/@murphai/core/dist/history/api.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/history/api.js +18 -3
- package/node_modules/@murphai/core/dist/history/api.js.map +1 -1
- package/node_modules/@murphai/core/dist/index.d.ts +4 -2
- package/node_modules/@murphai/core/dist/index.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/index.js +3 -1
- package/node_modules/@murphai/core/dist/index.js.map +1 -1
- package/node_modules/@murphai/core/dist/mutations.d.ts +25 -2
- package/node_modules/@murphai/core/dist/mutations.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/mutations.js +1588 -78
- package/node_modules/@murphai/core/dist/mutations.js.map +1 -1
- package/node_modules/@murphai/core/dist/operations/index.d.ts +1 -1
- package/node_modules/@murphai/core/dist/operations/index.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/operations/index.js +1 -1
- package/node_modules/@murphai/core/dist/operations/index.js.map +1 -1
- package/node_modules/@murphai/core/dist/operations/write-batch.d.ts +2 -0
- package/node_modules/@murphai/core/dist/operations/write-batch.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/operations/write-batch.js +7 -0
- package/node_modules/@murphai/core/dist/operations/write-batch.js.map +1 -1
- package/node_modules/@murphai/core/dist/protocols.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/protocols.js +52 -9
- package/node_modules/@murphai/core/dist/protocols.js.map +1 -1
- package/node_modules/@murphai/core/dist/public-mutations.d.ts +1 -1
- package/node_modules/@murphai/core/dist/public-mutations.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/public-mutations.js +2 -2
- package/node_modules/@murphai/core/dist/public-mutations.js.map +1 -1
- package/node_modules/@murphai/core/dist/scheduled-logs.d.ts +3 -0
- package/node_modules/@murphai/core/dist/scheduled-logs.d.ts.map +1 -1
- package/node_modules/@murphai/core/dist/scheduled-logs.js +65 -29
- package/node_modules/@murphai/core/dist/scheduled-logs.js.map +1 -1
- package/node_modules/@murphai/device-syncd/README.md +140 -46
- package/node_modules/@murphai/device-syncd/dist/client.d.ts +14 -0
- package/node_modules/@murphai/device-syncd/dist/client.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/client.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/connect-routes.d.ts +4 -1
- package/node_modules/@murphai/device-syncd/dist/config/connect-routes.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/connect-routes.js +24 -2
- package/node_modules/@murphai/device-syncd/dist/config/connect-routes.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/junction-connect-sources.js +2 -2
- package/node_modules/@murphai/device-syncd/dist/config/junction-connect-sources.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.d.ts +53 -0
- package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.js +13 -0
- package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/connect-config.d.ts +1 -1
- package/node_modules/@murphai/device-syncd/dist/connect-config.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/connect-config.js +1 -1
- package/node_modules/@murphai/device-syncd/dist/connect-config.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/fitbit-migration.d.ts +57 -0
- package/node_modules/@murphai/device-syncd/dist/fitbit-migration.d.ts.map +1 -0
- package/node_modules/@murphai/device-syncd/dist/fitbit-migration.js +166 -0
- package/node_modules/@murphai/device-syncd/dist/fitbit-migration.js.map +1 -0
- package/node_modules/@murphai/device-syncd/dist/hosted-runtime.d.ts +43 -0
- package/node_modules/@murphai/device-syncd/dist/hosted-runtime.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/hosted-runtime.js +129 -5
- package/node_modules/@murphai/device-syncd/dist/hosted-runtime.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.d.ts +3 -0
- package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.js +36 -16
- package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.d.ts +17 -2
- package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.js +99 -40
- package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.d.ts +17 -0
- package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.d.ts.map +1 -0
- package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.js +45 -0
- package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.js.map +1 -0
- package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.d.ts +8 -1
- package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.js +12 -1
- package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/providers/junction-client.d.ts +7 -0
- package/node_modules/@murphai/device-syncd/dist/providers/junction-client.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/providers/junction-client.js +285 -46
- package/node_modules/@murphai/device-syncd/dist/providers/junction-client.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/providers/junction.d.ts +9 -0
- package/node_modules/@murphai/device-syncd/dist/providers/junction.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/providers/junction.js +2045 -367
- package/node_modules/@murphai/device-syncd/dist/providers/junction.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/public-account.d.ts +2 -0
- package/node_modules/@murphai/device-syncd/dist/public-account.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/public-account.js +96 -0
- package/node_modules/@murphai/device-syncd/dist/public-account.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/public-ingress.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/public-ingress.js +6 -4
- package/node_modules/@murphai/device-syncd/dist/public-ingress.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/service.d.ts +6 -2
- package/node_modules/@murphai/device-syncd/dist/service.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/service.js +385 -44
- package/node_modules/@murphai/device-syncd/dist/service.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/source-staleness.d.ts +15 -0
- package/node_modules/@murphai/device-syncd/dist/source-staleness.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/source-staleness.js +25 -1
- package/node_modules/@murphai/device-syncd/dist/source-staleness.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/jobs.d.ts +10 -1
- package/node_modules/@murphai/device-syncd/dist/store/jobs.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/jobs.js +95 -1
- package/node_modules/@murphai/device-syncd/dist/store/jobs.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/schema.d.ts +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/schema.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/schema.js +25 -3
- package/node_modules/@murphai/device-syncd/dist/store/schema.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/sources.d.ts +24 -2
- package/node_modules/@murphai/device-syncd/dist/store/sources.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store/sources.js +223 -14
- package/node_modules/@murphai/device-syncd/dist/store/sources.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store.d.ts +13 -3
- package/node_modules/@murphai/device-syncd/dist/store.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/store.js +104 -22
- package/node_modules/@murphai/device-syncd/dist/store.js.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/types.d.ts +96 -9
- package/node_modules/@murphai/device-syncd/dist/types.d.ts.map +1 -1
- package/node_modules/@murphai/device-syncd/dist/types.js +4 -0
- package/node_modules/@murphai/device-syncd/dist/types.js.map +1 -1
- package/node_modules/@murphai/device-syncd/package.json +8 -0
- package/node_modules/@murphai/health-metrics/dist/definitions/activity.d.ts.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/definitions/activity.js +66 -0
- package/node_modules/@murphai/health-metrics/dist/definitions/activity.js.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/definitions/body.js +2 -2
- package/node_modules/@murphai/health-metrics/dist/definitions/body.js.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/normalize.d.ts.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/normalize.js +3 -0
- package/node_modules/@murphai/health-metrics/dist/normalize.js.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.d.ts +3 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.d.ts.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.js +5 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.js.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.d.ts +79 -0
- package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.d.ts.map +1 -1
- package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.js +19 -0
- package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.js.map +1 -1
- package/node_modules/@murphai/importers/dist/core-port.d.ts +15 -1
- package/node_modules/@murphai/importers/dist/core-port.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/core-port.js.map +1 -1
- package/node_modules/@murphai/importers/dist/create-importers.d.ts +9 -1
- package/node_modules/@murphai/importers/dist/create-importers.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/create-importers.js +12 -2
- package/node_modules/@murphai/importers/dist/create-importers.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.d.ts +17 -3
- package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.js +102 -8
- package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/index.d.ts +5 -4
- package/node_modules/@murphai/importers/dist/device-providers/index.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/index.js +4 -3
- package/node_modules/@murphai/importers/dist/device-providers/index.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.d.ts +11 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.js +420 -27
- package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-origin.d.ts +1 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction-origin.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-origin.js +7 -2
- package/node_modules/@murphai/importers/dist/device-providers/junction-origin.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-resources.d.ts +4 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-resources.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-resources.js +42 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-resources.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.d.ts +79 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.d.ts.map +1 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.js +356 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.js.map +1 -0
- package/node_modules/@murphai/importers/dist/device-providers/junction.d.ts +76 -3
- package/node_modules/@murphai/importers/dist/device-providers/junction.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/junction.js +1676 -347
- package/node_modules/@murphai/importers/dist/device-providers/junction.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.js +1 -0
- package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.js.map +1 -1
- package/node_modules/@murphai/importers/dist/device-providers/types.d.ts +9 -1
- package/node_modules/@murphai/importers/dist/device-providers/types.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/index.d.ts +2 -2
- package/node_modules/@murphai/importers/dist/index.d.ts.map +1 -1
- package/node_modules/@murphai/importers/dist/index.js +1 -1
- package/node_modules/@murphai/importers/dist/index.js.map +1 -1
- package/node_modules/@murphai/inboxd/README.md +1 -1
- package/node_modules/@murphai/inboxd/dist/indexing/retention.d.ts +6 -0
- package/node_modules/@murphai/inboxd/dist/indexing/retention.d.ts.map +1 -1
- package/node_modules/@murphai/inboxd/dist/indexing/retention.js +64 -18
- package/node_modules/@murphai/inboxd/dist/indexing/retention.js.map +1 -1
- package/node_modules/@murphai/inboxd/dist/retention.d.ts +1 -1
- package/node_modules/@murphai/inboxd/dist/retention.d.ts.map +1 -1
- package/node_modules/@murphai/inboxd/dist/retention.js +1 -1
- package/node_modules/@murphai/inboxd/dist/retention.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.d.ts +1 -0
- package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.js +5 -0
- package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/assistant-response-cards.d.ts +175 -152
- package/node_modules/@murphai/operator-config/dist/assistant-response-cards.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/assistant-response-cards.js +62 -41
- package/node_modules/@murphai/operator-config/dist/assistant-response-cards.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/device-sync-client.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/device-sync-client.js +1 -5
- package/node_modules/@murphai/operator-config/dist/device-sync-client.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.d.ts +66 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.js +58 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.d.ts +24 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.d.ts.map +1 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.js +219 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.js.map +1 -0
- package/node_modules/@murphai/operator-config/dist/vault-cli-errors.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/vault-cli-errors.js.map +1 -1
- package/node_modules/@murphai/operator-config/dist/workout-action-binding.d.ts +9 -0
- package/node_modules/@murphai/operator-config/dist/workout-action-binding.d.ts.map +1 -1
- package/node_modules/@murphai/operator-config/dist/workout-action-binding.js +48 -5
- package/node_modules/@murphai/operator-config/dist/workout-action-binding.js.map +1 -1
- package/node_modules/@murphai/operator-config/package.json +5 -0
- package/node_modules/@murphai/query/README.md +20 -5
- package/node_modules/@murphai/query/dist/browser-replica/build.d.ts +1 -1
- package/node_modules/@murphai/query/dist/browser-replica/build.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/browser-replica/build.js +25 -16
- package/node_modules/@murphai/query/dist/browser-replica/build.js.map +1 -1
- package/node_modules/@murphai/query/dist/browser-replica/json.d.ts +6 -0
- package/node_modules/@murphai/query/dist/browser-replica/json.d.ts.map +1 -0
- package/node_modules/@murphai/query/dist/browser-replica/json.js +103 -0
- package/node_modules/@murphai/query/dist/browser-replica/json.js.map +1 -0
- package/node_modules/@murphai/query/dist/browser-replica/shared.d.ts +1 -0
- package/node_modules/@murphai/query/dist/browser-replica/shared.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/browser-replica/source.d.ts +13 -0
- package/node_modules/@murphai/query/dist/browser-replica/source.d.ts.map +1 -0
- package/node_modules/@murphai/query/dist/browser-replica/source.js +45 -0
- package/node_modules/@murphai/query/dist/browser-replica/source.js.map +1 -0
- package/node_modules/@murphai/query/dist/browser-replica-server.d.ts +2 -0
- package/node_modules/@murphai/query/dist/browser-replica-server.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/browser-replica-server.js +2 -0
- package/node_modules/@murphai/query/dist/browser-replica-server.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/blood-tests.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/blood-tests.js +3 -2
- package/node_modules/@murphai/query/dist/health/blood-tests.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/canonical-collector.d.ts +11 -1
- package/node_modules/@murphai/query/dist/health/canonical-collector.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/canonical-collector.js +39 -16
- package/node_modules/@murphai/query/dist/health/canonical-collector.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/entity-slices.d.ts +9 -3
- package/node_modules/@murphai/query/dist/health/entity-slices.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/entity-slices.js +6 -6
- package/node_modules/@murphai/query/dist/health/entity-slices.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/immunizations.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/immunizations.js +14 -11
- package/node_modules/@murphai/query/dist/health/immunizations.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/loaders.d.ts +21 -7
- package/node_modules/@murphai/query/dist/health/loaders.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/loaders.js +53 -15
- package/node_modules/@murphai/query/dist/health/loaders.js.map +1 -1
- package/node_modules/@murphai/query/dist/health/registries.d.ts +1 -0
- package/node_modules/@murphai/query/dist/health/registries.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/health/registries.js.map +1 -1
- package/node_modules/@murphai/query/dist/index.d.ts +4 -3
- package/node_modules/@murphai/query/dist/index.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/index.js +4 -3
- package/node_modules/@murphai/query/dist/index.js.map +1 -1
- package/node_modules/@murphai/query/dist/meal-nutrition.d.ts +1 -1
- package/node_modules/@murphai/query/dist/meal-nutrition.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/meal-nutrition.js +10 -3
- package/node_modules/@murphai/query/dist/meal-nutrition.js.map +1 -1
- package/node_modules/@murphai/query/dist/metrics/projection.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/metrics/projection.js +19 -6
- package/node_modules/@murphai/query/dist/metrics/projection.js.map +1 -1
- package/node_modules/@murphai/query/dist/murph-age.d.ts +5 -1
- package/node_modules/@murphai/query/dist/murph-age.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/murph-age.js +22 -8
- package/node_modules/@murphai/query/dist/murph-age.js.map +1 -1
- package/node_modules/@murphai/query/dist/projection/entity-store.js +4 -4
- package/node_modules/@murphai/query/dist/projection/entity-store.js.map +1 -1
- package/node_modules/@murphai/query/dist/projection/schema.d.ts +1 -1
- package/node_modules/@murphai/query/dist/projection/schema.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/projection/schema.js +3 -1
- package/node_modules/@murphai/query/dist/projection/schema.js.map +1 -1
- package/node_modules/@murphai/query/dist/projection/wearable-summary-compose.js +4 -0
- package/node_modules/@murphai/query/dist/projection/wearable-summary-compose.js.map +1 -1
- package/node_modules/@murphai/query/dist/projection/wearable-summary-projector.js +4 -0
- package/node_modules/@murphai/query/dist/projection/wearable-summary-projector.js.map +1 -1
- package/node_modules/@murphai/query/dist/query-visibility.d.ts +5 -0
- package/node_modules/@murphai/query/dist/query-visibility.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/query-visibility.js +11 -5
- package/node_modules/@murphai/query/dist/query-visibility.js.map +1 -1
- package/node_modules/@murphai/query/dist/read-model.d.ts +8 -0
- package/node_modules/@murphai/query/dist/read-model.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/read-model.js +11 -1
- package/node_modules/@murphai/query/dist/read-model.js.map +1 -1
- package/node_modules/@murphai/query/dist/scheduled-logs.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/scheduled-logs.js +55 -79
- package/node_modules/@murphai/query/dist/scheduled-logs.js.map +1 -1
- package/node_modules/@murphai/query/dist/vault-source.d.ts +22 -4
- package/node_modules/@murphai/query/dist/vault-source.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/vault-source.js +188 -59
- package/node_modules/@murphai/query/dist/vault-source.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/candidates.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/candidates.js +26 -15
- package/node_modules/@murphai/query/dist/wearables/candidates.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/public-output.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/public-output.js +6 -0
- package/node_modules/@murphai/query/dist/wearables/public-output.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/source-health.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/source-health.js +4 -0
- package/node_modules/@murphai/query/dist/wearables/source-health.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/types.d.ts +41 -4
- package/node_modules/@murphai/query/dist/wearables/types.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/types.js +2 -0
- package/node_modules/@murphai/query/dist/wearables/types.js.map +1 -1
- package/node_modules/@murphai/query/dist/wearables/workout-features.d.ts +5 -0
- package/node_modules/@murphai/query/dist/wearables/workout-features.d.ts.map +1 -0
- package/node_modules/@murphai/query/dist/wearables/workout-features.js +179 -0
- package/node_modules/@murphai/query/dist/wearables/workout-features.js.map +1 -0
- package/node_modules/@murphai/query/dist/wearables.d.ts.map +1 -1
- package/node_modules/@murphai/query/dist/wearables.js +23 -6
- package/node_modules/@murphai/query/dist/wearables.js.map +1 -1
- package/node_modules/@murphai/query/package.json +5 -0
- package/node_modules/@murphai/runtime-state/README.md +1 -1
- package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.d.ts +4 -0
- package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.d.ts.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.js +16 -3
- package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.js.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/hosted-bundles.d.ts +1 -0
- package/node_modules/@murphai/runtime-state/dist/hosted-bundles.d.ts.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/hosted-bundles.js +31 -0
- package/node_modules/@murphai/runtime-state/dist/hosted-bundles.js.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/hosted-email.d.ts +5 -0
- package/node_modules/@murphai/runtime-state/dist/hosted-email.d.ts.map +1 -1
- package/node_modules/@murphai/runtime-state/dist/hosted-email.js +10 -0
- package/node_modules/@murphai/runtime-state/dist/hosted-email.js.map +1 -1
- package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.d.ts.map +1 -1
- package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.js +6 -15
- package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/README.md +9 -0
- package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.d.ts +6 -0
- package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.js +49 -1
- package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/capture.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/capture.js +22 -12
- package/node_modules/@murphai/vault-usecases/dist/usecases/capture.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.js +1 -3
- package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.js +24 -18
- package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.js +1 -3
- package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.d.ts +8 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.js +56 -5
- package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.d.ts +20 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.d.ts.map +1 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.js +72 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.js.map +1 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.d.ts +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.js +42 -9
- package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.js +158 -24
- package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/food.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/food.js +98 -5
- package/node_modules/@murphai/vault-usecases/dist/usecases/food.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.js +20 -4
- package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.js +49 -24
- package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.js +13 -11
- package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.js +18 -11
- package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.js +43 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/shared.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/shared.js +1 -3
- package/node_modules/@murphai/vault-usecases/dist/usecases/shared.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.d.ts +2 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.js +24 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-format.d.ts +2 -0
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-format.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.d.ts +36 -4
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.js +48 -12
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.d.ts +14 -14
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.js +84 -82
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.d.ts +16 -4
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.js +316 -79
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.js +19 -17
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.js.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout.d.ts +20 -7
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout.d.ts.map +1 -1
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout.js +56 -31
- package/node_modules/@murphai/vault-usecases/dist/usecases/workout.js.map +1 -1
- package/node_modules/incur/dist/Cli.js +7 -32
- package/node_modules/incur/dist/Errors.d.ts +17 -0
- package/node_modules/incur/dist/Errors.js +23 -0
- package/node_modules/incur/dist/internal/command.d.ts +2 -0
- package/node_modules/incur/dist/internal/command.js +2 -6
- package/node_modules/incur/src/Cli.ts +11 -32
- package/node_modules/incur/src/Errors.ts +36 -0
- package/node_modules/incur/src/internal/command.ts +4 -6
- package/package.json +4 -4
- package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/references/daily-nutrition-card-safety.md +0 -211
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { createHmac, createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { COMPANION_HRV_RMSSD_RESOURCE, JUNCTION_ALLOWED_SUMMARY_RESOURCES, JUNCTION_ALLOWED_TIMESERIES_RESOURCES, JUNCTION_DEFAULT_SUMMARY_RESOURCES, JUNCTION_DEFAULT_TIMESERIES_RESOURCES, JUNCTION_KNOWN_TIMESERIES_RESOURCES, normalizeJunctionResourceName, resolveJunctionTimeseriesResourcePolicy, usesJunctionTimeseriesIntervalStartOwnership, parseCompanionHrvRmssdAdmissionId, parseSerializedCompanionHrvRmssdObservation, serializeCompanionHrvRmssdObservation, toLocalDayKey, } from "@murphai/contracts";
|
|
4
|
+
import { JUNCTION_ECG_VOLTAGE_FEATURE_SCHEMA, JUNCTION_WORKOUT_STREAM_FEATURE_SCHEMA, JunctionWorkoutStreamTimestampCardinalityError, buildJunctionBoundedFeatureIdentity, buildJunctionDailyTimeseriesAggregateResourceId, canNormalizeJunctionSleepCycleRecordToCompactStages, classifyJunctionSummaryNormalizationEvidence, countAcceptedJunctionDailyTimeseriesProviderRecords, identifyJunctionBloodPressureProviderRecords, normalizeJunctionCanonicalBodyTimestamp, JUNCTION_DENSE_FIDELITY_RESOURCES, parseJunctionCompleteDayTimestamp, reduceJunctionElectrocardiogramVoltageRecords, reduceJunctionWorkoutStreamPayload, resolveJunctionBoundedFeatureRecords, resolveJunctionWeightProviderRecordIdentity, selectJunctionWorkoutStreamCandidates, } from "@murphai/importers/device-providers/junction";
|
|
4
5
|
import { normalizeJunctionSourceProviderSlug, resolveJunctionOrigin, } from "@murphai/importers/device-providers/junction-origin";
|
|
5
|
-
import { isJunctionRawDirectIdentityContainerKey, isJunctionRawDirectIdentityKey, } from "@murphai/importers/device-providers/junction-resources";
|
|
6
|
+
import { isJunctionRawDirectIdentityContainerKey, isJunctionRawDirectIdentityKey, maxJunctionCanonicalCoverageBoundary, normalizeJunctionCanonicalCoverageBoundary, } from "@murphai/importers/device-providers/junction-resources";
|
|
6
7
|
import { JUNCTION_DEVICE_PROVIDER_DESCRIPTOR } from "@murphai/importers/device-providers/provider-descriptors";
|
|
7
8
|
import { deviceSyncError, isDeviceSyncError } from "../errors.js";
|
|
8
9
|
import { JunctionTimeseriesProgressError } from "../junction-timeseries-progress.js";
|
|
9
10
|
import { isHostedRuntimeIdShapedDiagnosticToken, sanitizeHostedRuntimeDiagnosticText, } from "../hosted-runtime.js";
|
|
10
|
-
import {
|
|
11
|
-
import { addJunctionExtendedTimeseriesHistoryBackfillCoverage, addJunctionHistoricalBackfillEvidence, canRepresentJunctionExtendedTimeseriesHistoryBackfillCoverage, canCurrentRuntimeMutateJunctionExtendedTimeseriesHistoryBackfillCoverage, canCurrentRuntimeMutateJunctionHistoricalBackfillProgress, encodeJunctionHistoricalBackfillStatus, hasJunctionExtendedTimeseriesHistoryBackfillCoverage, hasJunctionHistoricalBackfillEvidence, JUNCTION_HISTORICAL_BACKFILL_COVERAGE_VERSION, JUNCTION_HISTORICAL_BACKFILL_METADATA_KEYS, readJunctionHistoricalBackfillEvidence, readJunctionHistoricalBackfillStatus, } from "../junction-historical-backfill-progress.js";
|
|
11
|
+
import { areJunctionProviderSlugsDataEquivalent, classifyDeviceSyncJunctionInlineSourceProviderSlug, isJunctionInlineImportJob, resolveDeviceSyncJunctionInlineSourceProviderSlug, } from "../junction-inline-authority.js";
|
|
12
|
+
import { addJunctionExtendedTimeseriesHistoryBackfillCoverage, addJunctionHistoricalBackfillEvidence, canRepresentJunctionExtendedTimeseriesHistoryBackfillCoverage, canCurrentRuntimeMutateJunctionExtendedTimeseriesHistoryBackfillCoverage, canCurrentRuntimeMutateJunctionHistoricalBackfillProgress, encodeJunctionHistoricalBackfillStatus, hasJunctionExtendedTimeseriesHistoryBackfillCoverage, hasJunctionHistoricalBackfillEvidence, JUNCTION_HISTORICAL_BACKFILL_COVERAGE_VERSION, JUNCTION_HISTORICAL_BACKFILL_METADATA_KEYS, readJunctionHistoricalBackfillEvidence, readJunctionHistoricalBackfillStatus, resolveJunctionExtendedTimeseriesHistoryBackfillVersion, } from "../junction-historical-backfill-progress.js";
|
|
12
13
|
import { DEVICE_SYNC_METADATA_MAX_STRING_LENGTH } from "../metadata.js";
|
|
13
|
-
import {
|
|
14
|
+
import { buildDeviceSyncSourceCanonicalCoverageBoundaryKey, buildDeviceSyncSourceCanonicalCoverageFinalizedAtKey, DEVICE_SYNC_SOURCE_HISTORICAL_BACKFILL_COMPLETED_AT_KEY, DEVICE_SYNC_SOURCE_PROVIDER_DISCONNECTED_ERROR_CODE, isGoogleHealthFitbitMigrationLegacyTerminal, isGoogleHealthFitbitMigrationLegacyCoverageReady, isDeviceSyncSourceResourceAvailabilityMetadataKey, readDeviceSyncSourceCanonicalCoverageBoundary, readDeviceSyncSourceCanonicalCoverageFinalizedAt, } from "../fitbit-migration.js";
|
|
15
|
+
import { DEVICE_SYNC_HISTORICAL_DATA_RECONNECT_REQUIRED_ERROR_CODE, DEVICE_SYNC_SOURCE_USER_DISCONNECTED_ERROR_CODE, isDeviceSyncSourceAdmitted, isDeviceSyncSourceDisconnectFenced, isJunctionHistoricalResetProviderSlug, resolveDeviceSyncSourceState, requiresHistoricalResetDeviceSyncSource, } from "../public-account.js";
|
|
14
16
|
import { assertValidJunctionClientUserIdSecret, buildJunctionDeviceSyncRuntimeDescriptor, normalizeJunctionClientUserIdNamespace, normalizeJunctionDeviceSyncRuntimeConfig, } from "../configured-provider-runtime-descriptors.js";
|
|
15
17
|
import { addMilliseconds, normalizeString, sha256Text, subtractDays, } from "../shared.js";
|
|
16
18
|
import { JUNCTION_COMPANION_HRV_OBSERVATION_INVALID_CODE, JUNCTION_COMPANION_HRV_SOURCE_PROVIDER, JUNCTION_COMPANION_HEALTH_METADATA_EVENT_TYPE, JUNCTION_COMPANION_HEALTH_METADATA_MAX_BATCH_BYTES, JUNCTION_COMPANION_HEALTH_METADATA_RESOURCE, JUNCTION_COMPANION_HEALTH_METADATA_SOURCE_PROVIDER, JUNCTION_COMPANION_HEALTH_METADATA_SOURCE_TYPE, JUNCTION_CALENDAR_REFRESH_EMPTY_IDENTITY_INVALID_CODE, JUNCTION_CALENDAR_REFRESH_JOB_INVALID_CODE, JunctionCompanionHealthMetadataParseError, parseJunctionCompanionHealthMetadataBatch, } from "../junction-resources.js";
|
|
17
19
|
import { JunctionClient, JUNCTION_MAX_USER_PROVIDERS, } from "./junction-client.js";
|
|
18
|
-
import {
|
|
20
|
+
import { canonicalizeJunctionProviderSlug, JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG, JUNCTION_GOOGLE_HEALTH_PROVIDER_SLUG, resolveJunctionDeviceConnectRouteByProviderSlug, } from "../config/connect-routes.js";
|
|
19
21
|
import { buildJunctionProviderSourceInstanceKey, JUNCTION_DEFAULT_PROVIDER_FILTER, normalizeJunctionProviderFilter, } from "../config/junction-connect-sources.js";
|
|
20
|
-
import { classifyDeviceSyncWebhookAcceptanceMode } from "../types.js";
|
|
22
|
+
import { classifyDeviceSyncWebhookAcceptanceMode, JUNCTION_TEMPORAL_AUTHORITY_DEDUPE_PREFIX, } from "../types.js";
|
|
21
23
|
import { evaluatePushPrimarySourceStaleness } from "../source-staleness.js";
|
|
22
24
|
import { JUNCTION_PUSH_SOURCE_RECOVERY_JOB_KIND, JUNCTION_PUSH_SOURCE_RECOVERY_METADATA_KEYS, buildJunctionPushSourceRecoveryMetadataPatch, readJunctionPushSourceRecoveryState, resolveJunctionPushSourceRecoveryAttempts, resolveJunctionPushSourceRecoveryStatus, selectDueJunctionPushSourceRecovery, } from "../junction-push-source-recovery.js";
|
|
23
25
|
export { JUNCTION_DEVICE_PROVIDER_DESCRIPTOR };
|
|
@@ -39,7 +41,7 @@ const JUNCTION_WEBHOOK_ROOT_FIELDS = Object.freeze({
|
|
|
39
41
|
const JUNCTION_SDK_ISO_8601_DATE_PATTERN = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
|
|
40
42
|
const JUNCTION_PROFILE_SUMMARY_RESOURCE = "profile";
|
|
41
43
|
const JUNCTION_PROFILE_SUMMARY_CHECKED_AT_METADATA_KEY = "junctionProfileSummaryCheckedAt";
|
|
42
|
-
const JUNCTION_PROFILE_SUMMARY_NORMALIZATION_REVISION =
|
|
44
|
+
const JUNCTION_PROFILE_SUMMARY_NORMALIZATION_REVISION = 2;
|
|
43
45
|
const JUNCTION_PROFILE_SUMMARY_NORMALIZATION_REVISION_METADATA_KEY = "junctionProfileSummaryNormalizationRevision";
|
|
44
46
|
// `profile` is deliberately excluded: it is a current-state snapshot, so
|
|
45
47
|
// counting it as completion evidence would mark every backfill useful and
|
|
@@ -88,7 +90,7 @@ const JUNCTION_KNOWN_WEBHOOK_RESOURCE_NAMES = new Set([
|
|
|
88
90
|
]);
|
|
89
91
|
const DEFAULT_SUMMARY_BACKFILL_DAYS = JUNCTION_DEVICE_PROVIDER_DESCRIPTOR.sync.windows.backfillDays;
|
|
90
92
|
const DEFAULT_TIMESERIES_BACKFILL_DAYS = 14;
|
|
91
|
-
const
|
|
93
|
+
const JUNCTION_EXTENDED_TIMESERIES_BACKFILL_DAYS = 180;
|
|
92
94
|
const JUNCTION_BOUNDED_TIMESERIES_BACKFILL_POLICY = Object.freeze({
|
|
93
95
|
history: "bounded",
|
|
94
96
|
});
|
|
@@ -96,7 +98,6 @@ const JUNCTION_SPARSE_DAILY_TIMESERIES_BACKFILL_POLICY = Object.freeze({
|
|
|
96
98
|
anchor: "current_day",
|
|
97
99
|
completion: "daily_aggregate",
|
|
98
100
|
history: "extended",
|
|
99
|
-
version: 1,
|
|
100
101
|
});
|
|
101
102
|
// This is the history-depth decision for every admitted resource. Only the
|
|
102
103
|
// sparse resources represented by the package-owned source/resource matrix
|
|
@@ -109,7 +110,6 @@ const JUNCTION_TIMESERIES_BACKFILL_POLICIES = Object.freeze({
|
|
|
109
110
|
anchor: "source_first_seen",
|
|
110
111
|
completion: "exact_records",
|
|
111
112
|
history: "extended",
|
|
112
|
-
version: 1,
|
|
113
113
|
},
|
|
114
114
|
body_temperature: JUNCTION_SPARSE_DAILY_TIMESERIES_BACKFILL_POLICY,
|
|
115
115
|
body_temperature_delta: JUNCTION_SPARSE_DAILY_TIMESERIES_BACKFILL_POLICY,
|
|
@@ -125,7 +125,6 @@ const JUNCTION_TIMESERIES_BACKFILL_POLICIES = Object.freeze({
|
|
|
125
125
|
anchor: "current_day",
|
|
126
126
|
completion: "fetch_complete",
|
|
127
127
|
history: "extended",
|
|
128
|
-
version: JUNCTION_NOTE_HISTORY_BACKFILL_VERSION,
|
|
129
128
|
},
|
|
130
129
|
respiratory_rate: JUNCTION_BOUNDED_TIMESERIES_BACKFILL_POLICY,
|
|
131
130
|
sleep_breathing_disturbance: JUNCTION_SPARSE_DAILY_TIMESERIES_BACKFILL_POLICY,
|
|
@@ -137,7 +136,6 @@ const JUNCTION_TIMESERIES_BACKFILL_POLICIES = Object.freeze({
|
|
|
137
136
|
anchor: "current_day",
|
|
138
137
|
completion: "exact_records",
|
|
139
138
|
history: "extended",
|
|
140
|
-
version: 1,
|
|
141
139
|
},
|
|
142
140
|
body_mass_index: JUNCTION_BOUNDED_TIMESERIES_BACKFILL_POLICY,
|
|
143
141
|
carbohydrates: JUNCTION_BOUNDED_TIMESERIES_BACKFILL_POLICY,
|
|
@@ -171,7 +169,23 @@ function resolveJunctionExtendedTimeseriesBackfillPolicy(resource) {
|
|
|
171
169
|
return null;
|
|
172
170
|
}
|
|
173
171
|
const policy = JUNCTION_TIMESERIES_BACKFILL_POLICIES[resource];
|
|
174
|
-
|
|
172
|
+
if (policy.history !== "extended") {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const version = resolveJunctionExtendedTimeseriesHistoryBackfillVersion(resource);
|
|
176
|
+
if (version === null) {
|
|
177
|
+
throw new TypeError("Junction extended-history policy requires a coverage generation.");
|
|
178
|
+
}
|
|
179
|
+
return { ...policy, version };
|
|
180
|
+
}
|
|
181
|
+
function requiresJunctionHistoricalPullReadiness(policy) {
|
|
182
|
+
return policy?.anchor === "current_day"
|
|
183
|
+
&& (policy.completion === "daily_aggregate" || policy.completion === "exact_records");
|
|
184
|
+
}
|
|
185
|
+
function doesJunctionImportReceiptResolveDeliveredRows(resource) {
|
|
186
|
+
return isJunctionBodyTimeseriesResource(resource)
|
|
187
|
+
|| resource === "carbohydrates"
|
|
188
|
+
|| resource === "insulin_injection";
|
|
175
189
|
}
|
|
176
190
|
const JUNCTION_DENSE_FIDELITY_RESOURCE_SET = new Set(JUNCTION_DENSE_FIDELITY_RESOURCES);
|
|
177
191
|
const JUNCTION_CALENDAR_DAY_AGGREGATE_RESOURCE_SET = new Set([
|
|
@@ -225,6 +239,13 @@ const JUNCTION_FULL_JOB_INVENTORY_COLLECTION_WORK_LIMIT = Object.freeze({
|
|
|
225
239
|
maxPages: 1,
|
|
226
240
|
requestTimeoutMs: 8_000,
|
|
227
241
|
});
|
|
242
|
+
const JUNCTION_TEMPORAL_AUTHORITY_LAG_MS = TIMESERIES_CHUNK_MS;
|
|
243
|
+
const JUNCTION_TEMPORAL_AUTHORITY_RESOURCES = new Set([
|
|
244
|
+
"blood_oxygen",
|
|
245
|
+
"stress_level",
|
|
246
|
+
]);
|
|
247
|
+
const JUNCTION_TEMPORAL_AUTHORITY_JOB_VERSION = 1;
|
|
248
|
+
const JUNCTION_MAX_TEMPORAL_RECONCILE_DAYS = 14;
|
|
228
249
|
const JUNCTION_MAX_DIAGNOSTIC_TIMESERIES_PROBE_DAYS = 14;
|
|
229
250
|
const JUNCTION_DIAGNOSTIC_SHAPE_KEY_LIMIT = 24;
|
|
230
251
|
const JUNCTION_DIAGNOSTIC_RESOURCE_NAME_LIMIT = 64;
|
|
@@ -234,20 +255,45 @@ const EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS = Object.freeze([
|
|
|
234
255
|
6 * 60 * 60_000,
|
|
235
256
|
24 * 60 * 60_000,
|
|
236
257
|
]);
|
|
258
|
+
const JUNCTION_WORKOUT_STREAM_EMPTY_REPLAY_DELAY_MS = 24 * 60 * 60_000;
|
|
237
259
|
const JUNCTION_HISTORICAL_UNRESOLVED_PROVIDER_RECORD_IDENTITIES_VERSION = 1;
|
|
238
260
|
const JUNCTION_BLOOD_PRESSURE_PROVIDER_RECORD_IDENTITY_PATTERN = /^blood-pressure-[0-9a-f]{16}$/u;
|
|
239
261
|
const JUNCTION_SCHEDULED_RECONCILE_PRIORITY = 40;
|
|
240
262
|
const JUNCTION_HISTORICAL_BACKFILL_PRIORITY = 30;
|
|
241
263
|
const JUNCTION_HISTORICAL_BACKFILL_RETRY_PRIORITY = 50;
|
|
264
|
+
const JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS = 4;
|
|
265
|
+
/**
|
|
266
|
+
* Keeps the reopened and ordinary ordinals independent from the 3:1 cadence so
|
|
267
|
+
* every member of either stable pool remains reachable.
|
|
268
|
+
*/
|
|
269
|
+
export function selectJunctionScheduledHistoryCandidate(input) {
|
|
270
|
+
if (input.reopenedCandidates.length === 0) {
|
|
271
|
+
return input.ordinaryCandidates.length === 0
|
|
272
|
+
? undefined
|
|
273
|
+
: input.ordinaryCandidates[input.scheduleSlot % input.ordinaryCandidates.length];
|
|
274
|
+
}
|
|
275
|
+
if (input.ordinaryCandidates.length === 0) {
|
|
276
|
+
return input.reopenedCandidates[input.scheduleSlot % input.reopenedCandidates.length];
|
|
277
|
+
}
|
|
278
|
+
const priorityCycle = Math.floor(input.scheduleSlot / JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS);
|
|
279
|
+
const priorityCycleOffset = input.scheduleSlot % JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS;
|
|
280
|
+
if (priorityCycleOffset === JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS - 1) {
|
|
281
|
+
return input.ordinaryCandidates[priorityCycle % input.ordinaryCandidates.length];
|
|
282
|
+
}
|
|
283
|
+
const reopenedOrdinal = priorityCycle
|
|
284
|
+
* (JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS - 1)
|
|
285
|
+
+ priorityCycleOffset;
|
|
286
|
+
return input.reopenedCandidates[reopenedOrdinal % input.reopenedCandidates.length];
|
|
287
|
+
}
|
|
242
288
|
export function createJunctionDeviceSyncProvider(config) {
|
|
243
289
|
const runtimeConfig = normalizeJunctionDeviceSyncRuntimeConfig(config);
|
|
244
290
|
const client = new JunctionClient(toClientConfig(config));
|
|
245
291
|
const { clientUserIdNamespace, providerFilter, reconcileIntervalMs, summaryResources, timeseriesResources, } = runtimeConfig;
|
|
246
292
|
const summaryBackfillDays = config.summaryBackfillDays ?? DEFAULT_SUMMARY_BACKFILL_DAYS;
|
|
247
293
|
const timeseriesBackfillDays = config.timeseriesBackfillDays ?? DEFAULT_TIMESERIES_BACKFILL_DAYS;
|
|
248
|
-
const extendedTimeseriesBackfillDays = config.timeseriesBackfillDays ?? summaryBackfillDays;
|
|
249
294
|
const extendedBackfillTimeseriesResources = timeseriesResources.filter((resource) => resolveJunctionExtendedTimeseriesBackfillPolicy(resource) !== null);
|
|
250
295
|
const reconcileDays = config.reconcileDays ?? DEFAULT_RECONCILE_DAYS;
|
|
296
|
+
const temporalReconcileDays = normalizeJunctionTemporalReconcileDays(reconcileDays);
|
|
251
297
|
const pushSourceRecoveryEnabled = config.pushSourceRecoveryEnabled === true;
|
|
252
298
|
const webhookTimestampToleranceMs = config.webhookTimestampToleranceMs ?? DEFAULT_WEBHOOK_TIMESTAMP_TOLERANCE_MS;
|
|
253
299
|
async function beginConnection(context) {
|
|
@@ -356,6 +402,14 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
356
402
|
});
|
|
357
403
|
}
|
|
358
404
|
const sourceConnectionWork = buildSourceConnectionWork({
|
|
405
|
+
...(context.sourceProviderSlug
|
|
406
|
+
? {
|
|
407
|
+
historicalProofAuthorization: {
|
|
408
|
+
firstSeenAt: context.now,
|
|
409
|
+
sourceProviderSlug: context.sourceProviderSlug,
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
: {}),
|
|
359
413
|
now: context.now,
|
|
360
414
|
sourceProviderSlug: context.sourceProviderSlug,
|
|
361
415
|
});
|
|
@@ -413,10 +467,16 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
413
467
|
});
|
|
414
468
|
}
|
|
415
469
|
}
|
|
416
|
-
async function revokeSourceAccess(account, sourceProviderSlug) {
|
|
470
|
+
async function revokeSourceAccess(account, sourceProviderSlug, options) {
|
|
417
471
|
const userId = normalizeString(account.externalAccountId);
|
|
418
|
-
const targetProviderSlug =
|
|
419
|
-
|
|
472
|
+
const targetProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
|
|
473
|
+
const requiredActiveProviderSlug = options?.requiredActiveSourceProviderSlug === undefined
|
|
474
|
+
? null
|
|
475
|
+
: canonicalizeJunctionProviderSlug(options.requiredActiveSourceProviderSlug);
|
|
476
|
+
if (!userId
|
|
477
|
+
|| !targetProviderSlug
|
|
478
|
+
|| (options?.requiredActiveSourceProviderSlug !== undefined
|
|
479
|
+
&& !requiredActiveProviderSlug)) {
|
|
420
480
|
throw deviceSyncError({
|
|
421
481
|
code: "JUNCTION_SOURCE_DEREGISTER_INPUT_INVALID",
|
|
422
482
|
message: "Junction source cleanup requires a stored user and provider slug.",
|
|
@@ -425,20 +485,34 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
425
485
|
});
|
|
426
486
|
}
|
|
427
487
|
const providers = await client.listUserProviders(userId);
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
488
|
+
const targetStatuses = providers
|
|
489
|
+
.filter((provider) => (canonicalizeJunctionProviderSlug(provider.origin.sourceProviderSlug)
|
|
490
|
+
?? canonicalizeJunctionProviderSlug(provider.slug)) === targetProviderSlug)
|
|
491
|
+
.map((provider) => mapJunctionSourceStatus(provider.status));
|
|
492
|
+
if (targetStatuses.length === 0
|
|
493
|
+
|| targetStatuses.every((status) => status === "disconnected")) {
|
|
432
494
|
return;
|
|
433
495
|
}
|
|
496
|
+
if (requiredActiveProviderSlug
|
|
497
|
+
&& (!targetStatuses.includes("connected")
|
|
498
|
+
|| !providers.some((provider) => mapJunctionSourceStatus(provider.status) === "connected"
|
|
499
|
+
&& (canonicalizeJunctionProviderSlug(provider.origin.sourceProviderSlug)
|
|
500
|
+
?? canonicalizeJunctionProviderSlug(provider.slug)) === requiredActiveProviderSlug))) {
|
|
501
|
+
throw deviceSyncError({
|
|
502
|
+
code: "JUNCTION_REQUIRED_SOURCE_NOT_ACTIVE",
|
|
503
|
+
message: "Junction source cleanup requires an active successor source.",
|
|
504
|
+
retryable: true,
|
|
505
|
+
httpStatus: 409,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
434
508
|
await client.deregisterProvider({
|
|
435
509
|
providerSlug: targetProviderSlug,
|
|
436
510
|
userId,
|
|
437
511
|
});
|
|
438
512
|
}
|
|
439
|
-
async function isSourceAccessActive(account, sourceProviderSlug) {
|
|
513
|
+
async function isSourceAccessActive(account, sourceProviderSlug, options) {
|
|
440
514
|
const userId = normalizeString(account.externalAccountId);
|
|
441
|
-
const targetProviderSlug =
|
|
515
|
+
const targetProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
|
|
442
516
|
if (!userId || !targetProviderSlug) {
|
|
443
517
|
throw deviceSyncError({
|
|
444
518
|
code: "JUNCTION_SOURCE_STATUS_INPUT_INVALID",
|
|
@@ -447,13 +521,33 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
447
521
|
httpStatus: 409,
|
|
448
522
|
});
|
|
449
523
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
524
|
+
const providerStatuses = (await client.listUserProviders(userId))
|
|
525
|
+
.filter((provider) => (canonicalizeJunctionProviderSlug(provider.origin.sourceProviderSlug)
|
|
526
|
+
?? canonicalizeJunctionProviderSlug(provider.slug)) === targetProviderSlug)
|
|
527
|
+
.map((provider) => provider.status.trim().toLowerCase());
|
|
528
|
+
// Connection authority is the provider row's literal status. The broader
|
|
529
|
+
// projection mapper also accepts lifecycle/resource aliases such as `available`.
|
|
530
|
+
if (providerStatuses.length > 0
|
|
531
|
+
&& providerStatuses.every((status) => status === "connected")) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
const statuses = providerStatuses.map(mapJunctionSourceStatus);
|
|
535
|
+
if (statuses.length === 0 || statuses.every((status) => status === "disconnected")) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
if (options?.requireDefinitive) {
|
|
539
|
+
throw deviceSyncError({
|
|
540
|
+
code: "JUNCTION_SOURCE_STATUS_AMBIGUOUS",
|
|
541
|
+
message: "Junction source status is not definitive yet. Retry shortly.",
|
|
542
|
+
retryable: true,
|
|
543
|
+
httpStatus: 503,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
453
547
|
}
|
|
454
|
-
function createScheduledJobs(account, now) {
|
|
548
|
+
function createScheduledJobs(account, now, context) {
|
|
455
549
|
const scheduledHistoricalBackfillJobs = buildScheduledHistoricalBackfillJobs(account, now);
|
|
456
|
-
const scheduledExtendedTimeseriesBackfillJobs = buildScheduledExtendedTimeseriesBackfillJobs(account, now);
|
|
550
|
+
const scheduledExtendedTimeseriesBackfillJobs = buildScheduledExtendedTimeseriesBackfillJobs(account, now, context);
|
|
457
551
|
const nextReconcileAt = resolveJunctionNextReconcileAt(account, now, addMilliseconds(now, reconcileIntervalMs));
|
|
458
552
|
return {
|
|
459
553
|
jobs: [
|
|
@@ -470,8 +564,8 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
470
564
|
nextReconcileAt,
|
|
471
565
|
};
|
|
472
566
|
}
|
|
473
|
-
function buildScheduledExtendedTimeseriesBackfillJobs(account, now) {
|
|
474
|
-
|
|
567
|
+
function buildScheduledExtendedTimeseriesBackfillJobs(account, now, context) {
|
|
568
|
+
const candidates = extendedBackfillTimeseriesResources.flatMap((resource) => {
|
|
475
569
|
const policy = resolveJunctionExtendedTimeseriesBackfillPolicy(resource);
|
|
476
570
|
if (!policy) {
|
|
477
571
|
return [];
|
|
@@ -479,11 +573,11 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
479
573
|
if (!canCurrentRuntimeMutateJunctionExtendedTimeseriesHistoryBackfillCoverage(account.metadata, resource, policy.version)) {
|
|
480
574
|
return [];
|
|
481
575
|
}
|
|
482
|
-
const scheduledSources =
|
|
576
|
+
const scheduledSources = [];
|
|
483
577
|
for (const sourceState of resolveJunctionAccountSourceStates(account.sources ?? [])) {
|
|
484
578
|
const identitySource = sourceState.identitySource;
|
|
485
579
|
const lifecycleSource = sourceState.lifecycleSource;
|
|
486
|
-
const sourceProviderSlug =
|
|
580
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(identitySource.sourceProviderSlug);
|
|
487
581
|
if (!sourceProviderSlug
|
|
488
582
|
|| !isDeviceSyncSourceAdmitted([lifecycleSource], lifecycleSource.sourceProviderSlug)
|
|
489
583
|
|| !isJunctionResourceAvailableInSummary(lifecycleSource.resourceAvailabilitySummary, resource)
|
|
@@ -491,33 +585,62 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
491
585
|
|| !canRepresentJunctionExtendedTimeseriesHistoryBackfillCoverage(account.metadata, sourceProviderSlug, resource, policy.version)) {
|
|
492
586
|
continue;
|
|
493
587
|
}
|
|
494
|
-
const
|
|
495
|
-
if (!
|
|
588
|
+
const firstSeenAt = identitySource.firstSeenAt;
|
|
589
|
+
if (!firstSeenAt) {
|
|
496
590
|
throw junctionSourceStateUnavailableError();
|
|
497
591
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
592
|
+
scheduledSources.push({
|
|
593
|
+
firstSeenAt,
|
|
594
|
+
lifecycleEpoch: readJunctionSourceLifecycleEpoch(lifecycleSource),
|
|
595
|
+
sourceProviderSlug,
|
|
596
|
+
});
|
|
502
597
|
}
|
|
503
|
-
return
|
|
598
|
+
return scheduledSources.map((source) => {
|
|
504
599
|
// Exact blood-pressure history is anchored to source admission. Other
|
|
505
600
|
// sparse resources catch up the current extended-history window once.
|
|
506
601
|
const window = buildExtendedTimeseriesBackfillWindow({
|
|
507
|
-
anchorAt: policy.anchor === "source_first_seen" ? firstSeenAt : now,
|
|
508
|
-
days:
|
|
602
|
+
anchorAt: policy.anchor === "source_first_seen" ? source.firstSeenAt : now,
|
|
603
|
+
days: JUNCTION_EXTENDED_TIMESERIES_BACKFILL_DAYS,
|
|
509
604
|
});
|
|
510
|
-
return
|
|
605
|
+
return {
|
|
606
|
+
anchor: policy.anchor,
|
|
511
607
|
availableAt: now,
|
|
512
|
-
|
|
608
|
+
historicalBackfillVersion: policy.version,
|
|
513
609
|
historicalWindowStart: window.windowStart,
|
|
514
610
|
resource,
|
|
515
|
-
|
|
611
|
+
sourceLifecycleEpoch: source.lifecycleEpoch,
|
|
612
|
+
sourceProviderSlug: source.sourceProviderSlug,
|
|
516
613
|
windowEnd: window.windowEnd,
|
|
517
614
|
windowStart: window.windowStart,
|
|
518
|
-
}
|
|
615
|
+
};
|
|
519
616
|
});
|
|
520
617
|
});
|
|
618
|
+
if (candidates.length === 0) {
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
candidates.sort((left, right) => left.resource.localeCompare(right.resource)
|
|
622
|
+
|| left.sourceProviderSlug.localeCompare(right.sourceProviderSlug));
|
|
623
|
+
const sourceFirstCandidates = candidates.filter((candidate) => candidate.anchor === "source_first_seen");
|
|
624
|
+
const scheduleTimeCandidates = candidates.filter((candidate) => candidate.anchor === "current_day");
|
|
625
|
+
const scheduleSlot = Math.floor(Date.parse(now) / reconcileIntervalMs);
|
|
626
|
+
const scheduleTimeJobs = scheduleTimeCandidates.map((candidate) => [candidate, buildExtendedTimeseriesBackfillJob(candidate)]);
|
|
627
|
+
const activeDedupeKeys = context?.findActiveDedupeKeys(scheduleTimeJobs.map(([, job]) => job.dedupeKey)) ?? new Set();
|
|
628
|
+
const inactiveJobs = scheduleTimeJobs.filter(([, job]) => !activeDedupeKeys.has(job.dedupeKey));
|
|
629
|
+
const reopenedInactiveJobs = context
|
|
630
|
+
? inactiveJobs.filter(([candidate]) => candidate.sourceLifecycleEpoch > 1)
|
|
631
|
+
: [];
|
|
632
|
+
const ordinaryInactiveJobs = context
|
|
633
|
+
? inactiveJobs.filter(([candidate]) => candidate.sourceLifecycleEpoch === 1)
|
|
634
|
+
: inactiveJobs;
|
|
635
|
+
const scheduledJob = selectJunctionScheduledHistoryCandidate({
|
|
636
|
+
ordinaryCandidates: ordinaryInactiveJobs,
|
|
637
|
+
reopenedCandidates: reopenedInactiveJobs,
|
|
638
|
+
scheduleSlot,
|
|
639
|
+
})?.[1];
|
|
640
|
+
return [
|
|
641
|
+
...sourceFirstCandidates.map((candidate) => buildExtendedTimeseriesBackfillJob(candidate)),
|
|
642
|
+
...(scheduledJob ? [scheduledJob] : []),
|
|
643
|
+
];
|
|
521
644
|
}
|
|
522
645
|
/**
|
|
523
646
|
* A dead push carrier cannot recover on its own and no pull can rediscover
|
|
@@ -640,11 +763,11 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
640
763
|
}
|
|
641
764
|
async function loadJunctionHistoricalPullSnapshot(context) {
|
|
642
765
|
try {
|
|
643
|
-
return await client.introspectHistoricalPull({
|
|
766
|
+
return await measureJunctionProviderRequest(context, "inventory", () => client.introspectHistoricalPull({
|
|
644
767
|
signal: context.signal ?? null,
|
|
645
768
|
userId: context.account.externalAccountId,
|
|
646
769
|
userLimit: 1,
|
|
647
|
-
});
|
|
770
|
+
}));
|
|
648
771
|
}
|
|
649
772
|
catch (error) {
|
|
650
773
|
if (context.signal?.aborted) {
|
|
@@ -664,7 +787,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
664
787
|
* cannot succeed.
|
|
665
788
|
*/
|
|
666
789
|
async function executePushSourceRecoveryJob(context, job) {
|
|
667
|
-
const sourceProviderSlug =
|
|
790
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(job.payload.sourceProviderSlug);
|
|
668
791
|
const silentSinceAt = normalizeString(job.payload.silentSinceAt);
|
|
669
792
|
if (!sourceProviderSlug || !silentSinceAt) {
|
|
670
793
|
return {};
|
|
@@ -715,10 +838,10 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
715
838
|
let endpointUnavailable = false;
|
|
716
839
|
let failureCode = null;
|
|
717
840
|
try {
|
|
718
|
-
({ endpointUnavailable } = await client.bulkTriggerHistoricalPull({
|
|
841
|
+
({ endpointUnavailable } = await measureJunctionProviderRequest(context, "inventory", () => client.bulkTriggerHistoricalPull({
|
|
719
842
|
sourceProviderSlug,
|
|
720
843
|
userIds: [context.account.externalAccountId],
|
|
721
|
-
}));
|
|
844
|
+
})));
|
|
722
845
|
}
|
|
723
846
|
catch (error) {
|
|
724
847
|
failureCode = isDeviceSyncError(error)
|
|
@@ -748,25 +871,36 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
748
871
|
}
|
|
749
872
|
async function executeJob(context, job) {
|
|
750
873
|
const skippedOptionalResources = [];
|
|
874
|
+
if (job.kind === JUNCTION_PUSH_SOURCE_RECOVERY_JOB_KIND) {
|
|
875
|
+
return executePushSourceRecoveryJob(context, job);
|
|
876
|
+
}
|
|
877
|
+
const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(job);
|
|
878
|
+
if (historicalProofAuthorization
|
|
879
|
+
&& !await isJunctionHistoricalProofAuthorizationCurrent(context, historicalProofAuthorization)) {
|
|
880
|
+
return {
|
|
881
|
+
nextReconcileAt: resolveJunctionNextReconcileAt(context.account, context.now, addMilliseconds(context.now, reconcileIntervalMs)),
|
|
882
|
+
};
|
|
883
|
+
}
|
|
751
884
|
const completedWorkoutStreamIdentities = readJunctionWorkoutStreamCompletedIdentities(job);
|
|
752
885
|
if (job.kind === "resource") {
|
|
753
886
|
return executeResourceJob(context, job, skippedOptionalResources, completedWorkoutStreamIdentities);
|
|
754
887
|
}
|
|
755
|
-
if (job.kind === JUNCTION_PUSH_SOURCE_RECOVERY_JOB_KIND) {
|
|
756
|
-
return executePushSourceRecoveryJob(context, job);
|
|
757
|
-
}
|
|
758
888
|
if (isFullJobTimeseriesContinuation(job)) {
|
|
759
889
|
return executeFullJobTimeseriesContinuation(context, job, skippedOptionalResources, completedWorkoutStreamIdentities);
|
|
760
890
|
}
|
|
761
891
|
const window = resolveJobWindow(job, context.now, job.kind === "backfill" ? summaryBackfillDays : reconcileDays);
|
|
892
|
+
const sourceProviderSlug = normalizeProviderSlug(job.payload.sourceProviderSlug);
|
|
762
893
|
const isConnectHistoricalBackfill = isConnectHistoricalBackfillWindow(context.account, window);
|
|
763
|
-
const
|
|
894
|
+
const listedSourceProviders = await measureJunctionProviderRequest(context, "inventory", () => client.listUserProviders(context.account.externalAccountId, {
|
|
764
895
|
...(job.kind === "reconcile" && context.shouldYield
|
|
765
896
|
? { collectionWorkLimit: JUNCTION_FULL_JOB_INVENTORY_COLLECTION_WORK_LIMIT }
|
|
766
897
|
: {}),
|
|
767
898
|
signal: context.signal ?? null,
|
|
768
|
-
});
|
|
769
|
-
await projectJunctionSources(context,
|
|
899
|
+
}));
|
|
900
|
+
await projectJunctionSources(context, listedSourceProviders);
|
|
901
|
+
const sourceProviders = sourceProviderSlug
|
|
902
|
+
? listedSourceProviders.filter((provider) => areJunctionProviderSlugsDataEquivalent(provider.origin.sourceProviderSlug ?? provider.slug, sourceProviderSlug))
|
|
903
|
+
: listedSourceProviders;
|
|
770
904
|
const isCurrentSummaryReconcile = job.kind === "reconcile" && isCurrentScheduledClosedWindow(window, context.now, reconcileDays);
|
|
771
905
|
const summaryWindow = isCurrentSummaryReconcile
|
|
772
906
|
? resolveCurrentSummaryWindow(context.now, reconcileDays)
|
|
@@ -791,9 +925,21 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
791
925
|
}
|
|
792
926
|
}
|
|
793
927
|
const summaryPhaseComplete = job.kind === "reconcile" && job.payload.summaryPhaseComplete === true;
|
|
794
|
-
const
|
|
795
|
-
? {}
|
|
796
|
-
: await fetchSummarySnapshots(context, summaryWindow.windowStart, summaryWindow.windowEnd, skippedOptionalResources, {
|
|
928
|
+
const summaryFetchResult = summaryPhaseComplete
|
|
929
|
+
? { providerRecordsSeen: false, snapshots: {} }
|
|
930
|
+
: await fetchSummarySnapshots(context, summaryWindow.windowStart, summaryWindow.windowEnd, skippedOptionalResources, {
|
|
931
|
+
dateQueryFormat: summaryDateQueryFormat,
|
|
932
|
+
sourceProviderSlug,
|
|
933
|
+
});
|
|
934
|
+
const { providerRecordsSeen: historicalSummaryHasFetchedRecords, snapshots: summaries, } = summaryFetchResult;
|
|
935
|
+
const sourceScopedHistoricalSummaryHasRecords = job.kind === "backfill" && sourceProviderSlug
|
|
936
|
+
? await hasJunctionSourceScopedAdmittedSnapshotRecords({
|
|
937
|
+
context,
|
|
938
|
+
snapshots: summaries,
|
|
939
|
+
sourceProviderSlug,
|
|
940
|
+
sourceProviders,
|
|
941
|
+
})
|
|
942
|
+
: false;
|
|
797
943
|
const profileSummaryResult = summaryPhaseComplete
|
|
798
944
|
? { checked: false, records: [] }
|
|
799
945
|
: await fetchProfileSummaryOnce(context, skippedOptionalResources);
|
|
@@ -818,23 +964,27 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
818
964
|
windowEnd: summaryWindow.windowEnd,
|
|
819
965
|
windowStart: summaryWindow.windowStart,
|
|
820
966
|
});
|
|
821
|
-
const historicalSummaryHasRecords = hasJunctionHistoricalBackfillSummaryRecords(
|
|
967
|
+
const historicalSummaryHasRecords = hasJunctionHistoricalBackfillSummaryRecords(sourceProviderSlug
|
|
968
|
+
? summaryNormalizationEvidence.filter((evidence) => areJunctionProviderSlugsDataEquivalent(evidence.sourceProviderSlug, sourceProviderSlug))
|
|
969
|
+
: summaryNormalizationEvidence);
|
|
822
970
|
const baseTimeseriesWindowStart = job.kind === "backfill"
|
|
823
971
|
? maxIsoTimestamp(window.windowStart, subtractDays(window.windowEnd, timeseriesBackfillDays))
|
|
824
972
|
: window.windowStart;
|
|
825
973
|
if (job.kind !== "backfill" || summaryHasFetchedRecords) {
|
|
826
|
-
await context
|
|
827
|
-
provider: "junction",
|
|
828
|
-
accountId: buildJunctionImportAccountId(context.account.externalAccountId),
|
|
829
|
-
connectionId: context.account.id,
|
|
974
|
+
await commitPreparedJunctionCanonicalImport(context, preparedSummaryImport, {
|
|
830
975
|
importedAt: summaryWindow.windowEnd,
|
|
831
976
|
windowStart: summaryWindow.windowStart,
|
|
832
977
|
windowEnd: summaryWindow.windowEnd,
|
|
833
|
-
connections: importConnections,
|
|
834
978
|
summaries: importSummaries,
|
|
835
979
|
timeseries: {},
|
|
836
|
-
});
|
|
837
|
-
}
|
|
980
|
+
}, context.now);
|
|
981
|
+
}
|
|
982
|
+
const historicalProviderRecordsSeen = sourceProviderSlug !== null
|
|
983
|
+
&& (job.payload.historicalProviderRecordsSeen === true
|
|
984
|
+
|| historicalSummaryHasFetchedRecords);
|
|
985
|
+
const historicalRecordsSeen = sourceProviderSlug !== null
|
|
986
|
+
&& (job.payload.historicalRecordsSeen === true
|
|
987
|
+
|| sourceScopedHistoricalSummaryHasRecords);
|
|
838
988
|
// Current records are durable before the optional historical-status probe.
|
|
839
989
|
// An unavailable introspection endpoint must not hold fresh ingestion hostage.
|
|
840
990
|
const historicalPullSnapshot = isConnectHistoricalBackfill && !context.shouldYield?.()
|
|
@@ -851,7 +1001,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
851
1001
|
const dailyTimeseriesResources = timeseriesResources.filter((resource) => JUNCTION_CALENDAR_DAY_AGGREGATE_RESOURCE_SET.has(resource));
|
|
852
1002
|
if (dailyTimeseriesResources.length > 0
|
|
853
1003
|
&& hourlyFidelityWindow) {
|
|
854
|
-
const timeseriesImport = await importTimeseriesDailyAggregateSnapshots(context, sourceProviders, hourlyFidelityWindow.windowStart, hourlyFidelityWindow.windowEnd, skippedOptionalResources, dailyTimeseriesResources);
|
|
1004
|
+
const timeseriesImport = await importTimeseriesDailyAggregateSnapshots(context, sourceProviders, hourlyFidelityWindow.windowStart, hourlyFidelityWindow.windowEnd, skippedOptionalResources, dailyTimeseriesResources, sourceProviderSlug);
|
|
855
1005
|
if (timeseriesImport.yieldedAt) {
|
|
856
1006
|
return withJunctionSkippedResourceMetadata(context, withJunctionMetadataPatch(buildYieldedJunctionJobResult({
|
|
857
1007
|
context,
|
|
@@ -873,7 +1023,15 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
873
1023
|
windowEnd: window.windowEnd,
|
|
874
1024
|
})
|
|
875
1025
|
: buildNonConnectHistoricalBackfillFollowUp({
|
|
876
|
-
hasRecords:
|
|
1026
|
+
hasRecords: sourceProviderSlug
|
|
1027
|
+
? historicalRecordsSeen
|
|
1028
|
+
: historicalSummaryHasRecords,
|
|
1029
|
+
...(sourceProviderSlug
|
|
1030
|
+
? {
|
|
1031
|
+
historicalProviderRecordsSeen,
|
|
1032
|
+
historicalRecordsSeen,
|
|
1033
|
+
}
|
|
1034
|
+
: {}),
|
|
877
1035
|
job,
|
|
878
1036
|
now: context.now,
|
|
879
1037
|
windowStart: window.windowStart,
|
|
@@ -911,32 +1069,128 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
911
1069
|
await markJunctionHistoricalReconnectRequired(context, historicalSummaryCoverage.reconnectProviderSlugs);
|
|
912
1070
|
}
|
|
913
1071
|
}
|
|
1072
|
+
const temporalResources = context.vaultTimeZone
|
|
1073
|
+
? timeseriesResources.filter((resource) => JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource))
|
|
1074
|
+
: [];
|
|
1075
|
+
const temporalRecoveryWindows = context.vaultTimeZone
|
|
1076
|
+
? buildLatestAuthoritativeDailyWindows({
|
|
1077
|
+
horizonDays: temporalReconcileDays,
|
|
1078
|
+
now: context.now,
|
|
1079
|
+
timeZone: context.vaultTimeZone,
|
|
1080
|
+
windowEnd: window.windowEnd,
|
|
1081
|
+
})
|
|
1082
|
+
: [];
|
|
1083
|
+
const newestTemporalWindow = temporalRecoveryWindows[0] ?? null;
|
|
1084
|
+
const deferredNewestTemporalResources = new Set();
|
|
1085
|
+
if (newestTemporalWindow && context.vaultTimeZone) {
|
|
1086
|
+
for (const [resourceIndex, resource] of temporalResources.entries()) {
|
|
1087
|
+
if (context.shouldYield?.()) {
|
|
1088
|
+
for (const remainingResource of temporalResources.slice(resourceIndex)) {
|
|
1089
|
+
deferredNewestTemporalResources.add(remainingResource);
|
|
1090
|
+
}
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
const skippedResourceCountBeforeFetch = skippedOptionalResources.length;
|
|
1094
|
+
try {
|
|
1095
|
+
await importJunctionTimeseriesResourceSnapshot({
|
|
1096
|
+
authorizedLocalDay: {
|
|
1097
|
+
dayKey: newestTemporalWindow.dayKey,
|
|
1098
|
+
timeZone: context.vaultTimeZone,
|
|
1099
|
+
},
|
|
1100
|
+
context,
|
|
1101
|
+
dateQueryFormat: "datetime",
|
|
1102
|
+
resource,
|
|
1103
|
+
skippedOptionalResources,
|
|
1104
|
+
sourceProviders,
|
|
1105
|
+
windowEnd: newestTemporalWindow.windowEnd,
|
|
1106
|
+
windowStart: newestTemporalWindow.windowStart,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
catch (error) {
|
|
1110
|
+
if (isRetryableDeviceSyncFailure(error)
|
|
1111
|
+
|| isJunctionJobSignalAbort(error, context.signal)) {
|
|
1112
|
+
deferredNewestTemporalResources.add(resource);
|
|
1113
|
+
if (isJunctionJobSignalAbort(error, context.signal)) {
|
|
1114
|
+
for (const remainingResource of temporalResources.slice(resourceIndex + 1)) {
|
|
1115
|
+
deferredNewestTemporalResources.add(remainingResource);
|
|
1116
|
+
}
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
throw error;
|
|
1122
|
+
}
|
|
1123
|
+
if (skippedOptionalResources.length > skippedResourceCountBeforeFetch) {
|
|
1124
|
+
deferredNewestTemporalResources.add(resource);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const temporalContinuationJobs = context.vaultTimeZone
|
|
1129
|
+
? [
|
|
1130
|
+
...(newestTemporalWindow && deferredNewestTemporalResources.size > 0
|
|
1131
|
+
? buildJunctionTemporalAuthorityJobs({
|
|
1132
|
+
now: context.now,
|
|
1133
|
+
resources: [...deferredNewestTemporalResources],
|
|
1134
|
+
timeZone: context.vaultTimeZone,
|
|
1135
|
+
windows: [newestTemporalWindow],
|
|
1136
|
+
})
|
|
1137
|
+
: []),
|
|
1138
|
+
...buildJunctionTemporalAuthorityJobs({
|
|
1139
|
+
now: context.now,
|
|
1140
|
+
resources: temporalResources,
|
|
1141
|
+
timeZone: context.vaultTimeZone,
|
|
1142
|
+
windows: temporalRecoveryWindows.slice(1),
|
|
1143
|
+
}),
|
|
1144
|
+
]
|
|
1145
|
+
: [];
|
|
914
1146
|
const nextReconcileAt = backfillFollowUp.nextReconcileAt ?? resolveJunctionNextReconcileAt(context.account, context.now, addMilliseconds(context.now, reconcileIntervalMs));
|
|
915
1147
|
const shouldScheduleTimeseries = timeseriesResources.length > 0
|
|
916
1148
|
&& (job.kind === "backfill"
|
|
917
1149
|
|| shouldImportClosedTimeseriesForReconcile(context.account.lastSyncCompletedAt, window.windowEnd));
|
|
918
|
-
const
|
|
1150
|
+
const historicalProofStillCurrent = !historicalProofAuthorization
|
|
1151
|
+
|| await isJunctionHistoricalProofAuthorizationCurrent(context, historicalProofAuthorization);
|
|
1152
|
+
const timeseriesContinuation = shouldScheduleTimeseries && historicalProofStillCurrent
|
|
919
1153
|
? buildFullJobTimeseriesContinuationJob({
|
|
920
1154
|
deferredEmptyBackfillAttempts: readDeferredEmptyBackfillAttempts(backfillFollowUp),
|
|
1155
|
+
historicalProviderRecordsSeen,
|
|
1156
|
+
historicalRecordsSeen,
|
|
921
1157
|
job,
|
|
922
1158
|
timeseriesCursor: baseTimeseriesWindowStart,
|
|
923
1159
|
timeseriesResourceCursor: timeseriesResources[0] ?? null,
|
|
924
1160
|
timeseriesWindowHours: 24,
|
|
925
1161
|
window,
|
|
926
1162
|
workoutStreamCursor: null,
|
|
1163
|
+
workoutStreamEmptySeen: false,
|
|
927
1164
|
})
|
|
928
1165
|
: null;
|
|
929
|
-
if (timeseriesContinuation) {
|
|
1166
|
+
if (timeseriesContinuation || temporalContinuationJobs.length > 0) {
|
|
930
1167
|
return withJunctionSkippedResourceMetadata(context, withJunctionMetadataPatch({
|
|
931
1168
|
...(backfillFollowUp.metadataPatch
|
|
932
1169
|
? { metadataPatch: backfillFollowUp.metadataPatch }
|
|
933
1170
|
: {}),
|
|
934
1171
|
nextReconcileAt,
|
|
935
|
-
scheduledJobs: [
|
|
1172
|
+
scheduledJobs: [
|
|
1173
|
+
...temporalContinuationJobs,
|
|
1174
|
+
...(timeseriesContinuation
|
|
1175
|
+
? [{ ...timeseriesContinuation, availableAt: context.now }]
|
|
1176
|
+
: []),
|
|
1177
|
+
],
|
|
936
1178
|
}, profileMetadataPatch), skippedOptionalResources);
|
|
937
1179
|
}
|
|
1180
|
+
if (job.kind === "backfill"
|
|
1181
|
+
&& historicalProofStillCurrent
|
|
1182
|
+
&& !backfillFollowUp.scheduledJobs?.length) {
|
|
1183
|
+
await completeSourceScopedHistoricalBackfill({
|
|
1184
|
+
context,
|
|
1185
|
+
historicalProviderRecordsSeen,
|
|
1186
|
+
historicalRecordsSeen,
|
|
1187
|
+
job,
|
|
1188
|
+
sourceProviderSlug,
|
|
1189
|
+
sourceProviders,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
938
1192
|
return withJunctionSkippedResourceMetadata(context, withJunctionMetadataPatch({
|
|
939
|
-
...backfillFollowUp,
|
|
1193
|
+
...(historicalProofStillCurrent ? backfillFollowUp : {}),
|
|
940
1194
|
nextReconcileAt,
|
|
941
1195
|
}, profileMetadataPatch), skippedOptionalResources);
|
|
942
1196
|
}
|
|
@@ -949,7 +1203,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
949
1203
|
: [];
|
|
950
1204
|
const existingByInstanceKey = new Map(existingSources.map((source) => [source.sourceInstanceKey, source]));
|
|
951
1205
|
const recoveryProviderSlugs = new Set(pendingProviderSlugs
|
|
952
|
-
.map(
|
|
1206
|
+
.map(canonicalizeJunctionProviderSlug)
|
|
953
1207
|
.filter((providerSlug) => providerSlug !== null));
|
|
954
1208
|
for (const providerSlug of recoveryProviderSlugs) {
|
|
955
1209
|
if (!isJunctionHistoricalResetProviderSlug(providerSlug)) {
|
|
@@ -962,7 +1216,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
962
1216
|
if (!projectedSourceInstanceKey) {
|
|
963
1217
|
continue;
|
|
964
1218
|
}
|
|
965
|
-
const existing = existingByInstanceKey.get(projectedSourceInstanceKey) ?? existingSources.find((source) =>
|
|
1219
|
+
const existing = existingByInstanceKey.get(projectedSourceInstanceKey) ?? existingSources.find((source) => canonicalizeJunctionProviderSlug(source.sourceProviderSlug) === providerSlug);
|
|
966
1220
|
if (existing?.status === "error"
|
|
967
1221
|
&& existing.lastErrorCode === DEVICE_SYNC_HISTORICAL_DATA_RECONNECT_REQUIRED_ERROR_CODE) {
|
|
968
1222
|
continue;
|
|
@@ -989,7 +1243,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
989
1243
|
if (!webhookDataJson) {
|
|
990
1244
|
return null;
|
|
991
1245
|
}
|
|
992
|
-
if (!
|
|
1246
|
+
if (!isJunctionInlineImportJob({
|
|
993
1247
|
kind: job.kind,
|
|
994
1248
|
payload: job.payload,
|
|
995
1249
|
})) {
|
|
@@ -1345,7 +1599,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1345
1599
|
const calendarRefreshDay = readJunctionSparseCalendarRefreshDay(job);
|
|
1346
1600
|
const resource = normalizeJunctionResourceName(job.payload.resource);
|
|
1347
1601
|
const resourceCategory = normalizeString(job.payload.resourceCategory);
|
|
1348
|
-
const sourceProviderSlug =
|
|
1602
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(job.payload.sourceProviderSlug);
|
|
1349
1603
|
if (!calendarRefreshDay
|
|
1350
1604
|
&& sourceProviderSlug
|
|
1351
1605
|
&& !isJunctionSourceAdmittedForImport(context.account.sources ?? [], sourceProviderSlug)
|
|
@@ -1360,9 +1614,9 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1360
1614
|
if (listedSourceProviders) {
|
|
1361
1615
|
return listedSourceProviders;
|
|
1362
1616
|
}
|
|
1363
|
-
const sourceProviders = await client.listUserProviders(context.account.externalAccountId, {
|
|
1617
|
+
const sourceProviders = await measureJunctionProviderRequest(context, "inventory", () => client.listUserProviders(context.account.externalAccountId, {
|
|
1364
1618
|
signal: context.signal ?? null,
|
|
1365
|
-
});
|
|
1619
|
+
}));
|
|
1366
1620
|
listedSourceProviders = sourceProviders;
|
|
1367
1621
|
return sourceProviders;
|
|
1368
1622
|
};
|
|
@@ -1436,6 +1690,63 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1436
1690
|
}, skippedOptionalResources);
|
|
1437
1691
|
}
|
|
1438
1692
|
const summaries = {};
|
|
1693
|
+
const temporalAuthorityJob = readJunctionTemporalAuthorityJob(job);
|
|
1694
|
+
if (temporalAuthorityJob) {
|
|
1695
|
+
if (context.shouldYield?.()) {
|
|
1696
|
+
throw deviceSyncError({
|
|
1697
|
+
code: "JUNCTION_TEMPORAL_AUTHORITY_JOB_YIELDED",
|
|
1698
|
+
message: "Junction temporal catch-up yielded before collection.",
|
|
1699
|
+
retryable: true,
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
if (!context.vaultTimeZone) {
|
|
1703
|
+
throw deviceSyncError({
|
|
1704
|
+
code: "JUNCTION_TEMPORAL_AUTHORITY_TIMEZONE_UNAVAILABLE",
|
|
1705
|
+
message: "Junction temporal catch-up requires the vault timezone.",
|
|
1706
|
+
retryable: true,
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
if (context.vaultTimeZone !== temporalAuthorityJob.timeZone) {
|
|
1710
|
+
return {};
|
|
1711
|
+
}
|
|
1712
|
+
const expectedWindow = resolveVaultLocalDayWindow(temporalAuthorityJob.dayKey, temporalAuthorityJob.timeZone);
|
|
1713
|
+
if (!expectedWindow
|
|
1714
|
+
|| expectedWindow.windowStart !== window.windowStart
|
|
1715
|
+
|| expectedWindow.windowEnd !== window.windowEnd
|
|
1716
|
+
|| Date.parse(expectedWindow.windowEnd) + JUNCTION_TEMPORAL_AUTHORITY_LAG_MS
|
|
1717
|
+
> Date.parse(context.now)) {
|
|
1718
|
+
throw deviceSyncError({
|
|
1719
|
+
code: "JUNCTION_TEMPORAL_AUTHORITY_JOB_INVALID",
|
|
1720
|
+
message: "Junction temporal catch-up job did not describe an eligible complete local day.",
|
|
1721
|
+
retryable: false,
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
const skippedResourceCountBeforeFetch = skippedOptionalResources.length;
|
|
1725
|
+
const sourceProviders = await loadAndProjectSourceProviders();
|
|
1726
|
+
await importJunctionTimeseriesResourceSnapshot({
|
|
1727
|
+
authorizedLocalDay: {
|
|
1728
|
+
dayKey: temporalAuthorityJob.dayKey,
|
|
1729
|
+
timeZone: temporalAuthorityJob.timeZone,
|
|
1730
|
+
},
|
|
1731
|
+
context,
|
|
1732
|
+
dateQueryFormat: "datetime",
|
|
1733
|
+
resource: temporalAuthorityJob.resource,
|
|
1734
|
+
skippedOptionalResources,
|
|
1735
|
+
sourceProviders,
|
|
1736
|
+
windowEnd: expectedWindow.windowEnd,
|
|
1737
|
+
windowStart: expectedWindow.windowStart,
|
|
1738
|
+
});
|
|
1739
|
+
if (skippedOptionalResources.length > skippedResourceCountBeforeFetch) {
|
|
1740
|
+
throw deviceSyncError({
|
|
1741
|
+
code: "JUNCTION_TEMPORAL_AUTHORITY_RESOURCE_UNAVAILABLE",
|
|
1742
|
+
message: "Junction temporal catch-up resource was unavailable.",
|
|
1743
|
+
retryable: true,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return {
|
|
1747
|
+
nextReconcileAt: clampWebhookJobNextReconcileAt(context),
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1439
1750
|
if (resource) {
|
|
1440
1751
|
const directInput = readJunctionDirectResourceJobInput(job, window);
|
|
1441
1752
|
if (directInput) {
|
|
@@ -1522,6 +1833,13 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1522
1833
|
retryable: false,
|
|
1523
1834
|
});
|
|
1524
1835
|
}
|
|
1836
|
+
const sourceLifecycleEpoch = extendedHistoricalPolicy
|
|
1837
|
+
? readSafeInteger(job.payload.sourceLifecycleEpoch)
|
|
1838
|
+
: null;
|
|
1839
|
+
if (extendedHistoricalPolicy
|
|
1840
|
+
&& (sourceLifecycleEpoch === null || sourceLifecycleEpoch < 1)) {
|
|
1841
|
+
return {};
|
|
1842
|
+
}
|
|
1525
1843
|
const historicalWindowStart = toIsoTimestampIfValid(normalizeString(job.payload.historicalWindowStart))
|
|
1526
1844
|
?? window.windowStart;
|
|
1527
1845
|
const dailyAggregateRetryAttempts = readHistoricalBackfillJobEmptyAttempts(job);
|
|
@@ -1529,12 +1847,20 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1529
1847
|
let sourceIdentityAuthority;
|
|
1530
1848
|
let currentSourceAdmission = "admitted";
|
|
1531
1849
|
try {
|
|
1850
|
+
if (extendedHistoricalBackfill
|
|
1851
|
+
&& sourceProviderSlug
|
|
1852
|
+
&& sourceLifecycleEpoch !== null) {
|
|
1853
|
+
currentSourceAdmission = await resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug, sourceLifecycleEpoch ?? undefined);
|
|
1854
|
+
if (currentSourceAdmission === "fenced") {
|
|
1855
|
+
return {};
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1532
1858
|
sourceProviders = await loadAndProjectSourceProviders();
|
|
1533
1859
|
if (extendedHistoricalBackfill && sourceProviderSlug) {
|
|
1534
1860
|
sourceIdentityAuthority = context.listConnectionSources
|
|
1535
1861
|
? await context.listConnectionSources()
|
|
1536
1862
|
: context.account.sources ?? [];
|
|
1537
|
-
currentSourceAdmission = resolveJunctionCurrentSourceAdmissionFromSources(sourceIdentityAuthority, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only");
|
|
1863
|
+
currentSourceAdmission = resolveJunctionCurrentSourceAdmissionFromSources(sourceIdentityAuthority, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only", sourceLifecycleEpoch ?? undefined);
|
|
1538
1864
|
}
|
|
1539
1865
|
}
|
|
1540
1866
|
catch (error) {
|
|
@@ -1608,7 +1934,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1608
1934
|
window,
|
|
1609
1935
|
});
|
|
1610
1936
|
}
|
|
1611
|
-
if (extendedHistoricalPolicy
|
|
1937
|
+
if (requiresJunctionHistoricalPullReadiness(extendedHistoricalPolicy)
|
|
1612
1938
|
&& window.windowStart === historicalWindowStart) {
|
|
1613
1939
|
const historicalPullReadiness = resolveJunctionHistoricalPullReadiness({
|
|
1614
1940
|
resource: effectiveResource,
|
|
@@ -1643,12 +1969,8 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1643
1969
|
const retryDelayMs = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS.at(-1) ?? 0;
|
|
1644
1970
|
return {
|
|
1645
1971
|
nextReconcileAt: clampWebhookJobNextReconcileAt(context),
|
|
1646
|
-
scheduledJobs: [
|
|
1972
|
+
scheduledJobs: [buildExtendedTimeseriesBackfillFollowUp(job, {
|
|
1647
1973
|
availableAt: addMilliseconds(context.now, retryDelayMs),
|
|
1648
|
-
dedupeKey: job.dedupeKey,
|
|
1649
|
-
historicalWindowStart,
|
|
1650
|
-
resource: effectiveResource,
|
|
1651
|
-
sourceProviderSlug,
|
|
1652
1974
|
windowEnd: window.windowEnd,
|
|
1653
1975
|
windowStart: window.windowStart,
|
|
1654
1976
|
})],
|
|
@@ -1674,7 +1996,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1674
1996
|
if (timeseriesPolicy?.historyWindow === "dense_timeseries"
|
|
1675
1997
|
&& (timeseriesPolicy.enabledByDefault === false
|
|
1676
1998
|
|| JUNCTION_CLOSED_DAY_TIMESERIES_RESOURCES.has(effectiveResource))) {
|
|
1677
|
-
const dailyImport = await importTimeseriesDailySnapshots(context, sourceProviders, window.windowStart, window.windowEnd, skippedOptionalResources, effectiveResource, sourceProviderSlug, completedWorkoutStreamIdentities);
|
|
1999
|
+
const dailyImport = await importTimeseriesDailySnapshots(context, sourceProviders, window.windowStart, window.windowEnd, skippedOptionalResources, effectiveResource, sourceProviderSlug, completedWorkoutStreamIdentities, job.payload.workoutStreamEmptySeen === true);
|
|
1678
2000
|
const result = withJunctionSkippedResourceMetadata(context, dailyImport.yieldedAt
|
|
1679
2001
|
? buildYieldedJunctionJobResult({
|
|
1680
2002
|
context,
|
|
@@ -1684,9 +2006,41 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1684
2006
|
workoutStreamCursor: effectiveResource === "workout_stream"
|
|
1685
2007
|
? dailyImport.workoutStreamCursor
|
|
1686
2008
|
: undefined,
|
|
2009
|
+
workoutStreamEmptySeen: dailyImport.workoutStreamEmptySeen,
|
|
2010
|
+
})
|
|
2011
|
+
: { nextReconcileAt: clampWebhookJobNextReconcileAt(context) }, skippedOptionalResources);
|
|
2012
|
+
return withJunctionWorkoutStreamEmptyReplay({
|
|
2013
|
+
job,
|
|
2014
|
+
now: context.now,
|
|
2015
|
+
replayWindow: dailyImport.emptyWorkoutStreamReplayWindow,
|
|
2016
|
+
result,
|
|
2017
|
+
sourceProviderSlug,
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
if (!extendedHistoricalBackfill
|
|
2021
|
+
&& timeseriesPolicy?.maxCanonicalRecordsPerWindow !== undefined) {
|
|
2022
|
+
const [boundedWindow] = buildPreciseTimeseriesWindows(window.windowStart, window.windowEnd);
|
|
2023
|
+
if (!boundedWindow) {
|
|
2024
|
+
return withJunctionSkippedResourceMetadata(context, { nextReconcileAt: clampWebhookJobNextReconcileAt(context) }, skippedOptionalResources);
|
|
2025
|
+
}
|
|
2026
|
+
await importJunctionTimeseriesResourceSnapshot({
|
|
2027
|
+
context,
|
|
2028
|
+
dateQueryFormat: "datetime",
|
|
2029
|
+
resource: effectiveResource,
|
|
2030
|
+
skippedOptionalResources,
|
|
2031
|
+
sourceProviderSlug,
|
|
2032
|
+
sourceProviders,
|
|
2033
|
+
windowEnd: boundedWindow.windowEnd,
|
|
2034
|
+
windowStart: boundedWindow.windowStart,
|
|
2035
|
+
});
|
|
2036
|
+
return withJunctionSkippedResourceMetadata(context, Date.parse(boundedWindow.windowEnd) < Date.parse(window.windowEnd)
|
|
2037
|
+
? buildYieldedJunctionJobResult({
|
|
2038
|
+
context,
|
|
2039
|
+
job,
|
|
2040
|
+
windowEnd: window.windowEnd,
|
|
2041
|
+
windowStart: boundedWindow.windowEnd,
|
|
1687
2042
|
})
|
|
1688
2043
|
: { nextReconcileAt: clampWebhookJobNextReconcileAt(context) }, skippedOptionalResources);
|
|
1689
|
-
return result;
|
|
1690
2044
|
}
|
|
1691
2045
|
const timeseriesImport = await importTimeseriesPreciseSnapshots(context, sourceProviders, window.windowStart, window.windowEnd, skippedOptionalResources, [effectiveResource], sourceProviderSlug, {
|
|
1692
2046
|
dateQueryFormat: extendedHistoricalPolicy?.completion === "daily_aggregate"
|
|
@@ -1695,6 +2049,9 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1695
2049
|
historicalProviderRecordsSeen: job.payload.historicalProviderRecordsSeen === true,
|
|
1696
2050
|
preservePartialRetryableFailure: extendedHistoricalBackfill,
|
|
1697
2051
|
...(sourceIdentityAuthority ? { sourceIdentityAuthority } : {}),
|
|
2052
|
+
sourceLifecycleEpoch: extendedHistoricalBackfill
|
|
2053
|
+
? sourceLifecycleEpoch ?? undefined
|
|
2054
|
+
: undefined,
|
|
1698
2055
|
sourceStatusRequirement: extendedHistoricalBackfill
|
|
1699
2056
|
? "connected"
|
|
1700
2057
|
: undefined,
|
|
@@ -1732,7 +2089,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1732
2089
|
const historicalRecordsSeen = extendedHistoricalBackfill
|
|
1733
2090
|
? job.payload.historicalRecordsSeen === true
|
|
1734
2091
|
|| timeseriesImport.canonicalEventCount > 0
|
|
1735
|
-
|| (effectiveResource
|
|
2092
|
+
|| (doesJunctionImportReceiptResolveDeliveredRows(effectiveResource)
|
|
1736
2093
|
&& timeseriesImport.providerRecordsExamined)
|
|
1737
2094
|
: undefined;
|
|
1738
2095
|
const dailyAggregateNeedsRetry = extendedHistoricalPolicy?.completion === "daily_aggregate"
|
|
@@ -1744,14 +2101,12 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1744
2101
|
const retryDelayMs = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS[retryAttempts - 1] ?? 0;
|
|
1745
2102
|
return finalizePreciseImportResult(withJunctionSkippedResourceMetadata(context, {
|
|
1746
2103
|
nextReconcileAt: clampWebhookJobNextReconcileAt(context),
|
|
1747
|
-
scheduledJobs: [
|
|
2104
|
+
scheduledJobs: [buildExtendedTimeseriesBackfillFollowUp(job, {
|
|
1748
2105
|
availableAt: addMilliseconds(context.now, retryDelayMs),
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
resource: effectiveResource,
|
|
1754
|
-
sourceProviderSlug,
|
|
2106
|
+
payload: {
|
|
2107
|
+
emptyBackfillAttempts: retryAttempts,
|
|
2108
|
+
historicalRecordsSeen,
|
|
2109
|
+
},
|
|
1755
2110
|
windowEnd: window.windowEnd,
|
|
1756
2111
|
windowStart: window.windowStart,
|
|
1757
2112
|
})],
|
|
@@ -1759,7 +2114,11 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1759
2114
|
}
|
|
1760
2115
|
const historicalUnresolvedProviderRecords = extendedHistoricalBackfill
|
|
1761
2116
|
&& extendedHistoricalPolicy?.completion !== "daily_aggregate"
|
|
1762
|
-
?
|
|
2117
|
+
? isJunctionBodyTimeseriesResource(effectiveResource)
|
|
2118
|
+
&& timeseriesImport.fetchComplete
|
|
2119
|
+
&& timeseriesImport.unresolvedProviderRecordCount === 0
|
|
2120
|
+
? { identities: [], withoutStableIdentity: false }
|
|
2121
|
+
: resolveJunctionHistoricalUnresolvedProviderRecords(job, timeseriesImport)
|
|
1763
2122
|
: undefined;
|
|
1764
2123
|
const historicalUnresolvedProviderRecordCount = historicalUnresolvedProviderRecords === undefined
|
|
1765
2124
|
? undefined
|
|
@@ -1789,7 +2148,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1789
2148
|
const result = withJunctionSkippedResourceMetadata(context, {
|
|
1790
2149
|
nextReconcileAt: clampWebhookJobNextReconcileAt(context),
|
|
1791
2150
|
}, skippedOptionalResources);
|
|
1792
|
-
const historicalPullReadiness = extendedHistoricalPolicy
|
|
2151
|
+
const historicalPullReadiness = requiresJunctionHistoricalPullReadiness(extendedHistoricalPolicy)
|
|
1793
2152
|
&& timeseriesImport.fetchComplete
|
|
1794
2153
|
? resolveJunctionHistoricalPullReadiness({
|
|
1795
2154
|
resource: effectiveResource,
|
|
@@ -1797,6 +2156,13 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1797
2156
|
sourceProviderSlug,
|
|
1798
2157
|
})
|
|
1799
2158
|
: undefined;
|
|
2159
|
+
if (extendedHistoricalBackfill
|
|
2160
|
+
&& extendedHistoricalPolicy?.anchor === "current_day"
|
|
2161
|
+
&& sourceProviderSlug
|
|
2162
|
+
&& sourceLifecycleEpoch !== null
|
|
2163
|
+
&& await resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug, sourceLifecycleEpoch) !== "admitted") {
|
|
2164
|
+
return {};
|
|
2165
|
+
}
|
|
1800
2166
|
return finalizePreciseImportResult(withJunctionHistoricalCoverageVerification(context, job, window, withJunctionExtendedTimeseriesBackfillFollowUp({
|
|
1801
2167
|
context,
|
|
1802
2168
|
historicalPullReadiness,
|
|
@@ -1820,17 +2186,13 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1820
2186
|
}
|
|
1821
2187
|
const sourceProviders = await loadAndProjectSourceProviders();
|
|
1822
2188
|
const preparedImport = await prepareJunctionImportSnapshot(context, summaries, sourceProviders);
|
|
1823
|
-
await context
|
|
1824
|
-
provider: "junction",
|
|
1825
|
-
accountId: buildJunctionImportAccountId(context.account.externalAccountId),
|
|
1826
|
-
connectionId: context.account.id,
|
|
2189
|
+
await commitPreparedJunctionCanonicalImport(context, preparedImport, {
|
|
1827
2190
|
importedAt: context.now,
|
|
1828
2191
|
windowStart: window.windowStart,
|
|
1829
2192
|
windowEnd: window.windowEnd,
|
|
1830
|
-
connections: preparedImport.connections,
|
|
1831
2193
|
summaries: preparedImport.snapshots,
|
|
1832
2194
|
timeseries: {},
|
|
1833
|
-
});
|
|
2195
|
+
}, context.now);
|
|
1834
2196
|
return withJunctionHistoricalCoverageVerification(context, job, window, withJunctionSkippedResourceMetadata(context, {
|
|
1835
2197
|
nextReconcileAt: clampWebhookJobNextReconcileAt(context),
|
|
1836
2198
|
}, skippedOptionalResources));
|
|
@@ -1881,14 +2243,14 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1881
2243
|
if (!policy) {
|
|
1882
2244
|
return input.result;
|
|
1883
2245
|
}
|
|
1884
|
-
const sourceProviderSlug =
|
|
2246
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.job.payload.sourceProviderSlug);
|
|
1885
2247
|
const historicalWindowStart = toIsoTimestampIfValid(normalizeString(input.job.payload.historicalWindowStart))
|
|
1886
2248
|
?? input.window.windowStart;
|
|
1887
2249
|
const recordsSeen = input.job.payload.historicalRecordsSeen === true
|
|
1888
2250
|
|| input.importResult.canonicalEventCount > 0
|
|
1889
|
-
|| (input.resource
|
|
2251
|
+
|| (doesJunctionImportReceiptResolveDeliveredRows(input.resource)
|
|
1890
2252
|
&& input.importResult.providerRecordsExamined);
|
|
1891
|
-
const unresolvedProviderRecords = input.resource
|
|
2253
|
+
const unresolvedProviderRecords = isJunctionBodyTimeseriesResource(input.resource)
|
|
1892
2254
|
&& input.importResult.fetchComplete
|
|
1893
2255
|
&& input.importResult.unresolvedProviderRecordCount === 0
|
|
1894
2256
|
? { identities: [], withoutStableIdentity: false }
|
|
@@ -1896,22 +2258,20 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1896
2258
|
const unresolvedProviderRecordCount = countJunctionHistoricalUnresolvedProviderRecords(unresolvedProviderRecords);
|
|
1897
2259
|
const unresolvedProviderRecordIdentitiesJson = encodeJunctionHistoricalUnresolvedProviderRecords(unresolvedProviderRecords);
|
|
1898
2260
|
const unresolvedProviderRecordsSeen = unresolvedProviderRecordCount > 0;
|
|
1899
|
-
if (policy
|
|
2261
|
+
if (requiresJunctionHistoricalPullReadiness(policy) && input.importResult.fetchComplete) {
|
|
1900
2262
|
const historicalPullReadiness = input.historicalPullReadiness ?? "unavailable";
|
|
1901
2263
|
const currentReconcileWindowStart = floorUtcDayTimestamp(subtractDays(floorUtcDayTimestamp(input.context.now), reconcileDays));
|
|
1902
2264
|
if (historicalPullReadiness === "pending"
|
|
1903
|
-
|| (
|
|
2265
|
+
|| (policy.completion === "daily_aggregate"
|
|
2266
|
+
&& historicalPullReadiness === "unavailable"
|
|
2267
|
+
&& !recordsSeen)) {
|
|
1904
2268
|
const retryDelayMs = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS.at(-1) ?? 0;
|
|
1905
2269
|
return {
|
|
1906
2270
|
...input.result,
|
|
1907
2271
|
scheduledJobs: [
|
|
1908
2272
|
...(input.result.scheduledJobs ?? []),
|
|
1909
|
-
|
|
2273
|
+
buildExtendedTimeseriesBackfillFollowUp(input.job, {
|
|
1910
2274
|
availableAt: addMilliseconds(input.context.now, retryDelayMs),
|
|
1911
|
-
dedupeKey: input.job.dedupeKey,
|
|
1912
|
-
historicalWindowStart,
|
|
1913
|
-
resource: input.resource,
|
|
1914
|
-
sourceProviderSlug,
|
|
1915
2275
|
windowEnd: maxIsoTimestamp(input.window.windowEnd, currentReconcileWindowStart),
|
|
1916
2276
|
windowStart: historicalWindowStart,
|
|
1917
2277
|
}),
|
|
@@ -1923,13 +2283,9 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1923
2283
|
...input.result,
|
|
1924
2284
|
scheduledJobs: [
|
|
1925
2285
|
...(input.result.scheduledJobs ?? []),
|
|
1926
|
-
|
|
2286
|
+
buildExtendedTimeseriesBackfillFollowUp(input.job, {
|
|
1927
2287
|
availableAt: input.context.now,
|
|
1928
|
-
|
|
1929
|
-
historicalRecordsSeen: recordsSeen,
|
|
1930
|
-
historicalWindowStart,
|
|
1931
|
-
resource: input.resource,
|
|
1932
|
-
sourceProviderSlug,
|
|
2288
|
+
payload: { historicalRecordsSeen: recordsSeen },
|
|
1933
2289
|
windowEnd: currentReconcileWindowStart,
|
|
1934
2290
|
windowStart: input.window.windowEnd,
|
|
1935
2291
|
}),
|
|
@@ -1939,7 +2295,10 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1939
2295
|
if (historicalPullReadiness === "terminal_failure") {
|
|
1940
2296
|
return input.result;
|
|
1941
2297
|
}
|
|
1942
|
-
|
|
2298
|
+
if (policy.completion === "daily_aggregate"
|
|
2299
|
+
|| historicalPullReadiness !== "unavailable") {
|
|
2300
|
+
return withJunctionMetadataPatch(input.result, buildJunctionExtendedTimeseriesBackfillCompletionMetadataPatch(input.context, input.job, input.resource, sourceProviderSlug));
|
|
2301
|
+
}
|
|
1943
2302
|
}
|
|
1944
2303
|
// A complete note scan can close its one-time source coverage even when
|
|
1945
2304
|
// no action-like tags exist. Neutral wearable-tag notes, including a
|
|
@@ -1958,7 +2317,8 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1958
2317
|
let retryDelayMs = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS[emptyBackfillAttempts - 1]
|
|
1959
2318
|
?? null;
|
|
1960
2319
|
if (retryDelayMs === null
|
|
1961
|
-
&& (!input.importResult.fetchComplete
|
|
2320
|
+
&& (!input.importResult.fetchComplete
|
|
2321
|
+
|| unresolvedProviderRecords.identities.length > 0)) {
|
|
1962
2322
|
emptyBackfillAttempts = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS.length;
|
|
1963
2323
|
retryDelayMs =
|
|
1964
2324
|
EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS[emptyBackfillAttempts - 1]
|
|
@@ -1971,22 +2331,15 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
1971
2331
|
...input.result,
|
|
1972
2332
|
scheduledJobs: [
|
|
1973
2333
|
...(input.result.scheduledJobs ?? []),
|
|
1974
|
-
|
|
2334
|
+
buildExtendedTimeseriesBackfillFollowUp(input.job, {
|
|
1975
2335
|
availableAt: addMilliseconds(input.context.now, retryDelayMs),
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
historicalProviderRecordsSeen: unresolvedProviderRecordsSeen,
|
|
1984
|
-
historicalRecordsSeen: recordsSeen,
|
|
1985
|
-
historicalUnresolvedProviderRecordIdentitiesJson: unresolvedProviderRecordIdentitiesJson,
|
|
1986
|
-
historicalUnresolvedProviderRecordCount: unresolvedProviderRecordCount,
|
|
1987
|
-
historicalWindowStart,
|
|
1988
|
-
resource: input.resource,
|
|
1989
|
-
sourceProviderSlug,
|
|
2336
|
+
payload: {
|
|
2337
|
+
emptyBackfillAttempts,
|
|
2338
|
+
historicalProviderRecordsSeen: unresolvedProviderRecordsSeen,
|
|
2339
|
+
historicalRecordsSeen: recordsSeen,
|
|
2340
|
+
historicalUnresolvedProviderRecordIdentitiesJson: unresolvedProviderRecordIdentitiesJson,
|
|
2341
|
+
historicalUnresolvedProviderRecordCount: unresolvedProviderRecordCount || undefined,
|
|
2342
|
+
},
|
|
1990
2343
|
windowEnd: input.window.windowEnd,
|
|
1991
2344
|
windowStart: input.importResult.fetchComplete
|
|
1992
2345
|
? historicalWindowStart
|
|
@@ -2006,9 +2359,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2006
2359
|
if (!canCurrentRuntimeMutateJunctionExtendedTimeseriesHistoryBackfillCoverage(context.account.metadata, resource, policy.version)) {
|
|
2007
2360
|
return {};
|
|
2008
2361
|
}
|
|
2009
|
-
const admittedVersion =
|
|
2010
|
-
? readJunctionNoteHistoryBackfillVersion(job.payload)
|
|
2011
|
-
: policy.version;
|
|
2362
|
+
const admittedVersion = readJunctionHistoricalBackfillVersion(job.payload);
|
|
2012
2363
|
if (admittedVersion !== policy.version) {
|
|
2013
2364
|
// A semantic generation can only certify its own coverage. Older jobs
|
|
2014
2365
|
// may still import facts without closing the current generation's bit.
|
|
@@ -2070,7 +2421,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2070
2421
|
const acceptedCanonicalDelivery = importResult.durableDeliveryAccepted
|
|
2071
2422
|
&& providerSlug !== null
|
|
2072
2423
|
&& importResult.normalizationEvidence.some((entry) => entry.resource === directInput.resource
|
|
2073
|
-
&&
|
|
2424
|
+
&& canonicalizeJunctionProviderSlug(entry.sourceProviderSlug) === providerSlug);
|
|
2074
2425
|
if (!canCurrentRuntimeMutateJunctionHistoricalBackfillProgress(context.account.metadata)
|
|
2075
2426
|
|| !directHistoricalWindow
|
|
2076
2427
|
|| !acceptedCanonicalDelivery
|
|
@@ -2124,19 +2475,29 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2124
2475
|
const data = readPlainObject(verified.payload[JUNCTION_WEBHOOK_ROOT_FIELDS.data]);
|
|
2125
2476
|
const externalAccountSelection = requireJunctionWebhookUserIdSelection(verified.payload, data);
|
|
2126
2477
|
const resource = inferJunctionWebhookResource(eventType, data);
|
|
2127
|
-
const
|
|
2478
|
+
const envelopeSourceProviderSlug = extractJunctionWebhookSourceProviderSlug(data);
|
|
2128
2479
|
const objectId = extractJunctionWebhookObjectId(data);
|
|
2129
|
-
const eventOccurredAt = extractJunctionWebhookOccurredAt(data);
|
|
2480
|
+
const eventOccurredAt = extractJunctionWebhookOccurredAt(data, resource);
|
|
2130
2481
|
const occurredAt = eventOccurredAt ?? context.now;
|
|
2131
2482
|
const window = buildJunctionWebhookWindow(data, occurredAt, context.now, resource);
|
|
2483
|
+
const historicalPullCompleted = isJunctionHistoricalDataEvent(eventType)
|
|
2484
|
+
&& data !== null
|
|
2485
|
+
&& isJunctionHistoricalPullCompletedWebhookData(data, externalAccountSelection.userId);
|
|
2486
|
+
const dataSourceProviderSlug = resolveJunctionWebhookDataSourceProviderSlug({
|
|
2487
|
+
data,
|
|
2488
|
+
envelopeSourceProviderSlug,
|
|
2489
|
+
eventType,
|
|
2490
|
+
historicalPullCompleted,
|
|
2491
|
+
});
|
|
2132
2492
|
const webhookDataJsons = buildJunctionWebhookDataJobJsons({
|
|
2133
2493
|
data,
|
|
2134
2494
|
eventType,
|
|
2135
2495
|
externalAccountId: externalAccountSelection.userId,
|
|
2136
2496
|
resource,
|
|
2137
2497
|
summaryResources,
|
|
2138
|
-
sourceProviderSlug,
|
|
2498
|
+
sourceProviderSlug: dataSourceProviderSlug,
|
|
2139
2499
|
});
|
|
2500
|
+
const sourceProviderSlug = dataSourceProviderSlug ?? envelopeSourceProviderSlug;
|
|
2140
2501
|
const jobs = buildJunctionWebhookJobs({
|
|
2141
2502
|
eventType,
|
|
2142
2503
|
objectId,
|
|
@@ -2160,18 +2521,14 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2160
2521
|
// A historical-pull completion is a data-less notification, so accepting
|
|
2161
2522
|
// its fetch job proves nothing arrived. Treating it as delivery would
|
|
2162
2523
|
// refresh the arrival signal and hide the very stall this detects.
|
|
2163
|
-
dataSourceProviderSlug
|
|
2164
|
-
&& !(isJunctionHistoricalDataEvent(eventType)
|
|
2165
|
-
&& data !== null
|
|
2166
|
-
&& isJunctionHistoricalPullCompletedWebhookData(data, externalAccountSelection.userId))
|
|
2167
|
-
? sourceProviderSlug
|
|
2168
|
-
: null,
|
|
2524
|
+
dataSourceProviderSlug,
|
|
2169
2525
|
jobs,
|
|
2170
2526
|
unknownAccountAction: "accept",
|
|
2171
2527
|
};
|
|
2172
2528
|
}
|
|
2173
2529
|
async function fetchSummarySnapshots(context, windowStart, windowEnd, skippedOptionalResources, options = {}) {
|
|
2174
2530
|
const snapshots = {};
|
|
2531
|
+
let providerRecordsSeen = false;
|
|
2175
2532
|
for (const resource of summaryResources) {
|
|
2176
2533
|
if (isJunctionProfileSummaryResource(resource)) {
|
|
2177
2534
|
continue;
|
|
@@ -2179,6 +2536,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2179
2536
|
const request = {
|
|
2180
2537
|
resource,
|
|
2181
2538
|
signal: context.signal ?? null,
|
|
2539
|
+
sourceProviderSlug: options.sourceProviderSlug,
|
|
2182
2540
|
userId: context.account.externalAccountId,
|
|
2183
2541
|
windowStart,
|
|
2184
2542
|
windowEnd,
|
|
@@ -2186,9 +2544,11 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2186
2544
|
if (options.dateQueryFormat) {
|
|
2187
2545
|
request.dateQueryFormat = options.dateQueryFormat;
|
|
2188
2546
|
}
|
|
2189
|
-
|
|
2547
|
+
const records = await fetchOptionalJunctionResourceRecords(context, "summary", resource, skippedOptionalResources, () => client.listSummary(request));
|
|
2548
|
+
providerRecordsSeen ||= records.length > 0;
|
|
2549
|
+
snapshots[resource] = scopeJunctionRecordsToSourceProvider(records, options.sourceProviderSlug);
|
|
2190
2550
|
}
|
|
2191
|
-
return snapshots;
|
|
2551
|
+
return { providerRecordsSeen, snapshots };
|
|
2192
2552
|
}
|
|
2193
2553
|
async function executeBoundedSummaryReconcile(input) {
|
|
2194
2554
|
const profileAlreadyChecked = hasCheckedJunctionProfileSummary(input.context.account.metadata);
|
|
@@ -2239,17 +2599,13 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2239
2599
|
}
|
|
2240
2600
|
}
|
|
2241
2601
|
const preparedSummaryImport = await prepareJunctionImportSnapshot(input.context, summaries, input.sourceProviders);
|
|
2242
|
-
await input.context
|
|
2243
|
-
provider: "junction",
|
|
2244
|
-
accountId: buildJunctionImportAccountId(input.context.account.externalAccountId),
|
|
2245
|
-
connectionId: input.context.account.id,
|
|
2602
|
+
await commitPreparedJunctionCanonicalImport(input.context, preparedSummaryImport, {
|
|
2246
2603
|
importedAt: input.summaryWindow.windowEnd,
|
|
2247
2604
|
windowStart: input.summaryWindow.windowStart,
|
|
2248
2605
|
windowEnd: input.summaryWindow.windowEnd,
|
|
2249
|
-
connections: preparedSummaryImport.connections,
|
|
2250
2606
|
summaries: preparedSummaryImport.snapshots,
|
|
2251
2607
|
timeseries: {},
|
|
2252
|
-
});
|
|
2608
|
+
}, input.context.now);
|
|
2253
2609
|
const nextResource = eligibleUnits[cursorIndex >= 0 ? cursorIndex + 1 : 1]?.[0] ?? null;
|
|
2254
2610
|
const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
|
|
2255
2611
|
const continuation = buildExactWindowJob({
|
|
@@ -2287,12 +2643,14 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2287
2643
|
async function fetchTimeseriesSnapshots(context, windowStart, windowEnd, skippedOptionalResources, resources = timeseriesResources, sourceProviderSlug, options = {}) {
|
|
2288
2644
|
const snapshots = {};
|
|
2289
2645
|
for (const resource of resources) {
|
|
2290
|
-
|
|
2646
|
+
const fetched = await fetchTimeseriesResourceInChunks(context, resource, windowStart, windowEnd, skippedOptionalResources, sourceProviderSlug, options);
|
|
2647
|
+
snapshots[resource] = fetched.records;
|
|
2291
2648
|
}
|
|
2292
2649
|
return snapshots;
|
|
2293
2650
|
}
|
|
2294
2651
|
async function fetchTimeseriesResourceInChunks(context, resource, windowStart, windowEnd, skippedOptionalResources, sourceProviderSlug, options = {}) {
|
|
2295
2652
|
const records = [];
|
|
2653
|
+
let providerRecordsSeen = false;
|
|
2296
2654
|
let chunkStart = Date.parse(windowStart);
|
|
2297
2655
|
const end = Date.parse(windowEnd);
|
|
2298
2656
|
let optionalFailureLogged = false;
|
|
@@ -2317,8 +2675,9 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2317
2675
|
if (options.requireStructurallyCompleteCollection) {
|
|
2318
2676
|
request.requireStructurallyCompleteCollection = true;
|
|
2319
2677
|
}
|
|
2320
|
-
const chunkRecords = await fetchJunctionTimeseriesWindow(client, request);
|
|
2321
|
-
|
|
2678
|
+
const chunkRecords = await measureJunctionProviderRequest(context, "resource", () => fetchJunctionTimeseriesWindow(client, request));
|
|
2679
|
+
providerRecordsSeen ||= chunkRecords.length > 0;
|
|
2680
|
+
records.push(...filterJunctionTimeseriesRecordsToWindow(resource, chunkRecords, chunkWindowStart, chunkWindowEnd, options.dateQueryFormat, options.authorizedLocalDay, context.vaultTimeZone));
|
|
2322
2681
|
}
|
|
2323
2682
|
catch (error) {
|
|
2324
2683
|
const failure = classifyOptionalJunctionResourceFailure(error, "timeseries", resource, context.account.externalAccountId);
|
|
@@ -2339,9 +2698,15 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2339
2698
|
chunkStart = chunkEnd;
|
|
2340
2699
|
}
|
|
2341
2700
|
if (resource === "electrocardiogram_voltage" || resource === "workout_stream") {
|
|
2342
|
-
return
|
|
2701
|
+
return {
|
|
2702
|
+
providerRecordsSeen,
|
|
2703
|
+
records: resolveJunctionBoundedFeatureRecords(resource, records),
|
|
2704
|
+
};
|
|
2343
2705
|
}
|
|
2344
|
-
return
|
|
2706
|
+
return {
|
|
2707
|
+
providerRecordsSeen,
|
|
2708
|
+
records: dedupeJunctionTimeseriesRecords(resource, records),
|
|
2709
|
+
};
|
|
2345
2710
|
}
|
|
2346
2711
|
async function importTimeseriesPreciseSnapshots(context, sourceProviders, windowStart, windowEnd, skippedOptionalResources, resources, sourceProviderSlug, options = {}) {
|
|
2347
2712
|
const accumulatedTimeseries = {};
|
|
@@ -2370,9 +2735,8 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2370
2735
|
const skippedResourceCountBeforeFetch = skippedOptionalResources.length;
|
|
2371
2736
|
let timeseries;
|
|
2372
2737
|
try {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
};
|
|
2738
|
+
const fetched = await fetchTimeseriesResourceInChunks(context, resource, window.windowStart, window.windowEnd, skippedOptionalResources, sourceProviderSlug, { dateQueryFormat: options.dateQueryFormat ?? "datetime" });
|
|
2739
|
+
timeseries = { [resource]: fetched.records };
|
|
2376
2740
|
}
|
|
2377
2741
|
catch (error) {
|
|
2378
2742
|
if (options.preservePartialRetryableFailure === true
|
|
@@ -2410,7 +2774,8 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2410
2774
|
let unresolvedProviderRecordCount = providerRecordCount;
|
|
2411
2775
|
let unresolvedProviderRecordsWithoutStableIdentity = providerRecordCount > 0;
|
|
2412
2776
|
const requiresBloodPressureRecordResolution = resources.includes("blood_pressure");
|
|
2413
|
-
const
|
|
2777
|
+
const successfulImportTerminatesDeliveredRows = resources.length === 1
|
|
2778
|
+
&& doesJunctionImportReceiptResolveDeliveredRows(resources[0] ?? "");
|
|
2414
2779
|
if (executionWindowStart && executionWindowEnd) {
|
|
2415
2780
|
const identifyFetchedProviderRecords = (sourceIdentities) => requiresBloodPressureRecordResolution && providerRecordCount > 0
|
|
2416
2781
|
? identifyJunctionBloodPressureProviderRecords({
|
|
@@ -2442,7 +2807,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2442
2807
|
const importSourceIdentities = resolveJunctionAccountSourceIdentities(currentSources);
|
|
2443
2808
|
if (sourceProviderSlug
|
|
2444
2809
|
&& options.sourceStatusRequirement === "connected") {
|
|
2445
|
-
postFetchSourceAdmission = resolveJunctionCurrentSourceAdmissionFromSources(currentSources, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only");
|
|
2810
|
+
postFetchSourceAdmission = resolveJunctionCurrentSourceAdmissionFromSources(currentSources, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only", options.sourceLifecycleEpoch);
|
|
2446
2811
|
if (postFetchSourceAdmission !== "admitted") {
|
|
2447
2812
|
return {
|
|
2448
2813
|
acceptedProviderRecordCount: 0,
|
|
@@ -2477,18 +2842,14 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2477
2842
|
windowStart: executionWindowStart,
|
|
2478
2843
|
});
|
|
2479
2844
|
if (hasJunctionSnapshotRecords(preparedImport.snapshots)) {
|
|
2480
|
-
const receipt = await context
|
|
2481
|
-
provider: "junction",
|
|
2482
|
-
accountId: buildJunctionImportAccountId(context.account.externalAccountId),
|
|
2483
|
-
connectionId: context.account.id,
|
|
2845
|
+
const receipt = await commitPreparedJunctionCanonicalImport(context, preparedImport, {
|
|
2484
2846
|
importedAt: executionWindowEnd,
|
|
2485
2847
|
windowStart: executionWindowStart,
|
|
2486
2848
|
windowEnd: executionWindowEnd,
|
|
2487
2849
|
timeseriesWindowKind: "precise",
|
|
2488
|
-
connections: preparedImport.connections,
|
|
2489
2850
|
summaries: {},
|
|
2490
2851
|
timeseries: preparedImport.snapshots,
|
|
2491
|
-
});
|
|
2852
|
+
}, context.now);
|
|
2492
2853
|
providerRecordsExamined = true;
|
|
2493
2854
|
canonicalEventCount = readProviderSnapshotCanonicalEventCount(receipt);
|
|
2494
2855
|
canonicalEventDayKeys = readProviderSnapshotCanonicalEventDayKeys(receipt);
|
|
@@ -2510,11 +2871,11 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2510
2871
|
unresolvedProviderRecordIdentities.length
|
|
2511
2872
|
+ (unresolvedProviderRecordsWithoutStableIdentity ? 1 : 0);
|
|
2512
2873
|
}
|
|
2513
|
-
else if (
|
|
2514
|
-
// A resolved import receipt means the
|
|
2515
|
-
// every delivered row. Valid readings became canonical
|
|
2516
|
-
// deterministic validation rejects are terminal;
|
|
2517
|
-
// throw before this branch and
|
|
2874
|
+
else if (successfulImportTerminatesDeliveredRows) {
|
|
2875
|
+
// A resolved exact-record import receipt means the canonical owner
|
|
2876
|
+
// examined every delivered row. Valid readings became canonical
|
|
2877
|
+
// events and deterministic validation rejects are terminal;
|
|
2878
|
+
// delivery errors throw before this branch and remain retryable.
|
|
2518
2879
|
unresolvedProviderRecordCount = 0;
|
|
2519
2880
|
unresolvedProviderRecordsWithoutStableIdentity = false;
|
|
2520
2881
|
}
|
|
@@ -2524,6 +2885,10 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2524
2885
|
unresolvedProviderRecordsWithoutStableIdentity = unresolvedProviderRecordCount > 0;
|
|
2525
2886
|
}
|
|
2526
2887
|
}
|
|
2888
|
+
if (sourceProviderSlug
|
|
2889
|
+
&& options.sourceStatusRequirement === "connected") {
|
|
2890
|
+
postFetchSourceAdmission = await resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug, options.sourceLifecycleEpoch);
|
|
2891
|
+
}
|
|
2527
2892
|
}
|
|
2528
2893
|
catch (error) {
|
|
2529
2894
|
if (options.preservePartialRetryableFailure === true
|
|
@@ -2637,10 +3002,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2637
3002
|
: [],
|
|
2638
3003
|
});
|
|
2639
3004
|
if (hasJunctionSnapshotRecords(preparedImport.snapshots)) {
|
|
2640
|
-
const receipt = await context
|
|
2641
|
-
provider: "junction",
|
|
2642
|
-
accountId: buildJunctionImportAccountId(context.account.externalAccountId),
|
|
2643
|
-
connectionId: context.account.id,
|
|
3005
|
+
const receipt = await commitPreparedJunctionCanonicalImport(context, preparedImport, {
|
|
2644
3006
|
importedAt: window.windowEnd,
|
|
2645
3007
|
windowStart: window.windowStart,
|
|
2646
3008
|
windowEnd: window.windowEnd,
|
|
@@ -2649,16 +3011,17 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2649
3011
|
? {
|
|
2650
3012
|
strictSparseCalendarRepair: {
|
|
2651
3013
|
dayKey: window.windowStart.slice(0, 10),
|
|
2652
|
-
resource: windowResources[0],
|
|
2653
3014
|
...emptySparseCalendarSource,
|
|
3015
|
+
// Calendar-refresh admission already proved this member of
|
|
3016
|
+
// the fixed sparse resource set before collection began.
|
|
3017
|
+
resource: windowResources[0],
|
|
2654
3018
|
sourceProviderSlug: normalizeJunctionSourceProviderSlug(emptySparseCalendarSource.sourceProviderSlug) ?? emptySparseCalendarSource.sourceProviderSlug,
|
|
2655
3019
|
},
|
|
2656
3020
|
}
|
|
2657
3021
|
: {}),
|
|
2658
|
-
connections: preparedImport.connections,
|
|
2659
3022
|
summaries: {},
|
|
2660
3023
|
timeseries: preparedImport.snapshots,
|
|
2661
|
-
});
|
|
3024
|
+
}, context.now);
|
|
2662
3025
|
for (const resourceId of readProviderSnapshotCanonicalEventExternalRefResourceIds(receipt) ?? []) {
|
|
2663
3026
|
appliedDailyAggregateResourceIds.add(resourceId);
|
|
2664
3027
|
}
|
|
@@ -2669,7 +3032,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2669
3032
|
yieldedAt: null,
|
|
2670
3033
|
};
|
|
2671
3034
|
}
|
|
2672
|
-
async function importTimeseriesDailySnapshots(context, sourceProviders, windowStart, windowEnd, skippedOptionalResources, resource, sourceProviderSlug, completedWorkoutStreamIdentities = new Set()) {
|
|
3035
|
+
async function importTimeseriesDailySnapshots(context, sourceProviders, windowStart, windowEnd, skippedOptionalResources, resource, sourceProviderSlug, completedWorkoutStreamIdentities = new Set(), workoutStreamEmptySeen = false) {
|
|
2673
3036
|
let resumeWorkoutStreamIdentities = new Set(completedWorkoutStreamIdentities);
|
|
2674
3037
|
let madeProgress = false;
|
|
2675
3038
|
for (const window of buildClosedDailyWindows(windowStart, windowEnd)) {
|
|
@@ -2680,6 +3043,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2680
3043
|
if (madeProgress) {
|
|
2681
3044
|
return {
|
|
2682
3045
|
workoutStreamCursor,
|
|
3046
|
+
workoutStreamEmptySeen,
|
|
2683
3047
|
yieldedAt: window.windowStart,
|
|
2684
3048
|
};
|
|
2685
3049
|
}
|
|
@@ -2695,20 +3059,37 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2695
3059
|
sourceProviders,
|
|
2696
3060
|
windowEnd: window.windowEnd,
|
|
2697
3061
|
windowStart: window.windowStart,
|
|
3062
|
+
workoutStreamEmptySeen,
|
|
2698
3063
|
});
|
|
2699
3064
|
workoutStreamCursor = workoutImport.workoutStreamCursor;
|
|
3065
|
+
workoutStreamEmptySeen = workoutImport.emptyTimestampArraySeen;
|
|
2700
3066
|
madeProgress ||= workoutImport.madeProgress;
|
|
2701
3067
|
if (workoutImport.yieldedAt) {
|
|
2702
3068
|
return {
|
|
2703
3069
|
workoutStreamCursor,
|
|
3070
|
+
workoutStreamEmptySeen,
|
|
2704
3071
|
yieldedAt: workoutImport.yieldedAt,
|
|
2705
3072
|
};
|
|
2706
3073
|
}
|
|
3074
|
+
if (workoutStreamEmptySeen) {
|
|
3075
|
+
return {
|
|
3076
|
+
emptyWorkoutStreamReplayWindow: {
|
|
3077
|
+
windowEnd: window.windowEnd,
|
|
3078
|
+
windowStart: window.windowStart,
|
|
3079
|
+
},
|
|
3080
|
+
workoutStreamCursor: null,
|
|
3081
|
+
workoutStreamEmptySeen: false,
|
|
3082
|
+
yieldedAt: window.windowEnd,
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
2707
3085
|
madeProgress = true;
|
|
2708
3086
|
}
|
|
2709
3087
|
else {
|
|
2710
3088
|
try {
|
|
2711
3089
|
await importJunctionTimeseriesResourceSnapshot({
|
|
3090
|
+
collectionWorkLimit: resource === "electrocardiogram_voltage"
|
|
3091
|
+
? JUNCTION_FULL_JOB_TIMESERIES_COLLECTION_WORK_LIMIT
|
|
3092
|
+
: undefined,
|
|
2712
3093
|
context,
|
|
2713
3094
|
dateQueryFormat: "date",
|
|
2714
3095
|
resource,
|
|
@@ -2724,6 +3105,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2724
3105
|
if (madeProgress) {
|
|
2725
3106
|
return {
|
|
2726
3107
|
workoutStreamCursor,
|
|
3108
|
+
workoutStreamEmptySeen: false,
|
|
2727
3109
|
yieldedAt: window.windowStart,
|
|
2728
3110
|
};
|
|
2729
3111
|
}
|
|
@@ -2741,6 +3123,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2741
3123
|
}
|
|
2742
3124
|
return {
|
|
2743
3125
|
workoutStreamCursor: null,
|
|
3126
|
+
workoutStreamEmptySeen: false,
|
|
2744
3127
|
yieldedAt: null,
|
|
2745
3128
|
};
|
|
2746
3129
|
}
|
|
@@ -2766,15 +3149,33 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2766
3149
|
: persistedTimeseriesWindowHours;
|
|
2767
3150
|
const resource = resourceCursor.resource;
|
|
2768
3151
|
const policy = resolveJunctionTimeseriesResourcePolicy(resource);
|
|
3152
|
+
const sourceProviderSlug = normalizeProviderSlug(job.payload.sourceProviderSlug);
|
|
3153
|
+
let historicalProviderRecordsSeen = sourceProviderSlug !== null
|
|
3154
|
+
&& job.payload.historicalProviderRecordsSeen === true;
|
|
3155
|
+
let historicalRecordsSeen = sourceProviderSlug !== null
|
|
3156
|
+
&& job.payload.historicalRecordsSeen === true;
|
|
2769
3157
|
if ((timeseriesWindowHours === 1
|
|
2770
3158
|
&& policy?.normalizationMode !== "hourly_or_session_feature")
|
|
2771
3159
|
|| (!resourceCursor.restartFromWindowStart
|
|
2772
3160
|
&& resource !== "workout_stream"
|
|
2773
|
-
&& job.payload.workoutStreamCursor !== undefined
|
|
3161
|
+
&& (job.payload.workoutStreamCursor !== undefined
|
|
3162
|
+
|| job.payload.workoutStreamEmptySeen !== undefined))) {
|
|
2774
3163
|
throw invalidJunctionTimeseriesResourceProgress();
|
|
2775
3164
|
}
|
|
3165
|
+
const listedSourceProviders = sourceProviderSlug || resource === "workout_stream"
|
|
3166
|
+
? await measureJunctionProviderRequest(context, "inventory", () => client.listUserProviders(context.account.externalAccountId, {
|
|
3167
|
+
...(!sourceProviderSlug && resource === "workout_stream"
|
|
3168
|
+
? { collectionWorkLimit: JUNCTION_FULL_JOB_INVENTORY_COLLECTION_WORK_LIMIT }
|
|
3169
|
+
: {}),
|
|
3170
|
+
signal: context.signal ?? null,
|
|
3171
|
+
}))
|
|
3172
|
+
: [];
|
|
3173
|
+
const sourceProviders = sourceProviderSlug
|
|
3174
|
+
? listedSourceProviders.filter((provider) => areJunctionProviderSlugsDataEquivalent(provider.origin.sourceProviderSlug ?? provider.slug, sourceProviderSlug))
|
|
3175
|
+
: listedSourceProviders;
|
|
2776
3176
|
const executionWindowEnd = new Date(Math.min(Date.parse(timeseriesCursor) + timeseriesWindowHours * TIMESERIES_HOUR_MS, Date.parse(window.windowEnd))).toISOString();
|
|
2777
3177
|
let workoutStreamCursor = null;
|
|
3178
|
+
let workoutStreamReplayWindow;
|
|
2778
3179
|
if (resource === "workout_stream") {
|
|
2779
3180
|
try {
|
|
2780
3181
|
const workoutImport = await importJunctionWorkoutStreamWindow({
|
|
@@ -2783,41 +3184,69 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2783
3184
|
completedIdentities: completedWorkoutStreamIdentities,
|
|
2784
3185
|
context,
|
|
2785
3186
|
skippedOptionalResources,
|
|
2786
|
-
|
|
3187
|
+
sourceProviderSlug,
|
|
3188
|
+
sourceProviders,
|
|
2787
3189
|
windowEnd: executionWindowEnd,
|
|
2788
3190
|
windowStart: timeseriesCursor,
|
|
3191
|
+
workoutStreamEmptySeen: job.payload.workoutStreamEmptySeen === true,
|
|
2789
3192
|
});
|
|
3193
|
+
historicalProviderRecordsSeen ||=
|
|
3194
|
+
sourceProviderSlug !== null
|
|
3195
|
+
&& workoutImport.historicalProviderRecordsSeen === true;
|
|
3196
|
+
historicalRecordsSeen ||=
|
|
3197
|
+
sourceProviderSlug !== null
|
|
3198
|
+
&& workoutImport.historicalRecordsSeen === true;
|
|
2790
3199
|
workoutStreamCursor = workoutImport.workoutStreamCursor;
|
|
2791
3200
|
if (workoutImport.yieldedAt) {
|
|
2792
3201
|
return buildFullJobTimeseriesContinuationResult({
|
|
2793
3202
|
context,
|
|
3203
|
+
historicalProviderRecordsSeen,
|
|
3204
|
+
historicalRecordsSeen,
|
|
2794
3205
|
job,
|
|
2795
3206
|
skippedOptionalResources,
|
|
3207
|
+
sourceProviders,
|
|
2796
3208
|
continuation: {
|
|
2797
3209
|
timeseriesCursor,
|
|
2798
3210
|
timeseriesResourceCursor: resource,
|
|
2799
3211
|
timeseriesWindowHours,
|
|
2800
3212
|
workoutStreamCursor,
|
|
3213
|
+
workoutStreamEmptySeen: workoutImport.emptyTimestampArraySeen,
|
|
2801
3214
|
},
|
|
2802
3215
|
window,
|
|
2803
3216
|
});
|
|
2804
3217
|
}
|
|
3218
|
+
workoutStreamReplayWindow = workoutImport.emptyTimestampArraySeen
|
|
3219
|
+
? {
|
|
3220
|
+
windowEnd: executionWindowEnd,
|
|
3221
|
+
windowStart: timeseriesCursor,
|
|
3222
|
+
}
|
|
3223
|
+
: undefined;
|
|
2805
3224
|
}
|
|
2806
3225
|
catch (error) {
|
|
2807
3226
|
if (error instanceof JunctionTimeseriesProgressError) {
|
|
3227
|
+
historicalProviderRecordsSeen ||=
|
|
3228
|
+
sourceProviderSlug !== null
|
|
3229
|
+
&& error.historicalProviderRecordsSeen;
|
|
3230
|
+
historicalRecordsSeen ||=
|
|
3231
|
+
sourceProviderSlug !== null
|
|
3232
|
+
&& error.historicalRecordsSeen;
|
|
2808
3233
|
if (!error.workoutStreamCursor
|
|
2809
3234
|
|| error.workoutStreamCursor === job.payload.workoutStreamCursor) {
|
|
2810
3235
|
throw error.failure;
|
|
2811
3236
|
}
|
|
2812
3237
|
return buildFullJobTimeseriesContinuationResult({
|
|
2813
3238
|
context,
|
|
3239
|
+
historicalProviderRecordsSeen,
|
|
3240
|
+
historicalRecordsSeen,
|
|
2814
3241
|
job,
|
|
2815
3242
|
skippedOptionalResources,
|
|
3243
|
+
sourceProviders,
|
|
2816
3244
|
continuation: {
|
|
2817
3245
|
timeseriesCursor,
|
|
2818
3246
|
timeseriesResourceCursor: resource,
|
|
2819
3247
|
timeseriesWindowHours,
|
|
2820
3248
|
workoutStreamCursor: error.workoutStreamCursor,
|
|
3249
|
+
workoutStreamEmptySeen: error.workoutStreamEmptySeen,
|
|
2821
3250
|
},
|
|
2822
3251
|
window,
|
|
2823
3252
|
});
|
|
@@ -2838,16 +3267,23 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2838
3267
|
windowStart: timeseriesCursor,
|
|
2839
3268
|
};
|
|
2840
3269
|
if (Date.parse(importWindow.windowStart) < Date.parse(importWindow.windowEnd)) {
|
|
2841
|
-
await importJunctionTimeseriesResourceSnapshot({
|
|
3270
|
+
const timeseriesImport = await importJunctionTimeseriesResourceSnapshot({
|
|
2842
3271
|
collectionWorkLimit: JUNCTION_FULL_JOB_TIMESERIES_COLLECTION_WORK_LIMIT,
|
|
2843
3272
|
context,
|
|
2844
3273
|
dateQueryFormat: timeseriesWindowHours === 1 ? "datetime" : "date",
|
|
2845
3274
|
resource,
|
|
2846
3275
|
skippedOptionalResources,
|
|
2847
|
-
|
|
3276
|
+
sourceProviderSlug,
|
|
3277
|
+
sourceProviders,
|
|
2848
3278
|
windowEnd: importWindow.windowEnd,
|
|
2849
3279
|
windowStart: importWindow.windowStart,
|
|
2850
3280
|
});
|
|
3281
|
+
historicalProviderRecordsSeen ||=
|
|
3282
|
+
sourceProviderSlug !== null
|
|
3283
|
+
&& timeseriesImport.historicalProviderRecordsSeen === true;
|
|
3284
|
+
historicalRecordsSeen ||=
|
|
3285
|
+
sourceProviderSlug !== null
|
|
3286
|
+
&& timeseriesImport.historicalRecordsSeen === true;
|
|
2851
3287
|
}
|
|
2852
3288
|
}
|
|
2853
3289
|
catch (error) {
|
|
@@ -2856,13 +3292,17 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2856
3292
|
&& policy?.normalizationMode === "hourly_or_session_feature") {
|
|
2857
3293
|
return buildFullJobTimeseriesContinuationResult({
|
|
2858
3294
|
context,
|
|
3295
|
+
historicalProviderRecordsSeen,
|
|
3296
|
+
historicalRecordsSeen,
|
|
2859
3297
|
job,
|
|
2860
3298
|
skippedOptionalResources,
|
|
3299
|
+
sourceProviders,
|
|
2861
3300
|
continuation: {
|
|
2862
3301
|
timeseriesCursor,
|
|
2863
3302
|
timeseriesResourceCursor: resource,
|
|
2864
3303
|
timeseriesWindowHours: 1,
|
|
2865
3304
|
workoutStreamCursor: null,
|
|
3305
|
+
workoutStreamEmptySeen: false,
|
|
2866
3306
|
},
|
|
2867
3307
|
window,
|
|
2868
3308
|
});
|
|
@@ -2872,8 +3312,12 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2872
3312
|
}
|
|
2873
3313
|
return buildFullJobTimeseriesContinuationResult({
|
|
2874
3314
|
context,
|
|
3315
|
+
historicalProviderRecordsSeen,
|
|
3316
|
+
historicalRecordsSeen,
|
|
2875
3317
|
job,
|
|
3318
|
+
replayWindow: workoutStreamReplayWindow,
|
|
2876
3319
|
skippedOptionalResources,
|
|
3320
|
+
sourceProviders,
|
|
2877
3321
|
continuation: resolveNextFullJobTimeseriesContinuation({
|
|
2878
3322
|
baseTimeseriesWindowStart,
|
|
2879
3323
|
executionWindowEnd,
|
|
@@ -2887,38 +3331,110 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2887
3331
|
}
|
|
2888
3332
|
async function importJunctionTimeseriesResourceSnapshot(input) {
|
|
2889
3333
|
const calendarDayAggregate = JUNCTION_CALENDAR_DAY_AGGREGATE_RESOURCE_SET.has(input.resource);
|
|
2890
|
-
|
|
3334
|
+
// Temporal-authority fetches carry complete vault-local-day authority with
|
|
3335
|
+
// their own conservative closure lag; the provider calendar-day closure
|
|
3336
|
+
// gate owns only ordinary calendar-day imports.
|
|
3337
|
+
if (calendarDayAggregate && !input.authorizedLocalDay) {
|
|
2891
3338
|
const windowEndMs = Date.parse(input.windowEnd);
|
|
2892
3339
|
const globallyClosedProviderDayEnd = resolveGloballyClosedProviderDayEnd(input.windowEnd, input.context.now);
|
|
2893
3340
|
if (!Number.isFinite(windowEndMs)
|
|
2894
3341
|
|| !Number.isFinite(globallyClosedProviderDayEnd)
|
|
2895
3342
|
|| windowEndMs > globallyClosedProviderDayEnd) {
|
|
2896
|
-
return;
|
|
3343
|
+
return { yieldedAt: null };
|
|
2897
3344
|
}
|
|
2898
3345
|
}
|
|
2899
|
-
const
|
|
3346
|
+
const skippedResourceCountBeforeFetch = input.skippedOptionalResources.length;
|
|
3347
|
+
const sourceLifecycleFence = resolveJunctionTimeseriesResourcePolicy(input.resource)
|
|
3348
|
+
?.maxCanonicalRecordsPerWindow !== undefined
|
|
3349
|
+
&& input.context.listConnectionSources
|
|
3350
|
+
? captureJunctionSourceLifecycleFence(input.context.account.sources ?? [])
|
|
3351
|
+
: null;
|
|
3352
|
+
const fetched = await fetchTimeseriesResourceInChunks(input.context, input.resource, input.windowStart, input.windowEnd, input.skippedOptionalResources, input.sourceProviderSlug, {
|
|
3353
|
+
...(input.authorizedLocalDay
|
|
3354
|
+
? {
|
|
3355
|
+
authorizedLocalDay: input.authorizedLocalDay,
|
|
3356
|
+
// A lossy successful response must never be certified as an
|
|
3357
|
+
// authoritative empty or partial complete-day replacement.
|
|
3358
|
+
requireStructurallyCompleteCollection: true,
|
|
3359
|
+
}
|
|
3360
|
+
: {}),
|
|
2900
3361
|
collectionWorkLimit: input.collectionWorkLimit,
|
|
2901
3362
|
dateQueryFormat: input.dateQueryFormat,
|
|
2902
3363
|
});
|
|
2903
|
-
|
|
2904
|
-
|
|
3364
|
+
const { providerRecordsSeen } = fetched;
|
|
3365
|
+
const records = scopeJunctionRecordsToSourceProvider(fetched.records, input.sourceProviderSlug);
|
|
3366
|
+
if (input.skippedOptionalResources.length > skippedResourceCountBeforeFetch) {
|
|
3367
|
+
return {
|
|
3368
|
+
historicalProviderRecordsSeen: providerRecordsSeen,
|
|
3369
|
+
historicalRecordsSeen: false,
|
|
3370
|
+
yieldedAt: null,
|
|
3371
|
+
};
|
|
2905
3372
|
}
|
|
2906
|
-
const
|
|
2907
|
-
|
|
2908
|
-
|
|
3373
|
+
const currentSources = sourceLifecycleFence && input.context.listConnectionSources
|
|
3374
|
+
? await input.context.listConnectionSources()
|
|
3375
|
+
: null;
|
|
3376
|
+
if (sourceLifecycleFence
|
|
3377
|
+
&& (!currentSources
|
|
3378
|
+
|| !isJunctionSourceLifecycleFenceCurrent(sourceLifecycleFence, currentSources))) {
|
|
3379
|
+
throw junctionTimeseriesSourceLifecycleSuperseded();
|
|
2909
3380
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
3381
|
+
if (records.length === 0 && !input.authorizedLocalDay) {
|
|
3382
|
+
return {
|
|
3383
|
+
historicalProviderRecordsSeen: providerRecordsSeen,
|
|
3384
|
+
historicalRecordsSeen: false,
|
|
3385
|
+
yieldedAt: null,
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
const sourceScopedHistoricalRecordsSeen = input.sourceProviderSlug
|
|
3389
|
+
? await hasJunctionSourceScopedAdmittedSnapshotRecords({
|
|
3390
|
+
context: input.context,
|
|
3391
|
+
...(currentSources ? { listedSources: currentSources } : {}),
|
|
3392
|
+
options: { projectAccountSourceIdentities: calendarDayAggregate },
|
|
3393
|
+
snapshots: { [input.resource]: records },
|
|
3394
|
+
sourceProviderSlug: input.sourceProviderSlug,
|
|
3395
|
+
sourceProviders: input.sourceProviders,
|
|
3396
|
+
})
|
|
3397
|
+
: false;
|
|
3398
|
+
const preparedImport = currentSources
|
|
3399
|
+
? prepareJunctionImportSnapshotForSources({ [input.resource]: records }, input.sourceProviders, currentSources, calendarDayAggregate
|
|
3400
|
+
? { sourceIdentities: resolveJunctionAccountSourceIdentities(currentSources) }
|
|
3401
|
+
: {}, {
|
|
3402
|
+
allowUnlistedSources: input.context.connectionSourceAdmissionMode !== "listed_only",
|
|
3403
|
+
sourceStatusRequirement: "not_disconnected",
|
|
3404
|
+
})
|
|
3405
|
+
: await prepareJunctionImportSnapshot(input.context, { [input.resource]: records }, input.sourceProviders, { projectAccountSourceIdentities: calendarDayAggregate });
|
|
3406
|
+
if (!hasJunctionSnapshotRecords(preparedImport.snapshots) && !input.authorizedLocalDay) {
|
|
3407
|
+
return {
|
|
3408
|
+
historicalProviderRecordsSeen: providerRecordsSeen,
|
|
3409
|
+
historicalRecordsSeen: sourceScopedHistoricalRecordsSeen,
|
|
3410
|
+
yieldedAt: null,
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
await commitPreparedJunctionCanonicalImport(input.context, preparedImport, {
|
|
2914
3414
|
importedAt: input.windowEnd,
|
|
2915
3415
|
windowStart: input.windowStart,
|
|
2916
3416
|
windowEnd: input.windowEnd,
|
|
2917
|
-
...(calendarDayAggregate
|
|
2918
|
-
|
|
3417
|
+
...(calendarDayAggregate && !input.authorizedLocalDay
|
|
3418
|
+
? { timeseriesWindowKind: "calendar_day" }
|
|
3419
|
+
: {}),
|
|
2919
3420
|
summaries: {},
|
|
2920
3421
|
timeseries: preparedImport.snapshots,
|
|
2921
|
-
}
|
|
3422
|
+
}, input.context.now, input.authorizedLocalDay
|
|
3423
|
+
? {
|
|
3424
|
+
completeSourceDay: {
|
|
3425
|
+
connectionId: buildJunctionImportAccountId(input.context.account.externalAccountId),
|
|
3426
|
+
dayKey: input.authorizedLocalDay.dayKey,
|
|
3427
|
+
resources: [input.resource],
|
|
3428
|
+
revisionAt: input.context.now,
|
|
3429
|
+
timeZone: input.authorizedLocalDay.timeZone,
|
|
3430
|
+
},
|
|
3431
|
+
}
|
|
3432
|
+
: undefined);
|
|
3433
|
+
return {
|
|
3434
|
+
historicalProviderRecordsSeen: providerRecordsSeen,
|
|
3435
|
+
historicalRecordsSeen: sourceScopedHistoricalRecordsSeen,
|
|
3436
|
+
yieldedAt: null,
|
|
3437
|
+
};
|
|
2922
3438
|
}
|
|
2923
3439
|
async function importJunctionWorkoutStreamWindow(input) {
|
|
2924
3440
|
const policy = resolveJunctionTimeseriesResourcePolicy("workout_stream");
|
|
@@ -2928,11 +3444,17 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2928
3444
|
throw new TypeError("Junction workout_stream policy did not define bounded limits.");
|
|
2929
3445
|
}
|
|
2930
3446
|
let completedIdentities = new Set(input.completedIdentities);
|
|
3447
|
+
let historicalProviderRecordsSeen = false;
|
|
3448
|
+
let historicalRecordsSeen = false;
|
|
2931
3449
|
let madeProgress = false;
|
|
3450
|
+
let workoutStreamEmptySeen = input.workoutStreamEmptySeen === true;
|
|
2932
3451
|
const carryTerminalProgressOrThrow = (error) => {
|
|
2933
3452
|
if (isJunctionJobSignalAbort(error, input.context.signal)) {
|
|
2934
3453
|
if (input.allowImmediateYield || madeProgress) {
|
|
2935
3454
|
return {
|
|
3455
|
+
emptyTimestampArraySeen: workoutStreamEmptySeen,
|
|
3456
|
+
historicalProviderRecordsSeen,
|
|
3457
|
+
historicalRecordsSeen,
|
|
2936
3458
|
madeProgress,
|
|
2937
3459
|
workoutStreamCursor: encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities),
|
|
2938
3460
|
yieldedAt: input.windowStart,
|
|
@@ -2941,13 +3463,40 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2941
3463
|
throw error;
|
|
2942
3464
|
}
|
|
2943
3465
|
if (isRetryableDeviceSyncFailure(error)) {
|
|
2944
|
-
throw new JunctionTimeseriesProgressError(error, input.windowStart, encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities)
|
|
3466
|
+
throw new JunctionTimeseriesProgressError(error, input.windowStart, encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities), {
|
|
3467
|
+
historicalProviderRecordsSeen,
|
|
3468
|
+
historicalRecordsSeen,
|
|
3469
|
+
workoutStreamEmptySeen,
|
|
3470
|
+
});
|
|
2945
3471
|
}
|
|
2946
3472
|
throw error;
|
|
2947
3473
|
};
|
|
2948
|
-
let
|
|
3474
|
+
let eligibleSourceProviderSlugs;
|
|
2949
3475
|
try {
|
|
2950
|
-
|
|
3476
|
+
eligibleSourceProviderSlugs = await resolveJunctionWorkoutStreamEligibleSources(input.context, input.sourceProviders);
|
|
3477
|
+
}
|
|
3478
|
+
catch (error) {
|
|
3479
|
+
return carryTerminalProgressOrThrow(error);
|
|
3480
|
+
}
|
|
3481
|
+
const sourceScopeProvided = input.sourceProviderSlug !== undefined
|
|
3482
|
+
&& input.sourceProviderSlug !== null;
|
|
3483
|
+
const scopedSourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
|
|
3484
|
+
if (eligibleSourceProviderSlugs.size === 0
|
|
3485
|
+
|| (sourceScopeProvided
|
|
3486
|
+
&& (!scopedSourceProviderSlug
|
|
3487
|
+
|| !eligibleSourceProviderSlugs.has(scopedSourceProviderSlug)))) {
|
|
3488
|
+
return {
|
|
3489
|
+
emptyTimestampArraySeen: workoutStreamEmptySeen,
|
|
3490
|
+
historicalProviderRecordsSeen,
|
|
3491
|
+
historicalRecordsSeen,
|
|
3492
|
+
madeProgress,
|
|
3493
|
+
workoutStreamCursor: null,
|
|
3494
|
+
yieldedAt: null,
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
let candidateListing;
|
|
3498
|
+
try {
|
|
3499
|
+
candidateListing = await listJunctionWorkoutStreamCandidates(client, {
|
|
2951
3500
|
collectionWorkLimit: input.collectionWorkLimit,
|
|
2952
3501
|
resource: "workout_stream",
|
|
2953
3502
|
signal: input.context.signal ?? null,
|
|
@@ -2955,11 +3504,13 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2955
3504
|
userId: input.context.account.externalAccountId,
|
|
2956
3505
|
windowEnd: input.windowEnd,
|
|
2957
3506
|
windowStart: input.windowStart,
|
|
2958
|
-
});
|
|
3507
|
+
}, true, eligibleSourceProviderSlugs);
|
|
2959
3508
|
}
|
|
2960
3509
|
catch (error) {
|
|
2961
3510
|
return carryTerminalProgressOrThrow(error);
|
|
2962
3511
|
}
|
|
3512
|
+
const { candidates } = candidateListing;
|
|
3513
|
+
historicalProviderRecordsSeen = candidateListing.providerRecordsSeen;
|
|
2963
3514
|
const candidateIdentities = new Set(candidates.map((candidate) => candidate.identity));
|
|
2964
3515
|
completedIdentities = new Set([...completedIdentities].filter((identity) => candidateIdentities.has(identity)));
|
|
2965
3516
|
for (const [candidateIndex, candidate] of candidates.entries()) {
|
|
@@ -2969,6 +3520,9 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2969
3520
|
if (input.context.shouldYield?.()) {
|
|
2970
3521
|
if (input.allowImmediateYield || madeProgress) {
|
|
2971
3522
|
return {
|
|
3523
|
+
emptyTimestampArraySeen: workoutStreamEmptySeen,
|
|
3524
|
+
historicalProviderRecordsSeen,
|
|
3525
|
+
historicalRecordsSeen,
|
|
2972
3526
|
madeProgress,
|
|
2973
3527
|
workoutStreamCursor: encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities),
|
|
2974
3528
|
yieldedAt: input.windowStart,
|
|
@@ -2976,12 +3530,12 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2976
3530
|
}
|
|
2977
3531
|
input.context.throwIfAborted?.();
|
|
2978
3532
|
}
|
|
2979
|
-
let
|
|
3533
|
+
let fetchedFeature;
|
|
2980
3534
|
try {
|
|
2981
|
-
|
|
3535
|
+
fetchedFeature = await fetchJunctionWorkoutStreamFeature(client, candidate, maxSamples, input.context.signal ?? null, input.collectionWorkLimit);
|
|
2982
3536
|
}
|
|
2983
3537
|
catch (error) {
|
|
2984
|
-
const failure =
|
|
3538
|
+
const failure = classifyOptionalJunctionWorkoutStreamCandidateFailure(error, input.context.account.externalAccountId);
|
|
2985
3539
|
if (!failure) {
|
|
2986
3540
|
return carryTerminalProgressOrThrow(addJunctionWorkoutStreamCandidateFailureContext(error, {
|
|
2987
3541
|
aliasSource: candidate.workoutIdAliasSource,
|
|
@@ -2999,8 +3553,21 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
2999
3553
|
madeProgress = true;
|
|
3000
3554
|
continue;
|
|
3001
3555
|
}
|
|
3556
|
+
const feature = fetchedFeature.feature;
|
|
3557
|
+
if (fetchedFeature.emptyTimestampArray) {
|
|
3558
|
+
input.context.logger.warn?.("Deferring Junction workout with an empty stream.", {
|
|
3559
|
+
errorCode: "JUNCTION_WORKOUT_STREAM_EMPTY",
|
|
3560
|
+
provider: "junction",
|
|
3561
|
+
resource: "workout_stream",
|
|
3562
|
+
resourceCategory: "timeseries",
|
|
3563
|
+
});
|
|
3564
|
+
completedIdentities.add(candidate.identity);
|
|
3565
|
+
madeProgress = true;
|
|
3566
|
+
workoutStreamEmptySeen = true;
|
|
3567
|
+
continue;
|
|
3568
|
+
}
|
|
3002
3569
|
if (feature === undefined) {
|
|
3003
|
-
input.context.logger.warn?.("Skipping Junction workout
|
|
3570
|
+
input.context.logger.warn?.("Skipping Junction workout without an importable stream feature.", {
|
|
3004
3571
|
errorCode: "JUNCTION_WORKOUT_STREAM_CARDINALITY_MISMATCH",
|
|
3005
3572
|
provider: "junction",
|
|
3006
3573
|
resource: "workout_stream",
|
|
@@ -3011,19 +3578,26 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3011
3578
|
continue;
|
|
3012
3579
|
}
|
|
3013
3580
|
try {
|
|
3014
|
-
const
|
|
3581
|
+
const sourceScopedFeature = input.sourceProviderSlug
|
|
3582
|
+
? withJunctionSourceProviderFallback(feature, input.sourceProviderSlug)
|
|
3583
|
+
: feature;
|
|
3584
|
+
historicalRecordsSeen ||= input.sourceProviderSlug
|
|
3585
|
+
? await hasJunctionSourceScopedAdmittedSnapshotRecords({
|
|
3586
|
+
context: input.context,
|
|
3587
|
+
snapshots: { workout_stream: [sourceScopedFeature] },
|
|
3588
|
+
sourceProviderSlug: input.sourceProviderSlug,
|
|
3589
|
+
sourceProviders: input.sourceProviders,
|
|
3590
|
+
})
|
|
3591
|
+
: false;
|
|
3592
|
+
const preparedImport = await prepareJunctionImportSnapshot(input.context, { workout_stream: [sourceScopedFeature] }, input.sourceProviders);
|
|
3015
3593
|
if (hasJunctionSnapshotRecords(preparedImport.snapshots)) {
|
|
3016
|
-
await input.context
|
|
3017
|
-
provider: "junction",
|
|
3018
|
-
accountId: buildJunctionImportAccountId(input.context.account.externalAccountId),
|
|
3019
|
-
connectionId: input.context.account.id,
|
|
3594
|
+
await commitPreparedJunctionCanonicalImport(input.context, preparedImport, {
|
|
3020
3595
|
importedAt: input.windowEnd,
|
|
3021
3596
|
windowStart: input.windowStart,
|
|
3022
3597
|
windowEnd: input.windowEnd,
|
|
3023
|
-
connections: preparedImport.connections,
|
|
3024
3598
|
summaries: {},
|
|
3025
3599
|
timeseries: preparedImport.snapshots,
|
|
3026
|
-
});
|
|
3600
|
+
}, input.context.now);
|
|
3027
3601
|
}
|
|
3028
3602
|
}
|
|
3029
3603
|
catch (error) {
|
|
@@ -3033,6 +3607,9 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3033
3607
|
madeProgress = true;
|
|
3034
3608
|
}
|
|
3035
3609
|
return {
|
|
3610
|
+
emptyTimestampArraySeen: workoutStreamEmptySeen,
|
|
3611
|
+
historicalProviderRecordsSeen,
|
|
3612
|
+
historicalRecordsSeen,
|
|
3036
3613
|
madeProgress,
|
|
3037
3614
|
workoutStreamCursor: encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities),
|
|
3038
3615
|
yieldedAt: null,
|
|
@@ -3058,14 +3635,10 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3058
3635
|
normalizationEvidence,
|
|
3059
3636
|
};
|
|
3060
3637
|
}
|
|
3061
|
-
const receipt = await context
|
|
3062
|
-
provider: "junction",
|
|
3063
|
-
accountId: buildJunctionImportAccountId(context.account.externalAccountId),
|
|
3064
|
-
connectionId: context.account.id,
|
|
3638
|
+
const receipt = await commitPreparedJunctionCanonicalImport(context, preparedImport, {
|
|
3065
3639
|
importedAt: context.now,
|
|
3066
3640
|
windowStart,
|
|
3067
3641
|
windowEnd,
|
|
3068
|
-
connections,
|
|
3069
3642
|
summaries,
|
|
3070
3643
|
timeseries: {},
|
|
3071
3644
|
});
|
|
@@ -3100,7 +3673,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3100
3673
|
}
|
|
3101
3674
|
async function fetchOptionalJunctionResourceRecords(context, resourceCategory, resource, skippedOptionalResources, load) {
|
|
3102
3675
|
try {
|
|
3103
|
-
return await load
|
|
3676
|
+
return await measureJunctionProviderRequest(context, "resource", load);
|
|
3104
3677
|
}
|
|
3105
3678
|
catch (error) {
|
|
3106
3679
|
const failure = classifyOptionalJunctionResourceFailure(error, resourceCategory, resource, context.account.externalAccountId);
|
|
@@ -3116,6 +3689,15 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3116
3689
|
return [];
|
|
3117
3690
|
}
|
|
3118
3691
|
}
|
|
3692
|
+
async function measureJunctionProviderRequest(context, category, load) {
|
|
3693
|
+
const startedAt = performance.now();
|
|
3694
|
+
try {
|
|
3695
|
+
return await load();
|
|
3696
|
+
}
|
|
3697
|
+
finally {
|
|
3698
|
+
context.recordProviderRequestTiming?.(category, Math.max(0, performance.now() - startedAt));
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3119
3701
|
function logSkippedOptionalJunctionResource(context, resourceCategory, resource, failure) {
|
|
3120
3702
|
context.logger.warn?.("Skipping unavailable Junction resource response.", {
|
|
3121
3703
|
errorCode: "JUNCTION_API_REQUEST_FAILED",
|
|
@@ -3161,8 +3743,26 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3161
3743
|
if (retryDelayMs === null) {
|
|
3162
3744
|
return null;
|
|
3163
3745
|
}
|
|
3746
|
+
const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
|
|
3747
|
+
const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(input.job);
|
|
3164
3748
|
const retryJob = buildExactWindowJob({
|
|
3165
3749
|
kind: "backfill",
|
|
3750
|
+
payload: {
|
|
3751
|
+
emptyBackfillAttempts,
|
|
3752
|
+
...(sourceProviderSlug ? { sourceProviderSlug } : {}),
|
|
3753
|
+
...(historicalProofAuthorization
|
|
3754
|
+
? {
|
|
3755
|
+
historicalProofFirstSeenAt: historicalProofAuthorization.firstSeenAt,
|
|
3756
|
+
historicalProofSourceProviderSlug: historicalProofAuthorization.sourceProviderSlug,
|
|
3757
|
+
}
|
|
3758
|
+
: {}),
|
|
3759
|
+
...(input.historicalProviderRecordsSeen
|
|
3760
|
+
? { historicalProviderRecordsSeen: true }
|
|
3761
|
+
: {}),
|
|
3762
|
+
...(input.historicalRecordsSeen
|
|
3763
|
+
? { historicalRecordsSeen: true }
|
|
3764
|
+
: {}),
|
|
3765
|
+
},
|
|
3166
3766
|
priority: Math.max(input.job.priority, JUNCTION_HISTORICAL_BACKFILL_RETRY_PRIORITY),
|
|
3167
3767
|
windowEnd: input.window.windowEnd,
|
|
3168
3768
|
windowStart: input.window.windowStart,
|
|
@@ -3170,33 +3770,67 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3170
3770
|
return {
|
|
3171
3771
|
...retryJob,
|
|
3172
3772
|
availableAt: addMilliseconds(input.now, retryDelayMs),
|
|
3173
|
-
payload: {
|
|
3174
|
-
...(retryJob.payload ?? {}),
|
|
3175
|
-
emptyBackfillAttempts,
|
|
3176
|
-
},
|
|
3177
3773
|
};
|
|
3178
3774
|
}
|
|
3179
|
-
function
|
|
3180
|
-
const
|
|
3775
|
+
async function completeSourceScopedHistoricalBackfill(input) {
|
|
3776
|
+
const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(input.job);
|
|
3777
|
+
if (!input.sourceProviderSlug
|
|
3778
|
+
|| !historicalProofAuthorization
|
|
3779
|
+
|| !await isJunctionHistoricalProofAuthorizationCurrent(input.context, historicalProofAuthorization)
|
|
3780
|
+
|| (input.historicalProviderRecordsSeen && !input.historicalRecordsSeen)
|
|
3781
|
+
|| !input.sourceProviders.some((provider) => areJunctionProviderSlugsDataEquivalent(provider.origin.sourceProviderSlug ?? provider.slug, input.sourceProviderSlug))) {
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
await projectJunctionSources(input.context, input.sourceProviders, {
|
|
3785
|
+
historicalBackfillCompletedProviderSlug: input.sourceProviderSlug,
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
async function buildFullJobTimeseriesContinuationResult(input) {
|
|
3789
|
+
const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(input.job);
|
|
3790
|
+
const historicalProofStillCurrent = !historicalProofAuthorization
|
|
3791
|
+
|| await isJunctionHistoricalProofAuthorizationCurrent(input.context, historicalProofAuthorization);
|
|
3792
|
+
const continuationJob = input.continuation && historicalProofStillCurrent
|
|
3181
3793
|
? buildFullJobTimeseriesContinuationJob({
|
|
3182
3794
|
deferredEmptyBackfillAttempts: readHistoricalBackfillJobEmptyAttempts(input.job),
|
|
3795
|
+
historicalProviderRecordsSeen: input.historicalProviderRecordsSeen,
|
|
3796
|
+
historicalRecordsSeen: input.historicalRecordsSeen,
|
|
3183
3797
|
job: input.job,
|
|
3184
3798
|
...input.continuation,
|
|
3185
3799
|
window: input.window,
|
|
3186
3800
|
})
|
|
3187
3801
|
: null;
|
|
3188
|
-
const scheduledJob =
|
|
3189
|
-
?
|
|
3190
|
-
:
|
|
3191
|
-
|
|
3802
|
+
const scheduledJob = !historicalProofStillCurrent
|
|
3803
|
+
? null
|
|
3804
|
+
: continuationJob
|
|
3805
|
+
? { ...continuationJob, availableAt: input.context.now }
|
|
3806
|
+
: buildDeferredNonConnectHistoricalBackfillRetry({
|
|
3807
|
+
account: input.context.account,
|
|
3808
|
+
historicalProviderRecordsSeen: input.historicalProviderRecordsSeen,
|
|
3809
|
+
historicalRecordsSeen: input.historicalRecordsSeen,
|
|
3810
|
+
job: input.job,
|
|
3811
|
+
now: input.context.now,
|
|
3812
|
+
window: input.window,
|
|
3813
|
+
});
|
|
3814
|
+
if (!scheduledJob && input.job.kind === "backfill") {
|
|
3815
|
+
await completeSourceScopedHistoricalBackfill({
|
|
3816
|
+
context: input.context,
|
|
3817
|
+
historicalProviderRecordsSeen: input.historicalProviderRecordsSeen,
|
|
3818
|
+
historicalRecordsSeen: input.historicalRecordsSeen,
|
|
3192
3819
|
job: input.job,
|
|
3193
|
-
|
|
3194
|
-
|
|
3820
|
+
sourceProviderSlug: normalizeProviderSlug(input.job.payload.sourceProviderSlug),
|
|
3821
|
+
sourceProviders: input.sourceProviders,
|
|
3195
3822
|
});
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3823
|
+
}
|
|
3824
|
+
return withJunctionWorkoutStreamEmptyReplay({
|
|
3825
|
+
job: input.job,
|
|
3826
|
+
now: input.context.now,
|
|
3827
|
+
replayWindow: input.replayWindow,
|
|
3828
|
+
result: withJunctionSkippedResourceMetadata(input.context, {
|
|
3829
|
+
...(scheduledJob ? { scheduledJobs: [scheduledJob] } : {}),
|
|
3830
|
+
nextReconcileAt: resolveJunctionNextReconcileAt(input.context.account, input.context.now, addMilliseconds(input.context.now, reconcileIntervalMs)),
|
|
3831
|
+
}, input.skippedOptionalResources),
|
|
3832
|
+
sourceProviderSlug: normalizeProviderSlug(input.job.payload.sourceProviderSlug),
|
|
3833
|
+
});
|
|
3200
3834
|
}
|
|
3201
3835
|
function buildFullJobTimeseriesContinuationJob(input) {
|
|
3202
3836
|
if (input.job.kind !== "backfill" && input.job.kind !== "reconcile") {
|
|
@@ -3212,7 +3846,12 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3212
3846
|
})) {
|
|
3213
3847
|
return null;
|
|
3214
3848
|
}
|
|
3215
|
-
const sourceProviderSlug =
|
|
3849
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.job.payload.sourceProviderSlug);
|
|
3850
|
+
const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(input.job);
|
|
3851
|
+
const historicalProviderRecordsSeen = input.historicalProviderRecordsSeen === true
|
|
3852
|
+
|| input.job.payload.historicalProviderRecordsSeen === true;
|
|
3853
|
+
const historicalRecordsSeen = input.historicalRecordsSeen === true
|
|
3854
|
+
|| input.job.payload.historicalRecordsSeen === true;
|
|
3216
3855
|
return buildExactWindowJob({
|
|
3217
3856
|
kind: input.job.kind,
|
|
3218
3857
|
payload: {
|
|
@@ -3220,12 +3859,27 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3220
3859
|
? { emptyBackfillAttempts: input.deferredEmptyBackfillAttempts }
|
|
3221
3860
|
: {}),
|
|
3222
3861
|
...(sourceProviderSlug ? { sourceProviderSlug } : {}),
|
|
3862
|
+
...(historicalProofAuthorization
|
|
3863
|
+
? {
|
|
3864
|
+
historicalProofFirstSeenAt: historicalProofAuthorization.firstSeenAt,
|
|
3865
|
+
historicalProofSourceProviderSlug: historicalProofAuthorization.sourceProviderSlug,
|
|
3866
|
+
}
|
|
3867
|
+
: {}),
|
|
3868
|
+
...(input.job.kind === "backfill" && historicalProviderRecordsSeen
|
|
3869
|
+
? { historicalProviderRecordsSeen: true }
|
|
3870
|
+
: {}),
|
|
3871
|
+
...(input.job.kind === "backfill" && historicalRecordsSeen
|
|
3872
|
+
? { historicalRecordsSeen: true }
|
|
3873
|
+
: {}),
|
|
3223
3874
|
timeseriesCursor: input.timeseriesCursor,
|
|
3224
3875
|
timeseriesResourceCursor,
|
|
3225
3876
|
...(input.timeseriesWindowHours === 1 ? { timeseriesWindowHours: 1 } : {}),
|
|
3226
3877
|
...(input.workoutStreamCursor
|
|
3227
3878
|
? { workoutStreamCursor: input.workoutStreamCursor }
|
|
3228
3879
|
: {}),
|
|
3880
|
+
...(input.workoutStreamEmptySeen
|
|
3881
|
+
? { workoutStreamEmptySeen: true }
|
|
3882
|
+
: {}),
|
|
3229
3883
|
},
|
|
3230
3884
|
priority: input.job.priority,
|
|
3231
3885
|
windowEnd: input.window.windowEnd,
|
|
@@ -3236,7 +3890,7 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3236
3890
|
if (Date.parse(input.windowStart) >= Date.parse(input.windowEnd)) {
|
|
3237
3891
|
return null;
|
|
3238
3892
|
}
|
|
3239
|
-
const sourceProviderSlug =
|
|
3893
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.job.payload.sourceProviderSlug);
|
|
3240
3894
|
if (input.job.kind !== "resource") {
|
|
3241
3895
|
return null;
|
|
3242
3896
|
}
|
|
@@ -3264,6 +3918,9 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3264
3918
|
windowEnd: input.windowEnd,
|
|
3265
3919
|
windowStart: input.windowStart,
|
|
3266
3920
|
workoutStreamCursor: input.workoutStreamCursor || undefined,
|
|
3921
|
+
workoutStreamEmptySeen: input.workoutStreamEmptySeen === true
|
|
3922
|
+
? true
|
|
3923
|
+
: undefined,
|
|
3267
3924
|
});
|
|
3268
3925
|
return {
|
|
3269
3926
|
kind: "resource",
|
|
@@ -3300,31 +3957,41 @@ export function createJunctionDeviceSyncProvider(config) {
|
|
|
3300
3957
|
&& normalizeString(account.metadata[JUNCTION_HISTORICAL_BACKFILL_METADATA_KEYS.windowStart]) === connectWindow.windowStart
|
|
3301
3958
|
&& normalizeString(account.metadata[JUNCTION_HISTORICAL_BACKFILL_METADATA_KEYS.windowEnd]) === connectWindow.windowEnd;
|
|
3302
3959
|
}
|
|
3303
|
-
function buildInitialJobs(now, sourceProviderSlug) {
|
|
3304
|
-
const normalizedSourceProviderSlug =
|
|
3305
|
-
const
|
|
3306
|
-
? {
|
|
3960
|
+
function buildInitialJobs(now, sourceProviderSlug, historicalProofAuthorization) {
|
|
3961
|
+
const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
|
|
3962
|
+
const sourcePayload = normalizedSourceProviderSlug
|
|
3963
|
+
? {
|
|
3964
|
+
sourceProviderSlug: normalizedSourceProviderSlug,
|
|
3965
|
+
}
|
|
3307
3966
|
: undefined;
|
|
3308
3967
|
return [
|
|
3309
3968
|
buildWindowJob({
|
|
3310
3969
|
kind: "backfill",
|
|
3311
3970
|
now,
|
|
3312
|
-
payload
|
|
3971
|
+
payload: sourcePayload && historicalProofAuthorization
|
|
3972
|
+
? {
|
|
3973
|
+
...sourcePayload,
|
|
3974
|
+
historicalProofFirstSeenAt: historicalProofAuthorization.firstSeenAt,
|
|
3975
|
+
historicalProofSourceProviderSlug: historicalProofAuthorization.sourceProviderSlug,
|
|
3976
|
+
}
|
|
3977
|
+
: sourcePayload,
|
|
3313
3978
|
windowStart: subtractDays(now, summaryBackfillDays),
|
|
3314
3979
|
priority: JUNCTION_HISTORICAL_BACKFILL_PRIORITY,
|
|
3315
3980
|
}),
|
|
3316
3981
|
buildWindowJob({
|
|
3317
3982
|
kind: "reconcile",
|
|
3318
3983
|
now,
|
|
3319
|
-
payload,
|
|
3984
|
+
payload: sourcePayload,
|
|
3320
3985
|
windowStart: subtractDays(now, reconcileDays),
|
|
3321
3986
|
priority: JUNCTION_SCHEDULED_RECONCILE_PRIORITY,
|
|
3322
3987
|
}),
|
|
3323
3988
|
];
|
|
3324
3989
|
}
|
|
3325
3990
|
function buildSourceConnectionWork(input) {
|
|
3991
|
+
const sourceProviderSlug = normalizeProviderSlug(input.sourceProviderSlug);
|
|
3992
|
+
const historicalProofAuthorization = normalizeJunctionHistoricalProofAuthorization(input.historicalProofAuthorization);
|
|
3326
3993
|
return {
|
|
3327
|
-
initialJobs: buildInitialJobs(input.now,
|
|
3994
|
+
initialJobs: buildInitialJobs(input.now, sourceProviderSlug, historicalProofAuthorization),
|
|
3328
3995
|
nextReconcileAt: addMilliseconds(input.now, reconcileIntervalMs),
|
|
3329
3996
|
};
|
|
3330
3997
|
}
|
|
@@ -3370,32 +4037,207 @@ async function fetchJunctionTimeseriesWindow(junctionClient, input) {
|
|
|
3370
4037
|
if (!maxSamples) {
|
|
3371
4038
|
throw new TypeError("Junction workout_stream policy did not define bounded limits.");
|
|
3372
4039
|
}
|
|
3373
|
-
const candidates = await listJunctionWorkoutStreamCandidates(junctionClient, input);
|
|
4040
|
+
const { candidates } = await listJunctionWorkoutStreamCandidates(junctionClient, input);
|
|
3374
4041
|
const features = [];
|
|
3375
4042
|
for (const candidate of candidates) {
|
|
3376
|
-
const feature = await fetchJunctionWorkoutStreamFeature(junctionClient, candidate, maxSamples, input.signal ?? null);
|
|
4043
|
+
const { feature } = await fetchJunctionWorkoutStreamFeature(junctionClient, candidate, maxSamples, input.signal ?? null);
|
|
3377
4044
|
if (feature !== undefined) {
|
|
3378
4045
|
features.push(feature);
|
|
3379
4046
|
}
|
|
3380
4047
|
}
|
|
3381
4048
|
return features;
|
|
3382
4049
|
}
|
|
4050
|
+
if (policy.normalizationMode === "ecg_recording_feature") {
|
|
4051
|
+
if (!policy.maxRecordsPerWindow || !policy.maxSamplesPerWindow) {
|
|
4052
|
+
throw new TypeError("Junction ECG voltage policy did not define bounded limits.");
|
|
4053
|
+
}
|
|
4054
|
+
return fetchJunctionElectrocardiogramVoltageFeatures(junctionClient, input, {
|
|
4055
|
+
maxRecordings: policy.maxRecordsPerWindow,
|
|
4056
|
+
maxSamples: policy.maxSamplesPerWindow,
|
|
4057
|
+
});
|
|
4058
|
+
}
|
|
3383
4059
|
const records = await junctionClient.listTimeseries({
|
|
3384
4060
|
...input,
|
|
3385
4061
|
maxRecords: input.maxRecords ?? policy.maxSamplesPerWindow,
|
|
3386
4062
|
});
|
|
3387
|
-
|
|
3388
|
-
|
|
4063
|
+
return records;
|
|
4064
|
+
}
|
|
4065
|
+
async function fetchJunctionElectrocardiogramVoltageFeatures(junctionClient, input, limits) {
|
|
4066
|
+
const summaries = await junctionClient.listSummary({
|
|
4067
|
+
collectionWorkLimit: input.collectionWorkLimit,
|
|
4068
|
+
maxRecords: limits.maxRecordings + 1,
|
|
4069
|
+
resource: "electrocardiogram",
|
|
4070
|
+
signal: input.signal ?? null,
|
|
4071
|
+
sourceProviderSlug: input.sourceProviderSlug,
|
|
4072
|
+
userId: input.userId,
|
|
4073
|
+
windowEnd: input.windowEnd,
|
|
4074
|
+
windowStart: input.windowStart,
|
|
4075
|
+
});
|
|
4076
|
+
const candidates = selectJunctionElectrocardiogramRecordingCandidates(summaries, {
|
|
4077
|
+
maxRecordings: limits.maxRecordings,
|
|
4078
|
+
requestedSourceProviderSlug: input.sourceProviderSlug,
|
|
4079
|
+
windowEnd: input.windowEnd,
|
|
4080
|
+
windowStart: input.windowStart,
|
|
4081
|
+
});
|
|
4082
|
+
const expectedSampleCount = candidates.reduce((total, candidate) => total + candidate.voltageSampleCount, 0);
|
|
4083
|
+
if (expectedSampleCount > limits.maxSamples) {
|
|
4084
|
+
throw junctionElectrocardiogramBindingError("sample_limit_exceeded", {
|
|
4085
|
+
expectedSampleCount,
|
|
4086
|
+
maxSampleCount: limits.maxSamples,
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
const features = [];
|
|
4090
|
+
for (const candidate of candidates) {
|
|
4091
|
+
if (candidate.voltageSampleCount === 0) {
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
const samples = await junctionClient.listElectrocardiogramVoltage({
|
|
4095
|
+
collectionWorkLimit: input.collectionWorkLimit,
|
|
4096
|
+
maxRecords: candidate.voltageSampleCount + 1,
|
|
4097
|
+
recordingId: candidate.recordingId,
|
|
4098
|
+
signal: input.signal ?? null,
|
|
4099
|
+
sourceInstanceId: candidate.sourceInstanceId,
|
|
4100
|
+
sourceProviderSlug: candidate.sourceProviderSlug,
|
|
4101
|
+
sourceType: candidate.sourceType,
|
|
4102
|
+
userId: input.userId,
|
|
4103
|
+
windowEnd: candidate.sessionEnd,
|
|
4104
|
+
windowStart: candidate.sessionStart,
|
|
4105
|
+
});
|
|
4106
|
+
let candidateFeatures;
|
|
4107
|
+
try {
|
|
4108
|
+
candidateFeatures = reduceJunctionElectrocardiogramVoltageRecords(samples, { maxRecordings: 1, maxSamples: limits.maxSamples });
|
|
4109
|
+
}
|
|
4110
|
+
catch (error) {
|
|
4111
|
+
if (isDeviceSyncError(error)) {
|
|
4112
|
+
throw error;
|
|
4113
|
+
}
|
|
4114
|
+
throw junctionElectrocardiogramBindingError("sample_normalization_invalid");
|
|
4115
|
+
}
|
|
4116
|
+
const feature = candidateFeatures[0];
|
|
4117
|
+
if (!feature || candidateFeatures.length !== 1) {
|
|
4118
|
+
throw junctionElectrocardiogramBindingError("feature_cardinality_mismatch", {
|
|
4119
|
+
actualRecordingCount: candidateFeatures.length,
|
|
4120
|
+
expectedRecordingCount: 1,
|
|
4121
|
+
});
|
|
4122
|
+
}
|
|
4123
|
+
const actualSampleCount = readJunctionElectrocardiogramSampleCount(feature);
|
|
4124
|
+
if (actualSampleCount !== candidate.voltageSampleCount) {
|
|
4125
|
+
throw junctionElectrocardiogramBindingError("sample_count_mismatch", {
|
|
4126
|
+
actualSampleCount: actualSampleCount ?? 0,
|
|
4127
|
+
expectedSampleCount: candidate.voltageSampleCount,
|
|
4128
|
+
});
|
|
4129
|
+
}
|
|
4130
|
+
features.push(feature);
|
|
4131
|
+
}
|
|
4132
|
+
return features;
|
|
4133
|
+
}
|
|
4134
|
+
function selectJunctionElectrocardiogramRecordingCandidates(summaries, input) {
|
|
4135
|
+
const windowStartMs = Date.parse(input.windowStart);
|
|
4136
|
+
const windowEndMs = Date.parse(input.windowEnd);
|
|
4137
|
+
if (!Number.isFinite(windowStartMs)
|
|
4138
|
+
|| !Number.isFinite(windowEndMs)
|
|
4139
|
+
|| windowEndMs <= windowStartMs) {
|
|
4140
|
+
throw new TypeError("Junction ECG collection window was invalid.");
|
|
4141
|
+
}
|
|
4142
|
+
const selected = new Map();
|
|
4143
|
+
for (const value of summaries) {
|
|
4144
|
+
const summary = readPlainObject(value);
|
|
4145
|
+
const recordingId = normalizeString(summary?.id);
|
|
4146
|
+
const sessionStart = readJunctionElectrocardiogramTimestamp(summary, ["sessionStart", "session_start"]);
|
|
4147
|
+
const sessionEnd = readJunctionElectrocardiogramTimestamp(summary, ["sessionEnd", "session_end"]);
|
|
4148
|
+
const voltageSampleCount = readJunctionElectrocardiogramSampleCount(summary);
|
|
4149
|
+
if (!summary || !recordingId || !sessionStart || !sessionEnd || voltageSampleCount === null) {
|
|
4150
|
+
throw junctionElectrocardiogramBindingError("summary_identity_incomplete");
|
|
4151
|
+
}
|
|
4152
|
+
const sessionStartMs = Date.parse(sessionStart);
|
|
4153
|
+
const sessionEndMs = Date.parse(sessionEnd);
|
|
4154
|
+
if (sessionEndMs < sessionStartMs) {
|
|
4155
|
+
throw junctionElectrocardiogramBindingError("summary_window_invalid");
|
|
4156
|
+
}
|
|
4157
|
+
if (sessionStartMs < windowStartMs || sessionStartMs >= windowEndMs) {
|
|
4158
|
+
continue;
|
|
4159
|
+
}
|
|
4160
|
+
const unresolvedOrigin = resolveJunctionOrigin(summary);
|
|
4161
|
+
const canonicalSourceProviderSlug = canonicalizeJunctionProviderSlug(unresolvedOrigin.sourceProviderSlug);
|
|
4162
|
+
const origin = resolveJunctionOrigin({
|
|
4163
|
+
...summary,
|
|
4164
|
+
sourceProviderSlug: canonicalSourceProviderSlug
|
|
4165
|
+
?? unresolvedOrigin.sourceProviderSlug,
|
|
4166
|
+
});
|
|
4167
|
+
const sourceProviderSlug = normalizeJunctionSourceProviderSlug(origin.sourceProviderSlug);
|
|
4168
|
+
if (!sourceProviderSlug
|
|
4169
|
+
|| (input.requestedSourceProviderSlug
|
|
4170
|
+
&& !areJunctionProviderSlugsDataEquivalent(sourceProviderSlug, input.requestedSourceProviderSlug))) {
|
|
4171
|
+
throw junctionElectrocardiogramBindingError("summary_source_inconsistent");
|
|
4172
|
+
}
|
|
4173
|
+
const candidate = {
|
|
4174
|
+
recordingId,
|
|
4175
|
+
sessionEnd,
|
|
4176
|
+
sessionStart,
|
|
4177
|
+
sourceInstanceId: normalizeString(origin.sourceInstanceId),
|
|
4178
|
+
sourceProviderSlug,
|
|
4179
|
+
sourceType: normalizeString(origin.sourceType),
|
|
4180
|
+
voltageSampleCount,
|
|
4181
|
+
};
|
|
4182
|
+
const existing = selected.get(recordingId);
|
|
4183
|
+
if (existing && JSON.stringify(existing) !== JSON.stringify(candidate)) {
|
|
4184
|
+
throw junctionElectrocardiogramBindingError("summary_identity_conflict");
|
|
4185
|
+
}
|
|
4186
|
+
selected.set(recordingId, candidate);
|
|
4187
|
+
}
|
|
4188
|
+
if (selected.size > input.maxRecordings) {
|
|
4189
|
+
throw junctionElectrocardiogramBindingError("recording_limit_exceeded", {
|
|
4190
|
+
actualRecordingCount: selected.size,
|
|
4191
|
+
maxRecordingCount: input.maxRecordings,
|
|
4192
|
+
});
|
|
3389
4193
|
}
|
|
3390
|
-
|
|
3391
|
-
|
|
4194
|
+
const candidates = [...selected.values()].sort((left, right) => left.sessionStart.localeCompare(right.sessionStart)
|
|
4195
|
+
|| left.recordingId.localeCompare(right.recordingId));
|
|
4196
|
+
const latestEndBySource = new Map();
|
|
4197
|
+
for (const candidate of candidates) {
|
|
4198
|
+
const sourceIdentity = JSON.stringify([
|
|
4199
|
+
candidate.sourceProviderSlug,
|
|
4200
|
+
candidate.sourceType ?? null,
|
|
4201
|
+
candidate.sourceInstanceId ?? null,
|
|
4202
|
+
]);
|
|
4203
|
+
const latestEnd = latestEndBySource.get(sourceIdentity);
|
|
4204
|
+
if (latestEnd !== undefined && Date.parse(candidate.sessionStart) < latestEnd) {
|
|
4205
|
+
throw junctionElectrocardiogramBindingError("summary_windows_ambiguous");
|
|
4206
|
+
}
|
|
4207
|
+
latestEndBySource.set(sourceIdentity, Date.parse(candidate.sessionEnd));
|
|
3392
4208
|
}
|
|
3393
|
-
return
|
|
3394
|
-
|
|
3395
|
-
|
|
4209
|
+
return candidates;
|
|
4210
|
+
}
|
|
4211
|
+
function readJunctionElectrocardiogramTimestamp(summary, keys) {
|
|
4212
|
+
for (const key of keys) {
|
|
4213
|
+
const value = summary?.[key];
|
|
4214
|
+
const timestamp = value instanceof Date
|
|
4215
|
+
? value.getTime()
|
|
4216
|
+
: Date.parse(normalizeString(value) ?? "");
|
|
4217
|
+
if (Number.isFinite(timestamp)) {
|
|
4218
|
+
return new Date(timestamp).toISOString();
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
return null;
|
|
4222
|
+
}
|
|
4223
|
+
function readJunctionElectrocardiogramSampleCount(summary) {
|
|
4224
|
+
const value = summary?.voltageSampleCount ?? summary?.voltage_sample_count;
|
|
4225
|
+
return typeof value === "number"
|
|
4226
|
+
&& Number.isSafeInteger(value)
|
|
4227
|
+
&& value >= 0
|
|
4228
|
+
? value
|
|
4229
|
+
: null;
|
|
4230
|
+
}
|
|
4231
|
+
function junctionElectrocardiogramBindingError(reason, details = {}) {
|
|
4232
|
+
return deviceSyncError({
|
|
4233
|
+
code: "JUNCTION_ECG_RECORDING_BINDING_INCOMPLETE",
|
|
4234
|
+
details: { reason, ...details },
|
|
4235
|
+
message: "Junction ECG summary and voltage response were inconsistent.",
|
|
4236
|
+
retryable: true,
|
|
4237
|
+
httpStatus: 502,
|
|
3396
4238
|
});
|
|
3397
4239
|
}
|
|
3398
|
-
async function listJunctionWorkoutStreamCandidates(junctionClient, input) {
|
|
4240
|
+
async function listJunctionWorkoutStreamCandidates(junctionClient, input, strictSourceScope = false, eligibleSourceProviderSlugs) {
|
|
3399
4241
|
const maxWorkouts = resolveJunctionTimeseriesResourcePolicy("workout_stream")
|
|
3400
4242
|
?.maxRecordsPerWindow;
|
|
3401
4243
|
if (!maxWorkouts) {
|
|
@@ -3409,10 +4251,25 @@ async function listJunctionWorkoutStreamCandidates(junctionClient, input) {
|
|
|
3409
4251
|
userId: input.userId,
|
|
3410
4252
|
windowStart: input.windowStart,
|
|
3411
4253
|
windowEnd: input.windowEnd,
|
|
3412
|
-
maxRecords: maxWorkouts + 1,
|
|
3413
4254
|
});
|
|
3414
|
-
const summaries =
|
|
3415
|
-
|
|
4255
|
+
const summaries = strictSourceScope
|
|
4256
|
+
? scopeJunctionRecordsToSourceProvider(indexRecords, input.sourceProviderSlug)
|
|
4257
|
+
: indexRecords.map((record) => withJunctionSourceProviderFallback(record, input.sourceProviderSlug));
|
|
4258
|
+
const eligibleSummaries = eligibleSourceProviderSlugs === undefined
|
|
4259
|
+
? summaries
|
|
4260
|
+
: summaries.filter((summary) => {
|
|
4261
|
+
const summaryRecord = readPlainObject(summary);
|
|
4262
|
+
if (!summaryRecord) {
|
|
4263
|
+
return false;
|
|
4264
|
+
}
|
|
4265
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(resolveJunctionOrigin(summaryRecord).sourceProviderSlug);
|
|
4266
|
+
return sourceProviderSlug !== null
|
|
4267
|
+
&& eligibleSourceProviderSlugs.has(sourceProviderSlug);
|
|
4268
|
+
});
|
|
4269
|
+
return {
|
|
4270
|
+
candidates: selectJunctionWorkoutStreamCandidates(eligibleSummaries, maxWorkouts),
|
|
4271
|
+
providerRecordsSeen: indexRecords.length > 0,
|
|
4272
|
+
};
|
|
3416
4273
|
}
|
|
3417
4274
|
async function fetchJunctionWorkoutStreamFeature(junctionClient, candidate, maxSamples, signal, collectionWorkLimit) {
|
|
3418
4275
|
const stream = await junctionClient.getWorkoutStream({
|
|
@@ -3420,11 +4277,26 @@ async function fetchJunctionWorkoutStreamFeature(junctionClient, candidate, maxS
|
|
|
3420
4277
|
signal,
|
|
3421
4278
|
workoutId: candidate.workoutId,
|
|
3422
4279
|
});
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
4280
|
+
try {
|
|
4281
|
+
return {
|
|
4282
|
+
emptyTimestampArray: false,
|
|
4283
|
+
feature: reduceJunctionWorkoutStreamPayload({
|
|
4284
|
+
maxSamples,
|
|
4285
|
+
stream,
|
|
4286
|
+
summary: candidate.summary,
|
|
4287
|
+
}),
|
|
4288
|
+
};
|
|
4289
|
+
}
|
|
4290
|
+
catch (error) {
|
|
4291
|
+
if (error instanceof JunctionWorkoutStreamTimestampCardinalityError
|
|
4292
|
+
&& error.diagnostic.kind === "empty") {
|
|
4293
|
+
return {
|
|
4294
|
+
emptyTimestampArray: true,
|
|
4295
|
+
feature: undefined,
|
|
4296
|
+
};
|
|
4297
|
+
}
|
|
4298
|
+
throw error;
|
|
4299
|
+
}
|
|
3428
4300
|
}
|
|
3429
4301
|
function withJunctionSourceProviderFallback(value, sourceProviderSlug) {
|
|
3430
4302
|
const record = readPlainObject(value);
|
|
@@ -3434,6 +4306,24 @@ function withJunctionSourceProviderFallback(value, sourceProviderSlug) {
|
|
|
3434
4306
|
}
|
|
3435
4307
|
return { ...record, sourceProviderSlug: normalizedSource };
|
|
3436
4308
|
}
|
|
4309
|
+
function scopeJunctionRecordsToSourceProvider(records, sourceProviderSlug) {
|
|
4310
|
+
const normalizedSource = normalizeProviderSlug(sourceProviderSlug);
|
|
4311
|
+
if (!normalizedSource) {
|
|
4312
|
+
return [...records];
|
|
4313
|
+
}
|
|
4314
|
+
return records.flatMap((record) => {
|
|
4315
|
+
const scopedRecord = withJunctionSourceProviderFallback(record, normalizedSource);
|
|
4316
|
+
const scopedRecordObject = readPlainObject(scopedRecord);
|
|
4317
|
+
if (!scopedRecordObject) {
|
|
4318
|
+
return [];
|
|
4319
|
+
}
|
|
4320
|
+
const recordSourceProviderSlug = normalizeProviderSlug(resolveJunctionOrigin(scopedRecordObject).sourceProviderSlug);
|
|
4321
|
+
return recordSourceProviderSlug
|
|
4322
|
+
&& areJunctionProviderSlugsDataEquivalent(recordSourceProviderSlug, normalizedSource)
|
|
4323
|
+
? [scopedRecord]
|
|
4324
|
+
: [];
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
3437
4327
|
function withJunctionSkippedResourceMetadata(context, result, skippedOptionalResources) {
|
|
3438
4328
|
if (skippedOptionalResources.length === 0) {
|
|
3439
4329
|
return result;
|
|
@@ -3558,6 +4448,24 @@ function classifyOptionalJunctionResourceFailure(error, resourceCategory, resour
|
|
|
3558
4448
|
...(responseDetail ? { responseDetail } : {}),
|
|
3559
4449
|
};
|
|
3560
4450
|
}
|
|
4451
|
+
function classifyOptionalJunctionWorkoutStreamCandidateFailure(error, accountExternalId) {
|
|
4452
|
+
const existing = classifyOptionalJunctionResourceFailure(error, "timeseries", "workout_stream", accountExternalId);
|
|
4453
|
+
if (existing) {
|
|
4454
|
+
return existing;
|
|
4455
|
+
}
|
|
4456
|
+
if (!isDeviceSyncError(error)
|
|
4457
|
+
|| error.code !== "JUNCTION_API_REQUEST_FAILED"
|
|
4458
|
+
|| error.details?.status !== 400) {
|
|
4459
|
+
return null;
|
|
4460
|
+
}
|
|
4461
|
+
const reason = classifyClearOptionalJunctionResourceFailureReason({
|
|
4462
|
+
responseErrorCode: readJunctionDiagnosticString(error.details.responseErrorCode),
|
|
4463
|
+
responseErrorDescription: readJunctionDiagnosticString(error.details.responseErrorDescription),
|
|
4464
|
+
});
|
|
4465
|
+
return reason === "unsupported"
|
|
4466
|
+
? { reason, responseStatus: 400 }
|
|
4467
|
+
: null;
|
|
4468
|
+
}
|
|
3561
4469
|
function isRetryableDeviceSyncFailure(error) {
|
|
3562
4470
|
return isDeviceSyncError(error) && error.retryable;
|
|
3563
4471
|
}
|
|
@@ -4486,13 +5394,6 @@ function resolveJunctionProviderRouteSlug(value) {
|
|
|
4486
5394
|
return resolveJunctionDeviceConnectRouteByProviderSlug(normalized)?.route.sourceProviderSlug
|
|
4487
5395
|
?? normalized;
|
|
4488
5396
|
}
|
|
4489
|
-
function areJunctionProviderSlugsRouteEquivalent(left, right) {
|
|
4490
|
-
const normalizedLeft = normalizeProviderSlug(left);
|
|
4491
|
-
const normalizedRight = normalizeProviderSlug(right);
|
|
4492
|
-
return normalizedLeft !== null
|
|
4493
|
-
&& normalizedRight !== null
|
|
4494
|
-
&& areJunctionDeviceConnectProviderSlugsEquivalent(normalizedLeft, normalizedRight);
|
|
4495
|
-
}
|
|
4496
5397
|
function resolveJunctionAccountSourceIdentities(sources) {
|
|
4497
5398
|
const identities = [];
|
|
4498
5399
|
for (const source of sources) {
|
|
@@ -4507,7 +5408,7 @@ function resolveJunctionAccountSourceIdentities(sources) {
|
|
|
4507
5408
|
function resolveJunctionAccountSourceStates(sources) {
|
|
4508
5409
|
const states = [];
|
|
4509
5410
|
for (const source of sources) {
|
|
4510
|
-
if (states.some((state) =>
|
|
5411
|
+
if (states.some((state) => areJunctionProviderSlugsDataEquivalent(state.identitySource.sourceProviderSlug, source.sourceProviderSlug))) {
|
|
4511
5412
|
continue;
|
|
4512
5413
|
}
|
|
4513
5414
|
const state = resolveJunctionAccountSourceState(sources, source.sourceProviderSlug);
|
|
@@ -4520,7 +5421,7 @@ function resolveJunctionAccountSourceStates(sources) {
|
|
|
4520
5421
|
function mergeJunctionAccountSourceIdentities(accountSourceIdentities, requestedSourceIdentities) {
|
|
4521
5422
|
return [
|
|
4522
5423
|
...accountSourceIdentities,
|
|
4523
|
-
...requestedSourceIdentities.filter((requestedIdentity) => !accountSourceIdentities.some((accountIdentity) =>
|
|
5424
|
+
...requestedSourceIdentities.filter((requestedIdentity) => !accountSourceIdentities.some((accountIdentity) => areJunctionProviderSlugsDataEquivalent(requestedIdentity.sourceProviderSlug, accountIdentity.sourceProviderSlug))),
|
|
4524
5425
|
];
|
|
4525
5426
|
}
|
|
4526
5427
|
function resolveJunctionAccountSourceIdentity(sources, sourceProviderSlug, allowUnlistedSource = false) {
|
|
@@ -4559,7 +5460,7 @@ function findJunctionAccountSource(sources, sourceProviderSlug) {
|
|
|
4559
5460
|
?.identitySource;
|
|
4560
5461
|
}
|
|
4561
5462
|
function findJunctionAccountSources(sources, sourceProviderSlug) {
|
|
4562
|
-
return sources.filter((source) =>
|
|
5463
|
+
return sources.filter((source) => areJunctionProviderSlugsDataEquivalent(source.sourceProviderSlug, sourceProviderSlug));
|
|
4563
5464
|
}
|
|
4564
5465
|
function resolveJunctionAccountSourceState(sources, sourceProviderSlug) {
|
|
4565
5466
|
const [first, ...rest] = findJunctionAccountSources(sources, sourceProviderSlug);
|
|
@@ -4574,7 +5475,7 @@ function stripUndefined(value) {
|
|
|
4574
5475
|
function sanitizeJunctionImportConnections(providers, sourceIdentities = []) {
|
|
4575
5476
|
return providers.map((provider) => {
|
|
4576
5477
|
const sourceProviderSlug = provider.origin.sourceProviderSlug ?? provider.slug;
|
|
4577
|
-
const projectedSourceIdentity = sourceIdentities.find((identity) =>
|
|
5478
|
+
const projectedSourceIdentity = sourceIdentities.find((identity) => areJunctionProviderSlugsDataEquivalent(sourceProviderSlug, identity.sourceProviderSlug));
|
|
4578
5479
|
return stripUndefined({
|
|
4579
5480
|
sourceProviderSlug: projectedSourceIdentity?.sourceProviderSlug ?? sourceProviderSlug,
|
|
4580
5481
|
sourceInstanceId: projectedSourceIdentity?.sourceInstanceId ?? provider.origin.sourceInstanceId,
|
|
@@ -4593,11 +5494,40 @@ async function prepareJunctionImportSnapshot(context, snapshots, providers, opti
|
|
|
4593
5494
|
sourceStatusRequirement,
|
|
4594
5495
|
});
|
|
4595
5496
|
}
|
|
5497
|
+
async function hasJunctionSourceScopedAdmittedSnapshotRecords(input) {
|
|
5498
|
+
const listedSources = input.listedSources
|
|
5499
|
+
?? (input.context.listConnectionSources
|
|
5500
|
+
? await input.context.listConnectionSources({
|
|
5501
|
+
sourceProviderSlug: input.sourceProviderSlug,
|
|
5502
|
+
})
|
|
5503
|
+
: input.context.account.sources ?? []);
|
|
5504
|
+
const sourceAuthorities = listedSources.filter((source) => areJunctionProviderSlugsDataEquivalent(source.sourceProviderSlug, input.sourceProviderSlug));
|
|
5505
|
+
const sourceProviders = input.sourceProviders.filter((provider) => areJunctionProviderSlugsDataEquivalent(provider.origin.sourceProviderSlug ?? provider.slug, input.sourceProviderSlug));
|
|
5506
|
+
if (sourceAuthorities.length === 0 || sourceProviders.length === 0) {
|
|
5507
|
+
return false;
|
|
5508
|
+
}
|
|
5509
|
+
const options = input.options ?? {};
|
|
5510
|
+
const sourceIdentities = options.projectAccountSourceIdentities
|
|
5511
|
+
? mergeJunctionAccountSourceIdentities(resolveJunctionAccountSourceIdentities(sourceAuthorities), options.sourceIdentities ?? [])
|
|
5512
|
+
: options.sourceIdentities;
|
|
5513
|
+
const prepared = prepareJunctionImportSnapshotForSources(input.snapshots, sourceProviders, sourceAuthorities, {
|
|
5514
|
+
...options,
|
|
5515
|
+
...(sourceIdentities ? { sourceIdentities } : {}),
|
|
5516
|
+
}, {
|
|
5517
|
+
allowUnlistedSources: false,
|
|
5518
|
+
sourceStatusRequirement: "not_disconnected",
|
|
5519
|
+
});
|
|
5520
|
+
// This is evidence that the source-scoped historical pull produced an
|
|
5521
|
+
// admitted record. Canonical import still evaluates every account source,
|
|
5522
|
+
// preserving the Fitbit writer fence until cutover is actually ready.
|
|
5523
|
+
return hasJunctionSnapshotRecords(prepared.snapshots);
|
|
5524
|
+
}
|
|
4596
5525
|
function prepareJunctionImportSnapshotForSources(snapshots, providers, currentSources, options = {}, admission = {}) {
|
|
4597
5526
|
const allowUnlistedSources = admission.allowUnlistedSources ?? true;
|
|
4598
5527
|
const sourceStatusRequirement = admission.sourceStatusRequirement ?? "not_disconnected";
|
|
4599
5528
|
const sourceProviders = providers.filter((provider) => isJunctionSourceAdmittedForImport(currentSources, provider.origin.sourceProviderSlug ?? provider.slug, allowUnlistedSources, sourceStatusRequirement));
|
|
4600
5529
|
return {
|
|
5530
|
+
canonicalCoverageFence: buildJunctionGoogleHealthCanonicalCoverageFence(currentSources),
|
|
4601
5531
|
connections: sanitizeJunctionImportConnections(sourceProviders, options.sourceIdentities),
|
|
4602
5532
|
sourceProviders,
|
|
4603
5533
|
snapshots: sanitizeJunctionImportSnapshots(filterJunctionImportSnapshots(snapshots, providers, currentSources, allowUnlistedSources, sourceStatusRequirement), providers, options),
|
|
@@ -4614,15 +5544,133 @@ function filterJunctionImportSnapshots(snapshots, providers, sources, allowUnlis
|
|
|
4614
5544
|
records.filter((record) => isJunctionImportRecordAdmitted(record, sourceReferences, sources, hasPendingSourceAdmission, allowUnlistedSources, sourceStatusRequirement)),
|
|
4615
5545
|
]));
|
|
4616
5546
|
}
|
|
5547
|
+
async function commitPreparedJunctionCanonicalImport(context, preparedImport, snapshot, providerPulledAt, importOptions) {
|
|
5548
|
+
const receipt = await context.importSnapshot({
|
|
5549
|
+
provider: "junction",
|
|
5550
|
+
accountId: buildJunctionImportAccountId(context.account.externalAccountId),
|
|
5551
|
+
connectionId: context.account.id,
|
|
5552
|
+
...snapshot,
|
|
5553
|
+
connections: preparedImport.connections,
|
|
5554
|
+
canonicalCoverageFence: preparedImport.canonicalCoverageFence,
|
|
5555
|
+
...(providerPulledAt
|
|
5556
|
+
? { canonicalCoverageProviderPulledAt: providerPulledAt }
|
|
5557
|
+
: {}),
|
|
5558
|
+
}, importOptions);
|
|
5559
|
+
await recordAcceptedJunctionFitbitCoverage(context, receipt);
|
|
5560
|
+
return receipt;
|
|
5561
|
+
}
|
|
5562
|
+
async function recordAcceptedJunctionFitbitCoverage(context, receipt) {
|
|
5563
|
+
const canonicalCoverage = readProviderSnapshotJunctionCanonicalCoverage(receipt);
|
|
5564
|
+
if (!readProviderSnapshotDurableDeliveryAccepted(receipt)
|
|
5565
|
+
|| canonicalCoverage.length === 0
|
|
5566
|
+
|| !context.upsertConnectionSource) {
|
|
5567
|
+
return;
|
|
5568
|
+
}
|
|
5569
|
+
const coverageByResource = new Map();
|
|
5570
|
+
for (const evidence of canonicalCoverage) {
|
|
5571
|
+
if (normalizeProviderSlug(evidence.sourceProviderSlug)
|
|
5572
|
+
!== JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG
|
|
5573
|
+
|| !buildDeviceSyncSourceCanonicalCoverageBoundaryKey(evidence.resource)) {
|
|
5574
|
+
continue;
|
|
5575
|
+
}
|
|
5576
|
+
const existing = coverageByResource.get(evidence.resource);
|
|
5577
|
+
const coverageBoundary = existing
|
|
5578
|
+
? maxJunctionCanonicalCoverageBoundary(evidence.resource, existing.coverageBoundary, evidence.coverageBoundary)
|
|
5579
|
+
: evidence.coverageBoundary;
|
|
5580
|
+
coverageByResource.set(evidence.resource, {
|
|
5581
|
+
coverageBoundary,
|
|
5582
|
+
coverageFinalizedAt: coverageBoundary === evidence.coverageBoundary
|
|
5583
|
+
? evidence.coverageFinalizedAt ?? existing?.coverageFinalizedAt
|
|
5584
|
+
: existing?.coverageFinalizedAt,
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5587
|
+
if (coverageByResource.size === 0) {
|
|
5588
|
+
return;
|
|
5589
|
+
}
|
|
5590
|
+
const sources = context.listConnectionSources
|
|
5591
|
+
? await context.listConnectionSources({
|
|
5592
|
+
sourceProviderSlug: JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG,
|
|
5593
|
+
})
|
|
5594
|
+
: context.account.sources ?? [];
|
|
5595
|
+
const legacy = sources.find((source) => normalizeProviderSlug(source.sourceProviderSlug)
|
|
5596
|
+
=== JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG);
|
|
5597
|
+
if (!legacy || legacy.status === "disconnected") {
|
|
5598
|
+
return;
|
|
5599
|
+
}
|
|
5600
|
+
const resourceAvailabilitySummary = {
|
|
5601
|
+
...(legacy.resourceAvailabilitySummary ?? {}),
|
|
5602
|
+
};
|
|
5603
|
+
let changed = false;
|
|
5604
|
+
for (const [resource, coverage] of coverageByResource) {
|
|
5605
|
+
const key = buildDeviceSyncSourceCanonicalCoverageBoundaryKey(resource);
|
|
5606
|
+
if (!key) {
|
|
5607
|
+
continue;
|
|
5608
|
+
}
|
|
5609
|
+
const existingCoverage = readDeviceSyncSourceCanonicalCoverageBoundary(resourceAvailabilitySummary, resource);
|
|
5610
|
+
const coverageAdvanced = !existingCoverage
|
|
5611
|
+
|| maxJunctionCanonicalCoverageBoundary(resource, existingCoverage, coverage.coverageBoundary) !== existingCoverage;
|
|
5612
|
+
if (coverageAdvanced) {
|
|
5613
|
+
resourceAvailabilitySummary[key] = coverage.coverageBoundary;
|
|
5614
|
+
changed = true;
|
|
5615
|
+
}
|
|
5616
|
+
const finalizedAtKey = buildDeviceSyncSourceCanonicalCoverageFinalizedAtKey(resource);
|
|
5617
|
+
if (!finalizedAtKey) {
|
|
5618
|
+
continue;
|
|
5619
|
+
}
|
|
5620
|
+
const existingFinalizedAt = readDeviceSyncSourceCanonicalCoverageFinalizedAt(resourceAvailabilitySummary, resource);
|
|
5621
|
+
if (coverageAdvanced) {
|
|
5622
|
+
resourceAvailabilitySummary[finalizedAtKey] = coverage.coverageFinalizedAt ?? null;
|
|
5623
|
+
continue;
|
|
5624
|
+
}
|
|
5625
|
+
if (existingCoverage !== coverage.coverageBoundary
|
|
5626
|
+
|| !coverage.coverageFinalizedAt
|
|
5627
|
+
|| existingFinalizedAt) {
|
|
5628
|
+
continue;
|
|
5629
|
+
}
|
|
5630
|
+
resourceAvailabilitySummary[finalizedAtKey] = coverage.coverageFinalizedAt;
|
|
5631
|
+
changed = true;
|
|
5632
|
+
}
|
|
5633
|
+
if (!changed) {
|
|
5634
|
+
return;
|
|
5635
|
+
}
|
|
5636
|
+
const sourceInstanceKey = legacy.sourceInstanceKey
|
|
5637
|
+
?? buildJunctionProviderSourceInstanceKey({
|
|
5638
|
+
connectionId: context.account.id,
|
|
5639
|
+
sourceProviderSlug: JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG,
|
|
5640
|
+
});
|
|
5641
|
+
if (!sourceInstanceKey) {
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
await context.upsertConnectionSource({
|
|
5645
|
+
sourceInstanceKey,
|
|
5646
|
+
sourceProviderSlug: legacy.sourceProviderSlug,
|
|
5647
|
+
...(legacy.displayName === undefined
|
|
5648
|
+
? {}
|
|
5649
|
+
: { displayName: legacy.displayName }),
|
|
5650
|
+
status: legacy.status,
|
|
5651
|
+
resourceAvailabilitySummary,
|
|
5652
|
+
...(legacy.lastErrorCode === undefined
|
|
5653
|
+
? {}
|
|
5654
|
+
: { lastErrorCode: legacy.lastErrorCode }),
|
|
5655
|
+
...(legacy.lastErrorMessage === undefined
|
|
5656
|
+
? {}
|
|
5657
|
+
: { lastErrorMessage: legacy.lastErrorMessage }),
|
|
5658
|
+
...(legacy.firstSeenAt ? { firstSeenAt: legacy.firstSeenAt } : {}),
|
|
5659
|
+
lastSeenAt: context.now,
|
|
5660
|
+
});
|
|
5661
|
+
}
|
|
4617
5662
|
function isJunctionImportRecordAdmitted(value, sourceReferences, sources, hasPendingSourceAdmission, allowUnlistedSources, sourceStatusRequirement) {
|
|
4618
5663
|
const record = readPlainObject(value);
|
|
4619
5664
|
if (!record) {
|
|
4620
5665
|
return true;
|
|
4621
5666
|
}
|
|
4622
5667
|
const fallback = readJunctionSourceReference(record, sourceReferences);
|
|
4623
|
-
const sourceProviderSlug =
|
|
5668
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(resolveJunctionOrigin(record, fallback).sourceProviderSlug);
|
|
4624
5669
|
if (sourceProviderSlug) {
|
|
4625
|
-
|
|
5670
|
+
if (!isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnlistedSources, sourceStatusRequirement)) {
|
|
5671
|
+
return false;
|
|
5672
|
+
}
|
|
5673
|
+
return true;
|
|
4626
5674
|
}
|
|
4627
5675
|
return !hasPendingSourceAdmission || !hasJunctionSourceReferenceIdentity(record);
|
|
4628
5676
|
}
|
|
@@ -4678,7 +5726,7 @@ function sanitizeJunctionImportSnapshotValue(value, sourceReferences, options =
|
|
|
4678
5726
|
(unresolvedPreservedSourceReference ? undefined : origin.sourceInstanceId);
|
|
4679
5727
|
const sourceProviderSlug = normalizeProviderSlug(origin.sourceProviderSlug);
|
|
4680
5728
|
const projectedSourceIdentity = options.sourceIdentities
|
|
4681
|
-
?.find((identity) =>
|
|
5729
|
+
?.find((identity) => areJunctionProviderSlugsDataEquivalent(sourceProviderSlug, identity.sourceProviderSlug));
|
|
4682
5730
|
return stripUndefined({
|
|
4683
5731
|
...sanitized,
|
|
4684
5732
|
sourceProviderSlug: projectedSourceIdentity?.sourceProviderSlug
|
|
@@ -4791,24 +5839,28 @@ function dedupeJunctionTimeseriesRecords(resource, records) {
|
|
|
4791
5839
|
providerRowKey: buildJunctionTimeseriesProviderRowKey(resource, record),
|
|
4792
5840
|
record,
|
|
4793
5841
|
}));
|
|
4794
|
-
const
|
|
5842
|
+
const contentKeysByStableIdentity = new Map();
|
|
4795
5843
|
for (const candidate of candidates) {
|
|
4796
|
-
|
|
5844
|
+
const stableIdentity = candidate.providerRowKey
|
|
5845
|
+
?? (isJunctionBodyTimeseriesResource(resource) ? candidate.key : null);
|
|
5846
|
+
if (!stableIdentity || !candidate.contentKey) {
|
|
4797
5847
|
continue;
|
|
4798
5848
|
}
|
|
4799
|
-
const contentKeys =
|
|
5849
|
+
const contentKeys = contentKeysByStableIdentity.get(stableIdentity)
|
|
4800
5850
|
?? new Set();
|
|
4801
5851
|
contentKeys.add(candidate.contentKey);
|
|
4802
|
-
|
|
5852
|
+
contentKeysByStableIdentity.set(stableIdentity, contentKeys);
|
|
4803
5853
|
}
|
|
4804
|
-
const
|
|
5854
|
+
const conflictingStableIdentities = new Set([...contentKeysByStableIdentity.entries()]
|
|
4805
5855
|
.filter(([, contentKeys]) => contentKeys.size > 1)
|
|
4806
|
-
.map(([
|
|
5856
|
+
.map(([stableIdentity]) => stableIdentity));
|
|
4807
5857
|
const seen = new Set();
|
|
4808
5858
|
const deduped = [];
|
|
4809
5859
|
for (const candidate of candidates) {
|
|
4810
|
-
|
|
4811
|
-
|
|
5860
|
+
const stableIdentity = candidate.providerRowKey
|
|
5861
|
+
?? (isJunctionBodyTimeseriesResource(resource) ? candidate.key : null);
|
|
5862
|
+
if (stableIdentity
|
|
5863
|
+
&& conflictingStableIdentities.has(stableIdentity)) {
|
|
4812
5864
|
continue;
|
|
4813
5865
|
}
|
|
4814
5866
|
if (candidate.key && seen.has(candidate.key)) {
|
|
@@ -4828,7 +5880,7 @@ function dedupeJunctionTimeseriesSnapshotRecords(snapshot) {
|
|
|
4828
5880
|
}
|
|
4829
5881
|
return deduped;
|
|
4830
5882
|
}
|
|
4831
|
-
function filterJunctionTimeseriesRecordsToWindow(resource, records, windowStart, windowEnd, dateQueryFormat) {
|
|
5883
|
+
function filterJunctionTimeseriesRecordsToWindow(resource, records, windowStart, windowEnd, dateQueryFormat, authorizedLocalDay, vaultTimeZone) {
|
|
4832
5884
|
const startMs = Date.parse(windowStart);
|
|
4833
5885
|
const endMs = Date.parse(windowEnd);
|
|
4834
5886
|
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) {
|
|
@@ -4841,9 +5893,41 @@ function filterJunctionTimeseriesRecordsToWindow(resource, records, windowStart,
|
|
|
4841
5893
|
if (!entry) {
|
|
4842
5894
|
return true;
|
|
4843
5895
|
}
|
|
4844
|
-
const rawTimestamp =
|
|
5896
|
+
const rawTimestamp = isJunctionBodyTimeseriesResource(resource)
|
|
5897
|
+
? resolveJunctionTimeseriesRecordTimestampForResource(resource, entry)
|
|
5898
|
+
: resolveJunctionTimeseriesRecordRawTimestamp(entry, preferIntervalStart);
|
|
4845
5899
|
if (!rawTimestamp) {
|
|
4846
|
-
return
|
|
5900
|
+
return !isJunctionBodyTimeseriesResource(resource);
|
|
5901
|
+
}
|
|
5902
|
+
if (authorizedLocalDay) {
|
|
5903
|
+
// Drop only rows the strict complete-day parse proves belong to a
|
|
5904
|
+
// different valid calendar day, and only when the raw shape and the
|
|
5905
|
+
// record's declared semantics agree on absoluteness. Anything the
|
|
5906
|
+
// parse rejects — and any raw-versus-semantics disagreement, which the
|
|
5907
|
+
// importer must see to fail the day closed — stays in the collection
|
|
5908
|
+
// so a contradictory row can never be laundered into a partial or
|
|
5909
|
+
// empty replacement by being discarded as adjacent-day overlap.
|
|
5910
|
+
const parsed = parseJunctionCompleteDayTimestamp(rawTimestamp);
|
|
5911
|
+
if (!parsed) {
|
|
5912
|
+
return true;
|
|
5913
|
+
}
|
|
5914
|
+
if ((parsed.kind === "absolute")
|
|
5915
|
+
!== hasAbsoluteJunctionTimeseriesTimestamp(rawTimestamp, entry)) {
|
|
5916
|
+
return true;
|
|
5917
|
+
}
|
|
5918
|
+
if (parsed.kind === "absolute") {
|
|
5919
|
+
return toLocalDayKey(parsed.instant, authorizedLocalDay.timeZone)
|
|
5920
|
+
=== authorizedLocalDay.dayKey;
|
|
5921
|
+
}
|
|
5922
|
+
return parsed.dayKey === authorizedLocalDay.dayKey;
|
|
5923
|
+
}
|
|
5924
|
+
if (!hasAbsoluteJunctionTimeseriesTimestamp(rawTimestamp, entry) && vaultTimeZone) {
|
|
5925
|
+
const rawDayKey = /^\d{4}-\d{2}-\d{2}/u.exec(rawTimestamp)?.[0];
|
|
5926
|
+
const windowStartDayKey = toLocalDayKey(windowStart, vaultTimeZone);
|
|
5927
|
+
const windowEndDayKey = toLocalDayKey(new Date(endMs - 1).toISOString(), vaultTimeZone);
|
|
5928
|
+
return Boolean(rawDayKey
|
|
5929
|
+
&& rawDayKey >= windowStartDayKey
|
|
5930
|
+
&& rawDayKey <= windowEndDayKey);
|
|
4847
5931
|
}
|
|
4848
5932
|
if (dateQueryFormat === "date"
|
|
4849
5933
|
&& policy?.normalizationMode === "daily_aggregate") {
|
|
@@ -4886,7 +5970,7 @@ function buildJunctionTimeseriesRecordKey(resource, record) {
|
|
|
4886
5970
|
}
|
|
4887
5971
|
const origin = resolveJunctionOrigin(entry);
|
|
4888
5972
|
const sourceProviderSlug = normalizeProviderSlug(origin.sourceProviderSlug);
|
|
4889
|
-
const timestamp =
|
|
5973
|
+
const timestamp = resolveJunctionTimeseriesRecordTimestampForResource(resource, entry);
|
|
4890
5974
|
if (!sourceProviderSlug || !timestamp) {
|
|
4891
5975
|
return null;
|
|
4892
5976
|
}
|
|
@@ -4936,7 +6020,7 @@ function buildJunctionTimeseriesRecordContentKey(resource, record) {
|
|
|
4936
6020
|
}
|
|
4937
6021
|
return JSON.stringify([
|
|
4938
6022
|
"junction-timeseries-content",
|
|
4939
|
-
|
|
6023
|
+
resolveJunctionTimeseriesRecordTimestampForResource(resource, entry) ?? "",
|
|
4940
6024
|
normalizeString(entry.recordedAt ?? entry.recorded_at ?? entry.updatedAt ?? entry.updated_at)
|
|
4941
6025
|
?? "",
|
|
4942
6026
|
String(entry.timeZoneOffsetMinutes
|
|
@@ -4988,10 +6072,50 @@ function readJunctionTimeseriesProviderRowId(resource, entry) {
|
|
|
4988
6072
|
// Sparse records without stable provider row ids retain their semantic tuple
|
|
4989
6073
|
// identity so same-time rows remain distinct when their canonical bodies differ.
|
|
4990
6074
|
function junctionTimeseriesRecordValueIdentity(resource, entry, includeProviderRowId = true) {
|
|
6075
|
+
if (JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource)) {
|
|
6076
|
+
const providerRowId = [
|
|
6077
|
+
"id",
|
|
6078
|
+
"resourceId",
|
|
6079
|
+
"resource_id",
|
|
6080
|
+
"externalId",
|
|
6081
|
+
"external_id",
|
|
6082
|
+
"providerId",
|
|
6083
|
+
"provider_id",
|
|
6084
|
+
"recordId",
|
|
6085
|
+
"record_id",
|
|
6086
|
+
"sampleId",
|
|
6087
|
+
"sample_id",
|
|
6088
|
+
].map((key) => normalizeString(entry[key])).find(Boolean) ?? "";
|
|
6089
|
+
const value = resource === "blood_oxygen"
|
|
6090
|
+
? firstJunctionTimeseriesIdentityValue(entry, [
|
|
6091
|
+
"value",
|
|
6092
|
+
"spo2",
|
|
6093
|
+
"spO2",
|
|
6094
|
+
"bloodOxygen",
|
|
6095
|
+
"blood_oxygen",
|
|
6096
|
+
"oxygenSaturation",
|
|
6097
|
+
"oxygen_saturation",
|
|
6098
|
+
])
|
|
6099
|
+
: firstJunctionTimeseriesIdentityValue(entry, [
|
|
6100
|
+
"value",
|
|
6101
|
+
"stressLevel",
|
|
6102
|
+
"stress_level",
|
|
6103
|
+
"averageStressLevel",
|
|
6104
|
+
"average_stress_level",
|
|
6105
|
+
"stress.average",
|
|
6106
|
+
"stressLevelValue",
|
|
6107
|
+
"stress_level_value",
|
|
6108
|
+
"score",
|
|
6109
|
+
]);
|
|
6110
|
+
return [providerRowId, value];
|
|
6111
|
+
}
|
|
4991
6112
|
if (!junctionTimeseriesRecordNeedsSemanticIdentity(resource)) {
|
|
4992
6113
|
return [];
|
|
4993
6114
|
}
|
|
4994
6115
|
const providerRowId = readJunctionTimeseriesProviderRowId(resource, entry) ?? "";
|
|
6116
|
+
if (isJunctionBodyTimeseriesResource(resource) && includeProviderRowId) {
|
|
6117
|
+
return providerRowId ? [providerRowId] : [];
|
|
6118
|
+
}
|
|
4995
6119
|
if (resource === "weight") {
|
|
4996
6120
|
if (includeProviderRowId && providerRowId) {
|
|
4997
6121
|
return [providerRowId];
|
|
@@ -5121,12 +6245,63 @@ function junctionTimeseriesRecordValueIdentity(resource, entry, includeProviderR
|
|
|
5121
6245
|
String(entry.delivery_mode ?? entry.deliveryMode ?? ""),
|
|
5122
6246
|
];
|
|
5123
6247
|
}
|
|
6248
|
+
function firstJunctionTimeseriesIdentityValue(entry, paths) {
|
|
6249
|
+
for (const path of paths) {
|
|
6250
|
+
let value = entry;
|
|
6251
|
+
for (const segment of path.split(".")) {
|
|
6252
|
+
value = readPlainObject(value)?.[segment];
|
|
6253
|
+
}
|
|
6254
|
+
const numericValue = typeof value === "number"
|
|
6255
|
+
? value
|
|
6256
|
+
: Number(normalizeString(value));
|
|
6257
|
+
if (Number.isFinite(numericValue)) {
|
|
6258
|
+
return String(numericValue);
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
return "";
|
|
6262
|
+
}
|
|
5124
6263
|
function resolveJunctionTimeseriesRecordTimestamp(record, preferIntervalStart = false) {
|
|
5125
6264
|
const rawTimestamp = resolveJunctionTimeseriesRecordRawTimestamp(record, preferIntervalStart);
|
|
5126
6265
|
return rawTimestamp
|
|
5127
6266
|
? toIsoTimestampIfValid(rawTimestamp) ?? rawTimestamp
|
|
5128
6267
|
: null;
|
|
5129
6268
|
}
|
|
6269
|
+
const JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES = new Set([
|
|
6270
|
+
"fat",
|
|
6271
|
+
"weight",
|
|
6272
|
+
]);
|
|
6273
|
+
const JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES = new Set([
|
|
6274
|
+
"body_mass_index",
|
|
6275
|
+
"lean_body_mass",
|
|
6276
|
+
"waist_circumference",
|
|
6277
|
+
]);
|
|
6278
|
+
function isJunctionBodyTimeseriesResource(resource) {
|
|
6279
|
+
return resource !== null
|
|
6280
|
+
&& resource !== undefined
|
|
6281
|
+
&& (JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES.has(resource)
|
|
6282
|
+
|| JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES.has(resource));
|
|
6283
|
+
}
|
|
6284
|
+
function resolveJunctionTimeseriesRecordTimestampForResource(resource, record) {
|
|
6285
|
+
if (JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource)) {
|
|
6286
|
+
const rawTimestamp = resolveJunctionTimeseriesRecordRawTimestamp(record);
|
|
6287
|
+
if (!rawTimestamp || !hasAbsoluteJunctionTimeseriesTimestamp(rawTimestamp, record)) {
|
|
6288
|
+
return rawTimestamp;
|
|
6289
|
+
}
|
|
6290
|
+
return toIsoTimestampIfValid(rawTimestamp) ?? rawTimestamp;
|
|
6291
|
+
}
|
|
6292
|
+
if (JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES.has(resource)) {
|
|
6293
|
+
return normalizeJunctionCanonicalBodyTimestamp(record.timestamp) ?? null;
|
|
6294
|
+
}
|
|
6295
|
+
if (JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES.has(resource)) {
|
|
6296
|
+
const start = normalizeJunctionCanonicalBodyTimestamp(record.start);
|
|
6297
|
+
const end = normalizeJunctionCanonicalBodyTimestamp(record.end);
|
|
6298
|
+
if (!start || !end || Date.parse(end) <= Date.parse(start)) {
|
|
6299
|
+
return null;
|
|
6300
|
+
}
|
|
6301
|
+
return start;
|
|
6302
|
+
}
|
|
6303
|
+
return resolveJunctionTimeseriesRecordTimestamp(record);
|
|
6304
|
+
}
|
|
5130
6305
|
function resolveJunctionTimeseriesRecordRawTimestamp(record, preferIntervalStart = false) {
|
|
5131
6306
|
const intervalKeys = preferIntervalStart
|
|
5132
6307
|
? ["sessionStart", "session_start", "timeStart", "time_start", "start", "startAt", "start_at", "sessionEnd", "session_end", "timeEnd", "time_end", "end", "endAt", "end_at"]
|
|
@@ -5159,6 +6334,16 @@ function isJunctionFloatingCalendarTimestamp(value) {
|
|
|
5159
6334
|
}
|
|
5160
6335
|
return /^\d{4}-\d{2}-\d{2}(?:$|[ t]\d{2}:\d{2})/iu.test(normalized);
|
|
5161
6336
|
}
|
|
6337
|
+
function hasAbsoluteJunctionTimeseriesTimestamp(timestamp, record) {
|
|
6338
|
+
const explicitSemantics = normalizeString(record.timestampSemantics ?? record.timestamp_semantics);
|
|
6339
|
+
if (explicitSemantics === "floating") {
|
|
6340
|
+
return false;
|
|
6341
|
+
}
|
|
6342
|
+
if (explicitSemantics === "utc" || explicitSemantics === "offset") {
|
|
6343
|
+
return true;
|
|
6344
|
+
}
|
|
6345
|
+
return /(?:z|[+-]\d{2}:?\d{2})$/iu.test(timestamp.trim());
|
|
6346
|
+
}
|
|
5162
6347
|
function buildJunctionRedirectUrl(callbackUrl, state) {
|
|
5163
6348
|
const url = new URL(callbackUrl);
|
|
5164
6349
|
url.searchParams.set("murph_state", state);
|
|
@@ -5318,6 +6503,7 @@ function resolveNextFullJobTimeseriesContinuation(input) {
|
|
|
5318
6503
|
timeseriesResourceCursor: input.resource,
|
|
5319
6504
|
timeseriesWindowHours: input.timeseriesWindowHours,
|
|
5320
6505
|
workoutStreamCursor: null,
|
|
6506
|
+
workoutStreamEmptySeen: false,
|
|
5321
6507
|
};
|
|
5322
6508
|
}
|
|
5323
6509
|
const nextResource = input.resources[input.resources.indexOf(input.resource) + 1] ?? null;
|
|
@@ -5327,6 +6513,7 @@ function resolveNextFullJobTimeseriesContinuation(input) {
|
|
|
5327
6513
|
timeseriesResourceCursor: nextResource,
|
|
5328
6514
|
timeseriesWindowHours: 24,
|
|
5329
6515
|
workoutStreamCursor: null,
|
|
6516
|
+
workoutStreamEmptySeen: false,
|
|
5330
6517
|
}
|
|
5331
6518
|
: null;
|
|
5332
6519
|
}
|
|
@@ -5339,12 +6526,6 @@ function readDeferredEmptyBackfillAttempts(followUp) {
|
|
|
5339
6526
|
}
|
|
5340
6527
|
return 0;
|
|
5341
6528
|
}
|
|
5342
|
-
function readBackfillTimeseriesCursor(job, ownerWindow) {
|
|
5343
|
-
const cursor = toIsoTimestampIfValid(job.payload.timeseriesCursor);
|
|
5344
|
-
return cursor && isTimestampInHalfOpenWindow(cursor, ownerWindow)
|
|
5345
|
-
? cursor
|
|
5346
|
-
: null;
|
|
5347
|
-
}
|
|
5348
6529
|
function isTimestampInHalfOpenWindow(timestamp, window) {
|
|
5349
6530
|
const timestampMs = Date.parse(timestamp);
|
|
5350
6531
|
const windowStartMs = Date.parse(window.windowStart);
|
|
@@ -5382,6 +6563,96 @@ function shouldImportClosedTimeseriesForReconcile(lastSyncCompletedAt, windowEnd
|
|
|
5382
6563
|
|| !Number.isFinite(windowEndMs)
|
|
5383
6564
|
|| lastCompletedClosedDayMs < windowEndMs;
|
|
5384
6565
|
}
|
|
6566
|
+
function normalizeJunctionTemporalReconcileDays(value) {
|
|
6567
|
+
if (!Number.isFinite(value)) {
|
|
6568
|
+
return DEFAULT_RECONCILE_DAYS;
|
|
6569
|
+
}
|
|
6570
|
+
return Math.max(1, Math.min(Math.floor(value), JUNCTION_MAX_TEMPORAL_RECONCILE_DAYS));
|
|
6571
|
+
}
|
|
6572
|
+
function buildLatestAuthoritativeDailyWindows(input) {
|
|
6573
|
+
const nowMs = Date.parse(input.now);
|
|
6574
|
+
const windowEndMs = Date.parse(input.windowEnd);
|
|
6575
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(windowEndMs)) {
|
|
6576
|
+
return [];
|
|
6577
|
+
}
|
|
6578
|
+
const latestAuthoritativeDayKey = latestAuthoritativeVaultDayKey(input.now, input.timeZone);
|
|
6579
|
+
if (!latestAuthoritativeDayKey) {
|
|
6580
|
+
return [];
|
|
6581
|
+
}
|
|
6582
|
+
let latestWindowBoundDayKey = toLocalDayKey(new Date(windowEndMs - 1).toISOString(), input.timeZone);
|
|
6583
|
+
for (let offset = 0; offset < 4; offset += 1) {
|
|
6584
|
+
const candidate = resolveVaultLocalDayWindow(latestWindowBoundDayKey, input.timeZone);
|
|
6585
|
+
if (candidate && Date.parse(candidate.windowEnd) <= windowEndMs) {
|
|
6586
|
+
break;
|
|
6587
|
+
}
|
|
6588
|
+
latestWindowBoundDayKey = addIsoDateDays(latestWindowBoundDayKey, -1);
|
|
6589
|
+
}
|
|
6590
|
+
const newestDayKey = latestAuthoritativeDayKey < latestWindowBoundDayKey
|
|
6591
|
+
? latestAuthoritativeDayKey
|
|
6592
|
+
: latestWindowBoundDayKey;
|
|
6593
|
+
const newestWindow = resolveVaultLocalDayWindow(newestDayKey, input.timeZone);
|
|
6594
|
+
if (!newestWindow || Date.parse(newestWindow.windowEnd) > windowEndMs) {
|
|
6595
|
+
return [];
|
|
6596
|
+
}
|
|
6597
|
+
const windows = [];
|
|
6598
|
+
for (let offset = 0; offset < input.horizonDays; offset += 1) {
|
|
6599
|
+
const dayKey = addIsoDateDays(newestDayKey, -offset);
|
|
6600
|
+
const window = resolveVaultLocalDayWindow(dayKey, input.timeZone);
|
|
6601
|
+
if (!window) {
|
|
6602
|
+
continue;
|
|
6603
|
+
}
|
|
6604
|
+
windows.push({
|
|
6605
|
+
dayKey,
|
|
6606
|
+
...window,
|
|
6607
|
+
});
|
|
6608
|
+
}
|
|
6609
|
+
return windows;
|
|
6610
|
+
}
|
|
6611
|
+
function buildJunctionTemporalAuthorityJobs(input) {
|
|
6612
|
+
const temporalResources = input.resources.filter((resource) => JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource));
|
|
6613
|
+
return input.windows.flatMap((window, windowIndex) => temporalResources.map((resource) => {
|
|
6614
|
+
const payload = {
|
|
6615
|
+
resource,
|
|
6616
|
+
resourceCategory: "timeseries",
|
|
6617
|
+
temporalAuthorityTimeZone: input.timeZone,
|
|
6618
|
+
windowEnd: window.windowEnd,
|
|
6619
|
+
windowStart: window.windowStart,
|
|
6620
|
+
};
|
|
6621
|
+
return {
|
|
6622
|
+
// The store orders equal-priority work by availability. Give each older
|
|
6623
|
+
// day a stable millisecond tier so restarts drain the newest missing day
|
|
6624
|
+
// first without outranking webhook work or introducing another cursor.
|
|
6625
|
+
availableAt: addMilliseconds(input.now, windowIndex),
|
|
6626
|
+
dedupeKey: `${JUNCTION_TEMPORAL_AUTHORITY_DEDUPE_PREFIX}v${JUNCTION_TEMPORAL_AUTHORITY_JOB_VERSION}:${sha256Text(JSON.stringify([input.timeZone, resource, window.dayKey]))}`,
|
|
6627
|
+
kind: "resource",
|
|
6628
|
+
payload,
|
|
6629
|
+
priority: JUNCTION_HISTORICAL_BACKFILL_PRIORITY,
|
|
6630
|
+
};
|
|
6631
|
+
}));
|
|
6632
|
+
}
|
|
6633
|
+
function readJunctionTemporalAuthorityJob(job) {
|
|
6634
|
+
if (job.kind !== "resource") {
|
|
6635
|
+
return null;
|
|
6636
|
+
}
|
|
6637
|
+
// The retained authority timezone is the only persisted authority state;
|
|
6638
|
+
// the day key derives from the stored exact window so hosted wake hints
|
|
6639
|
+
// cannot restore a temporal child with partial authority. Legacy payloads
|
|
6640
|
+
// may still carry temporalAuthorityDayKey; it is ignored.
|
|
6641
|
+
const timeZone = normalizeString(job.payload.temporalAuthorityTimeZone);
|
|
6642
|
+
const resource = normalizeJunctionResourceName(job.payload.resource);
|
|
6643
|
+
const windowStart = normalizeString(job.payload.windowStart);
|
|
6644
|
+
if (!timeZone
|
|
6645
|
+
|| !resource
|
|
6646
|
+
|| !JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource)
|
|
6647
|
+
|| !windowStart) {
|
|
6648
|
+
return null;
|
|
6649
|
+
}
|
|
6650
|
+
const dayKey = toLocalDayKey(windowStart, timeZone);
|
|
6651
|
+
if (!dayKey || !/^\d{4}-\d{2}-\d{2}$/u.test(dayKey)) {
|
|
6652
|
+
return null;
|
|
6653
|
+
}
|
|
6654
|
+
return { dayKey, resource, timeZone };
|
|
6655
|
+
}
|
|
5385
6656
|
function resolveLatestGloballyClosedProviderDayWindow(windowStart, windowEnd, asOf) {
|
|
5386
6657
|
const flooredWindowStartMs = Date.parse(floorUtcDayTimestamp(windowStart));
|
|
5387
6658
|
const globallyClosedEndMs = resolveGloballyClosedProviderDayEnd(windowEnd, asOf);
|
|
@@ -5414,14 +6685,76 @@ function buildClosedDailyWindows(windowStart, windowEnd) {
|
|
|
5414
6685
|
if (chunkEndMs <= chunkStartMs) {
|
|
5415
6686
|
break;
|
|
5416
6687
|
}
|
|
6688
|
+
const dailyWindowStart = new Date(chunkStartMs).toISOString();
|
|
5417
6689
|
windows.push({
|
|
5418
|
-
|
|
6690
|
+
dayKey: dailyWindowStart.slice(0, 10),
|
|
6691
|
+
windowStart: dailyWindowStart,
|
|
5419
6692
|
windowEnd: new Date(chunkEndMs).toISOString(),
|
|
5420
6693
|
});
|
|
5421
6694
|
chunkStartMs = chunkEndMs;
|
|
5422
6695
|
}
|
|
5423
6696
|
return windows;
|
|
5424
6697
|
}
|
|
6698
|
+
function latestAuthoritativeVaultDayKey(at, timeZone) {
|
|
6699
|
+
const atMs = Date.parse(at);
|
|
6700
|
+
if (!Number.isFinite(atMs)) {
|
|
6701
|
+
return null;
|
|
6702
|
+
}
|
|
6703
|
+
let candidateDayKey = toLocalDayKey(at, timeZone);
|
|
6704
|
+
for (let offset = 0; offset < 4; offset += 1) {
|
|
6705
|
+
const candidate = resolveVaultLocalDayWindow(candidateDayKey, timeZone);
|
|
6706
|
+
if (candidate
|
|
6707
|
+
&& Date.parse(candidate.windowEnd) + JUNCTION_TEMPORAL_AUTHORITY_LAG_MS <= atMs) {
|
|
6708
|
+
return candidateDayKey;
|
|
6709
|
+
}
|
|
6710
|
+
candidateDayKey = addIsoDateDays(candidateDayKey, -1);
|
|
6711
|
+
}
|
|
6712
|
+
return null;
|
|
6713
|
+
}
|
|
6714
|
+
function resolveVaultLocalDayWindow(dayKey, timeZone) {
|
|
6715
|
+
const startMs = findVaultLocalDayStartMs(dayKey, timeZone);
|
|
6716
|
+
const endMs = findVaultLocalDayStartMs(addIsoDateDays(dayKey, 1), timeZone);
|
|
6717
|
+
return startMs === null || endMs === null || startMs >= endMs
|
|
6718
|
+
? null
|
|
6719
|
+
: {
|
|
6720
|
+
windowStart: new Date(startMs).toISOString(),
|
|
6721
|
+
windowEnd: new Date(endMs).toISOString(),
|
|
6722
|
+
};
|
|
6723
|
+
}
|
|
6724
|
+
function findVaultLocalDayStartMs(dayKey, timeZone) {
|
|
6725
|
+
const anchorMs = Date.parse(`${dayKey}T00:00:00.000Z`);
|
|
6726
|
+
if (!Number.isFinite(anchorMs)) {
|
|
6727
|
+
return null;
|
|
6728
|
+
}
|
|
6729
|
+
const searchStartMs = anchorMs - 2 * TIMESERIES_CHUNK_MS;
|
|
6730
|
+
const searchEndMs = anchorMs + 2 * TIMESERIES_CHUNK_MS;
|
|
6731
|
+
let matchingMs = null;
|
|
6732
|
+
for (let candidateMs = searchStartMs; candidateMs <= searchEndMs; candidateMs += 60 * 60_000) {
|
|
6733
|
+
if (toLocalDayKey(new Date(candidateMs).toISOString(), timeZone) === dayKey) {
|
|
6734
|
+
matchingMs = candidateMs;
|
|
6735
|
+
break;
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
if (matchingMs === null) {
|
|
6739
|
+
return null;
|
|
6740
|
+
}
|
|
6741
|
+
let lowerMs = matchingMs - 60 * 60_000;
|
|
6742
|
+
let upperMs = matchingMs;
|
|
6743
|
+
while (upperMs - lowerMs > 1) {
|
|
6744
|
+
const middleMs = Math.floor((lowerMs + upperMs) / 2);
|
|
6745
|
+
if (toLocalDayKey(new Date(middleMs).toISOString(), timeZone) < dayKey) {
|
|
6746
|
+
lowerMs = middleMs;
|
|
6747
|
+
}
|
|
6748
|
+
else {
|
|
6749
|
+
upperMs = middleMs;
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
return upperMs;
|
|
6753
|
+
}
|
|
6754
|
+
function addIsoDateDays(dayKey, days) {
|
|
6755
|
+
const value = Date.parse(`${dayKey}T00:00:00.000Z`);
|
|
6756
|
+
return new Date(value + days * TIMESERIES_CHUNK_MS).toISOString().slice(0, 10);
|
|
6757
|
+
}
|
|
5425
6758
|
function resolveGloballyClosedProviderDayEnd(windowEnd, asOf) {
|
|
5426
6759
|
const requestedClosedEndMs = Date.parse(floorUtcDayTimestamp(windowEnd));
|
|
5427
6760
|
const asOfMs = Date.parse(asOf);
|
|
@@ -5480,13 +6813,11 @@ function hasJunctionHistoricalBackfillSummaryRecords(evidence) {
|
|
|
5480
6813
|
return evidence.some(({ resource }) => isJunctionHistoricalBackfillCompletionSummaryResource(resource));
|
|
5481
6814
|
}
|
|
5482
6815
|
function resolveJunctionHistoricalPullReadiness(input) {
|
|
5483
|
-
const sourceProviderSlug =
|
|
5484
|
-
?? normalizeProviderSlug(input.sourceProviderSlug);
|
|
6816
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
|
|
5485
6817
|
if (!sourceProviderSlug || !input.snapshot?.matchedUser) {
|
|
5486
6818
|
return "unavailable";
|
|
5487
6819
|
}
|
|
5488
|
-
const source = input.snapshot.sources.find((entry) => (
|
|
5489
|
-
?? normalizeProviderSlug(entry.sourceProviderSlug)) === sourceProviderSlug);
|
|
6820
|
+
const source = input.snapshot.sources.find((entry) => canonicalizeJunctionProviderSlug(entry.sourceProviderSlug) === sourceProviderSlug);
|
|
5490
6821
|
if (!source) {
|
|
5491
6822
|
return "unavailable";
|
|
5492
6823
|
}
|
|
@@ -5586,7 +6917,7 @@ function evaluateJunctionHistoricalBackfillCoverage(summaryEvidence, sourceProvi
|
|
|
5586
6917
|
continue;
|
|
5587
6918
|
}
|
|
5588
6919
|
const coveredBySummary = summaryEvidence.some((entry) => entry.resource === resource
|
|
5589
|
-
&&
|
|
6920
|
+
&& canonicalizeJunctionProviderSlug(entry.sourceProviderSlug) === providerSlug);
|
|
5590
6921
|
const coveredByPush = hasJunctionHistoricalBackfillEvidence(historicalPushEvidence, providerSlug, resource, window.windowStart, window.windowEnd);
|
|
5591
6922
|
if (!coveredBySummary && !coveredByPush) {
|
|
5592
6923
|
hasPendingResource = true;
|
|
@@ -5610,16 +6941,8 @@ function evaluateJunctionHistoricalBackfillCoverage(summaryEvidence, sourceProvi
|
|
|
5610
6941
|
.sort((left, right) => left.localeCompare(right)),
|
|
5611
6942
|
};
|
|
5612
6943
|
}
|
|
5613
|
-
function canonicalizeJunctionHistoricalProviderSlug(value) {
|
|
5614
|
-
const providerSlug = normalizeProviderSlug(value);
|
|
5615
|
-
if (!providerSlug) {
|
|
5616
|
-
return null;
|
|
5617
|
-
}
|
|
5618
|
-
return resolveJunctionDeviceConnectRouteByProviderSlug(providerSlug)?.route.sourceProviderSlug
|
|
5619
|
-
?? null;
|
|
5620
|
-
}
|
|
5621
6944
|
function resolveJunctionHistoricalCoverageProviderSlug(value, providerFilter) {
|
|
5622
|
-
const providerSlug =
|
|
6945
|
+
const providerSlug = canonicalizeJunctionProviderSlug(value);
|
|
5623
6946
|
if (!providerSlug) {
|
|
5624
6947
|
return null;
|
|
5625
6948
|
}
|
|
@@ -5639,7 +6962,7 @@ function readJunctionDirectHistoricalEvidenceWindow(input, connectWindow, eviden
|
|
|
5639
6962
|
if (!providerSlug
|
|
5640
6963
|
|| !isJunctionHistoricalBackfillRequiredSummaryResource(input.resource)
|
|
5641
6964
|
|| !evidence.some((entry) => entry.resource === input.resource
|
|
5642
|
-
&&
|
|
6965
|
+
&& canonicalizeJunctionProviderSlug(entry.sourceProviderSlug) === providerSlug)) {
|
|
5643
6966
|
return null;
|
|
5644
6967
|
}
|
|
5645
6968
|
return connectWindow;
|
|
@@ -5666,11 +6989,23 @@ function isJunctionResourceAvailableInSummary(availabilitySummary, resource) {
|
|
|
5666
6989
|
return Object.entries(availabilitySummary).some(([candidate, availability]) => normalizeJunctionResourceName(candidate) === normalizedResource
|
|
5667
6990
|
&& isJunctionResourceAdvertisedAvailable(availability));
|
|
5668
6991
|
}
|
|
6992
|
+
async function resolveJunctionWorkoutStreamEligibleSources(context, providers) {
|
|
6993
|
+
if (!context.listConnectionSources) {
|
|
6994
|
+
return new Set();
|
|
6995
|
+
}
|
|
6996
|
+
const sources = await context.listConnectionSources();
|
|
6997
|
+
return new Set(projectJunctionSourcesByProviderSlug(context.account.id, providers).flatMap((source) => isJunctionSourceAdmittedForImport(sources, source.sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only")
|
|
6998
|
+
&& source.status === "connected"
|
|
6999
|
+
&& isJunctionResourceAvailableInSummary(source.resourceAvailabilitySummary, "workout_stream")
|
|
7000
|
+
? [source.sourceProviderSlug]
|
|
7001
|
+
: []));
|
|
7002
|
+
}
|
|
5669
7003
|
function isJunctionSourceResourceCurrentlyAvailable(input) {
|
|
5670
|
-
|
|
7004
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
|
|
7005
|
+
if (!sourceProviderSlug) {
|
|
5671
7006
|
return false;
|
|
5672
7007
|
}
|
|
5673
|
-
return projectJunctionSourcesByProviderSlug(input.connectionId, input.providers).some((source) => source.sourceProviderSlug ===
|
|
7008
|
+
return projectJunctionSourcesByProviderSlug(input.connectionId, input.providers).some((source) => source.sourceProviderSlug === sourceProviderSlug
|
|
5674
7009
|
&& source.status === "connected"
|
|
5675
7010
|
&& isJunctionResourceAvailableInSummary(source.resourceAvailabilitySummary, input.resource));
|
|
5676
7011
|
}
|
|
@@ -5760,8 +7095,16 @@ function buildNonConnectHistoricalBackfillFollowUp(input) {
|
|
|
5760
7095
|
return {};
|
|
5761
7096
|
}
|
|
5762
7097
|
const retryAt = addMilliseconds(input.now, retryDelayMs);
|
|
7098
|
+
const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
|
|
5763
7099
|
const retryJob = buildExactWindowJob({
|
|
5764
7100
|
kind: "backfill",
|
|
7101
|
+
payload: stripUndefined({
|
|
7102
|
+
...(sourceProviderSlug ? { sourceProviderSlug } : {}),
|
|
7103
|
+
...(input.historicalProviderRecordsSeen
|
|
7104
|
+
? { historicalProviderRecordsSeen: true }
|
|
7105
|
+
: {}),
|
|
7106
|
+
...(input.historicalRecordsSeen ? { historicalRecordsSeen: true } : {}),
|
|
7107
|
+
}),
|
|
5765
7108
|
priority: Math.max(input.job.priority, JUNCTION_HISTORICAL_BACKFILL_RETRY_PRIORITY),
|
|
5766
7109
|
windowStart: input.windowStart,
|
|
5767
7110
|
windowEnd: input.windowEnd,
|
|
@@ -5952,7 +7295,7 @@ function resolveJunctionHistoricalUnresolvedProviderRecords(job, importResult) {
|
|
|
5952
7295
|
}
|
|
5953
7296
|
return {
|
|
5954
7297
|
identities: [...identities].sort(),
|
|
5955
|
-
withoutStableIdentity: carried.withoutStableIdentity
|
|
7298
|
+
withoutStableIdentity: (!importResult.fetchComplete && carried.withoutStableIdentity)
|
|
5956
7299
|
|| importResult.unresolvedProviderRecordsWithoutStableIdentity,
|
|
5957
7300
|
};
|
|
5958
7301
|
}
|
|
@@ -5972,6 +7315,32 @@ function buildConnectHistoricalBackfillWindow(account, summaryBackfillDays) {
|
|
|
5972
7315
|
windowEnd: floorUtcDayTimestamp(account.connectedAt),
|
|
5973
7316
|
};
|
|
5974
7317
|
}
|
|
7318
|
+
function normalizeJunctionHistoricalProofAuthorization(input) {
|
|
7319
|
+
const firstSeenAt = normalizeString(input?.firstSeenAt);
|
|
7320
|
+
const sourceProviderSlug = normalizeProviderSlug(input?.sourceProviderSlug);
|
|
7321
|
+
if (!firstSeenAt
|
|
7322
|
+
|| !Number.isFinite(Date.parse(firstSeenAt))
|
|
7323
|
+
|| !sourceProviderSlug) {
|
|
7324
|
+
return null;
|
|
7325
|
+
}
|
|
7326
|
+
return { firstSeenAt, sourceProviderSlug };
|
|
7327
|
+
}
|
|
7328
|
+
function readJunctionHistoricalProofAuthorization(job) {
|
|
7329
|
+
if (job.kind !== "backfill") {
|
|
7330
|
+
return null;
|
|
7331
|
+
}
|
|
7332
|
+
return normalizeJunctionHistoricalProofAuthorization({
|
|
7333
|
+
firstSeenAt: normalizeString(job.payload.historicalProofFirstSeenAt) ?? "",
|
|
7334
|
+
sourceProviderSlug: normalizeProviderSlug(job.payload.historicalProofSourceProviderSlug) ?? "",
|
|
7335
|
+
});
|
|
7336
|
+
}
|
|
7337
|
+
async function isJunctionHistoricalProofAuthorizationCurrent(context, authorization) {
|
|
7338
|
+
const sources = context.listConnectionSources
|
|
7339
|
+
? await context.listConnectionSources()
|
|
7340
|
+
: context.account.sources ?? [];
|
|
7341
|
+
return sources.some((source) => source.firstSeenAt === authorization.firstSeenAt
|
|
7342
|
+
&& areJunctionProviderSlugsDataEquivalent(source.sourceProviderSlug, authorization.sourceProviderSlug));
|
|
7343
|
+
}
|
|
5975
7344
|
function buildWindowJob(input) {
|
|
5976
7345
|
const windowEnd = floorUtcDayTimestamp(input.now);
|
|
5977
7346
|
const windowStart = floorUtcDayTimestamp(input.windowStart);
|
|
@@ -5986,13 +7355,26 @@ function buildWindowJob(input) {
|
|
|
5986
7355
|
}
|
|
5987
7356
|
function buildExactWindowJob(input) {
|
|
5988
7357
|
const windowPayload = input.payload;
|
|
5989
|
-
const sourceProviderSlug =
|
|
7358
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(windowPayload?.sourceProviderSlug);
|
|
7359
|
+
const historicalProofAuthorization = normalizeJunctionHistoricalProofAuthorization(windowPayload?.historicalProofFirstSeenAt
|
|
7360
|
+
&& windowPayload.historicalProofSourceProviderSlug
|
|
7361
|
+
? {
|
|
7362
|
+
firstSeenAt: windowPayload.historicalProofFirstSeenAt,
|
|
7363
|
+
sourceProviderSlug: windowPayload.historicalProofSourceProviderSlug,
|
|
7364
|
+
}
|
|
7365
|
+
: null);
|
|
5990
7366
|
const dedupeIdentity = [
|
|
5991
7367
|
"junction",
|
|
5992
7368
|
input.kind,
|
|
5993
7369
|
input.windowStart,
|
|
5994
7370
|
input.windowEnd,
|
|
5995
7371
|
...(sourceProviderSlug ? [sourceProviderSlug] : []),
|
|
7372
|
+
...(historicalProofAuthorization
|
|
7373
|
+
? [
|
|
7374
|
+
historicalProofAuthorization.sourceProviderSlug,
|
|
7375
|
+
historicalProofAuthorization.firstSeenAt,
|
|
7376
|
+
]
|
|
7377
|
+
: []),
|
|
5996
7378
|
];
|
|
5997
7379
|
return {
|
|
5998
7380
|
kind: input.kind,
|
|
@@ -6006,6 +7388,50 @@ function buildExactWindowJob(input) {
|
|
|
6006
7388
|
dedupeKey: sha256Text(JSON.stringify(dedupeIdentity)),
|
|
6007
7389
|
};
|
|
6008
7390
|
}
|
|
7391
|
+
function withJunctionWorkoutStreamEmptyReplay(input) {
|
|
7392
|
+
if (!input.replayWindow
|
|
7393
|
+
|| (input.job.kind === "resource"
|
|
7394
|
+
&& input.job.payload.workoutStreamEmptyReplay === true)) {
|
|
7395
|
+
return input.result;
|
|
7396
|
+
}
|
|
7397
|
+
const replayJob = buildJunctionWorkoutStreamEmptyReplayJob({
|
|
7398
|
+
availableAt: addMilliseconds(input.now, JUNCTION_WORKOUT_STREAM_EMPTY_REPLAY_DELAY_MS),
|
|
7399
|
+
sourceProviderSlug: input.sourceProviderSlug,
|
|
7400
|
+
windowEnd: input.replayWindow.windowEnd,
|
|
7401
|
+
windowStart: input.replayWindow.windowStart,
|
|
7402
|
+
});
|
|
7403
|
+
return {
|
|
7404
|
+
...input.result,
|
|
7405
|
+
scheduledJobs: [
|
|
7406
|
+
...(input.result.scheduledJobs ?? []),
|
|
7407
|
+
replayJob,
|
|
7408
|
+
],
|
|
7409
|
+
};
|
|
7410
|
+
}
|
|
7411
|
+
function buildJunctionWorkoutStreamEmptyReplayJob(input) {
|
|
7412
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
|
|
7413
|
+
const payload = {
|
|
7414
|
+
resource: "workout_stream",
|
|
7415
|
+
resourceCategory: "timeseries",
|
|
7416
|
+
...(sourceProviderSlug ? { sourceProviderSlug } : {}),
|
|
7417
|
+
windowEnd: input.windowEnd,
|
|
7418
|
+
windowStart: input.windowStart,
|
|
7419
|
+
workoutStreamEmptyReplay: true,
|
|
7420
|
+
};
|
|
7421
|
+
return {
|
|
7422
|
+
availableAt: input.availableAt,
|
|
7423
|
+
dedupeKey: sha256Text(JSON.stringify([
|
|
7424
|
+
"junction",
|
|
7425
|
+
"workout-stream-empty-replay",
|
|
7426
|
+
input.windowStart,
|
|
7427
|
+
input.windowEnd,
|
|
7428
|
+
sourceProviderSlug,
|
|
7429
|
+
])),
|
|
7430
|
+
kind: "resource",
|
|
7431
|
+
payload,
|
|
7432
|
+
priority: JUNCTION_HISTORICAL_BACKFILL_RETRY_PRIORITY,
|
|
7433
|
+
};
|
|
7434
|
+
}
|
|
6009
7435
|
function buildJunctionSparseCalendarRefreshJobs(input) {
|
|
6010
7436
|
const targets = [...new Map(input.targets.map((target) => [
|
|
6011
7437
|
JSON.stringify([
|
|
@@ -6116,7 +7542,7 @@ function filterJunctionSparseCalendarRecordsToSource(records, source) {
|
|
|
6116
7542
|
if (!recordSourceProviderSlug) {
|
|
6117
7543
|
throw incompleteJunctionSparseCalendarCollectionError();
|
|
6118
7544
|
}
|
|
6119
|
-
if (!
|
|
7545
|
+
if (!areJunctionProviderSlugsDataEquivalent(recordSourceProviderSlug, sourceProviderSlug)) {
|
|
6120
7546
|
return [];
|
|
6121
7547
|
}
|
|
6122
7548
|
const recordSourceType = normalizeString(origin.sourceType);
|
|
@@ -6183,28 +7609,59 @@ function buildJunctionExtendedTimeseriesBackfillDedupeKey(payload) {
|
|
|
6183
7609
|
|| !policy) {
|
|
6184
7610
|
return null;
|
|
6185
7611
|
}
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
7612
|
+
const sourceLifecycleEpoch = readSafeInteger(payload.sourceLifecycleEpoch);
|
|
7613
|
+
if (sourceLifecycleEpoch === null
|
|
7614
|
+
|| sourceLifecycleEpoch < 1) {
|
|
7615
|
+
return null;
|
|
7616
|
+
}
|
|
7617
|
+
const coverageVersion = readJunctionHistoricalBackfillVersion(payload);
|
|
7618
|
+
if (policy.completion !== "exact_records") {
|
|
6190
7619
|
return sha256Text(JSON.stringify([
|
|
6191
7620
|
"junction",
|
|
6192
7621
|
"extended-timeseries-backfill",
|
|
6193
|
-
|
|
7622
|
+
canonicalizeJunctionProviderSlug(payload.sourceProviderSlug),
|
|
6194
7623
|
resource,
|
|
7624
|
+
sourceLifecycleEpoch,
|
|
6195
7625
|
coverageVersion,
|
|
6196
7626
|
]));
|
|
6197
7627
|
}
|
|
6198
7628
|
return sha256Text(JSON.stringify([
|
|
6199
7629
|
"junction",
|
|
6200
7630
|
"extended-timeseries-backfill",
|
|
6201
|
-
|
|
7631
|
+
canonicalizeJunctionProviderSlug(payload.sourceProviderSlug),
|
|
6202
7632
|
resource,
|
|
7633
|
+
sourceLifecycleEpoch,
|
|
7634
|
+
coverageVersion,
|
|
6203
7635
|
historicalWindowStart,
|
|
6204
7636
|
windowEnd,
|
|
6205
7637
|
]));
|
|
6206
7638
|
}
|
|
7639
|
+
function buildExtendedTimeseriesBackfillFollowUp(job, input) {
|
|
7640
|
+
const payload = stripUndefined({
|
|
7641
|
+
...job.payload,
|
|
7642
|
+
...input.payload,
|
|
7643
|
+
...(input.payload?.historicalProviderRecordsSeen === false
|
|
7644
|
+
? { historicalProviderRecordsSeen: undefined }
|
|
7645
|
+
: {}),
|
|
7646
|
+
...(input.payload?.historicalRecordsSeen === false
|
|
7647
|
+
? { historicalRecordsSeen: undefined }
|
|
7648
|
+
: {}),
|
|
7649
|
+
sourceProviderSlug: canonicalizeJunctionProviderSlug(input.payload?.sourceProviderSlug ?? job.payload.sourceProviderSlug) ?? undefined,
|
|
7650
|
+
windowEnd: input.windowEnd,
|
|
7651
|
+
windowStart: input.windowStart,
|
|
7652
|
+
});
|
|
7653
|
+
return {
|
|
7654
|
+
kind: "resource",
|
|
7655
|
+
payload,
|
|
7656
|
+
availableAt: input.availableAt,
|
|
7657
|
+
priority: job.priority,
|
|
7658
|
+
dedupeKey: job.dedupeKey
|
|
7659
|
+
?? buildJunctionExtendedTimeseriesBackfillDedupeKey(payload)
|
|
7660
|
+
?? undefined,
|
|
7661
|
+
};
|
|
7662
|
+
}
|
|
6207
7663
|
function buildExtendedTimeseriesBackfillJob(input) {
|
|
7664
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
|
|
6208
7665
|
const payload = {
|
|
6209
7666
|
...(input.emptyBackfillAttempts && input.emptyBackfillAttempts > 0
|
|
6210
7667
|
? { emptyBackfillAttempts: input.emptyBackfillAttempts }
|
|
@@ -6233,8 +7690,11 @@ function buildExtendedTimeseriesBackfillJob(input) {
|
|
|
6233
7690
|
historicalWindowStart: input.historicalWindowStart,
|
|
6234
7691
|
resource: input.resource,
|
|
6235
7692
|
resourceCategory: "timeseries",
|
|
6236
|
-
...(input.
|
|
6237
|
-
? {
|
|
7693
|
+
...(input.sourceLifecycleEpoch === undefined
|
|
7694
|
+
? {}
|
|
7695
|
+
: { sourceLifecycleEpoch: input.sourceLifecycleEpoch }),
|
|
7696
|
+
...(sourceProviderSlug
|
|
7697
|
+
? { sourceProviderSlug }
|
|
6238
7698
|
: {}),
|
|
6239
7699
|
windowEnd: input.windowEnd,
|
|
6240
7700
|
windowStart: input.windowStart,
|
|
@@ -6252,13 +7712,14 @@ function buildExtendedTimeseriesBackfillJob(input) {
|
|
|
6252
7712
|
dedupeKey,
|
|
6253
7713
|
};
|
|
6254
7714
|
}
|
|
6255
|
-
function
|
|
7715
|
+
function readJunctionHistoricalBackfillVersion(payload) {
|
|
6256
7716
|
const version = payload.historicalBackfillVersion;
|
|
6257
7717
|
return typeof version === "number" && Number.isSafeInteger(version) && version >= 1
|
|
6258
7718
|
? version
|
|
6259
7719
|
: 1;
|
|
6260
7720
|
}
|
|
6261
7721
|
function buildJunctionWebhookJobs(input) {
|
|
7722
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
|
|
6262
7723
|
if (isJunctionProviderConnectionEvent(input.eventType)) {
|
|
6263
7724
|
const backfillWindowStart = subtractDays(input.window.windowEnd, input.summaryBackfillDays);
|
|
6264
7725
|
return [
|
|
@@ -6294,7 +7755,7 @@ function buildJunctionWebhookJobs(input) {
|
|
|
6294
7755
|
occurredAt: input.occurredAt,
|
|
6295
7756
|
resource: input.resource?.name ?? "",
|
|
6296
7757
|
resourceCategory: input.resource?.category ?? "",
|
|
6297
|
-
sourceProviderSlug:
|
|
7758
|
+
sourceProviderSlug: sourceProviderSlug ?? "",
|
|
6298
7759
|
...(webhookDataJson ? { webhookDataJson } : {}),
|
|
6299
7760
|
windowStart: input.window.windowStart,
|
|
6300
7761
|
windowEnd: input.window.windowEnd,
|
|
@@ -6304,7 +7765,7 @@ function buildJunctionWebhookJobs(input) {
|
|
|
6304
7765
|
? sha256Text(JSON.stringify([
|
|
6305
7766
|
"junction-webhook",
|
|
6306
7767
|
"resource-data",
|
|
6307
|
-
|
|
7768
|
+
sourceProviderSlug,
|
|
6308
7769
|
input.resource?.category,
|
|
6309
7770
|
input.resource?.name,
|
|
6310
7771
|
input.objectId ?? "",
|
|
@@ -6315,7 +7776,7 @@ function buildJunctionWebhookJobs(input) {
|
|
|
6315
7776
|
: sha256Text(JSON.stringify([
|
|
6316
7777
|
"junction-webhook",
|
|
6317
7778
|
"resource",
|
|
6318
|
-
|
|
7779
|
+
sourceProviderSlug,
|
|
6319
7780
|
input.resource?.category,
|
|
6320
7781
|
input.resource?.name,
|
|
6321
7782
|
input.window.windowStart,
|
|
@@ -6437,10 +7898,32 @@ function buildJunctionWebhookDataJobJsons(input) {
|
|
|
6437
7898
|
}
|
|
6438
7899
|
const withSource = stripUndefined({
|
|
6439
7900
|
...record,
|
|
6440
|
-
sourceProviderSlug:
|
|
7901
|
+
sourceProviderSlug: input.sourceProviderSlug ?? undefined,
|
|
6441
7902
|
});
|
|
6442
7903
|
return [serializeJunctionWebhookDataJobRecord(withSource)];
|
|
6443
7904
|
}
|
|
7905
|
+
function resolveJunctionWebhookDataSourceProviderSlug(input) {
|
|
7906
|
+
if (!input.data
|
|
7907
|
+
|| !isJunctionDataEvent(input.eventType)
|
|
7908
|
+
|| input.historicalPullCompleted) {
|
|
7909
|
+
return null;
|
|
7910
|
+
}
|
|
7911
|
+
const classification = classifyDeviceSyncJunctionInlineSourceProviderSlug(input.data);
|
|
7912
|
+
if (classification.status === "ambiguous") {
|
|
7913
|
+
throw junctionWebhookSourceNotReadyError();
|
|
7914
|
+
}
|
|
7915
|
+
return classification.status === "resolved"
|
|
7916
|
+
? classification.sourceProviderSlug
|
|
7917
|
+
: input.envelopeSourceProviderSlug;
|
|
7918
|
+
}
|
|
7919
|
+
function junctionWebhookSourceNotReadyError() {
|
|
7920
|
+
return deviceSyncError({
|
|
7921
|
+
code: "WEBHOOK_SOURCE_NOT_READY",
|
|
7922
|
+
message: "Junction webhook data source provenance was ambiguous. Retry later.",
|
|
7923
|
+
retryable: true,
|
|
7924
|
+
httpStatus: 503,
|
|
7925
|
+
});
|
|
7926
|
+
}
|
|
6444
7927
|
function serializeJunctionWebhookDataJobRecord(record) {
|
|
6445
7928
|
return JSON.stringify(record);
|
|
6446
7929
|
}
|
|
@@ -6619,7 +8102,7 @@ function readJunctionWebhookResourceFromEventType(eventType) {
|
|
|
6619
8102
|
return null;
|
|
6620
8103
|
}
|
|
6621
8104
|
function extractJunctionWebhookSourceProviderSlug(data) {
|
|
6622
|
-
return (
|
|
8105
|
+
return canonicalizeJunctionProviderSlug(resolveJunctionOrigin(data ?? undefined).sourceProviderSlug
|
|
6623
8106
|
?? extractJunctionWebhookSourceProviderSlugFallback(data));
|
|
6624
8107
|
}
|
|
6625
8108
|
function extractJunctionWebhookSourceProviderSlugFallback(data) {
|
|
@@ -6675,7 +8158,10 @@ function extractJunctionWebhookRootTimestamp(data) {
|
|
|
6675
8158
|
}
|
|
6676
8159
|
return null;
|
|
6677
8160
|
}
|
|
6678
|
-
function extractJunctionWebhookOccurredAt(data) {
|
|
8161
|
+
function extractJunctionWebhookOccurredAt(data, resource) {
|
|
8162
|
+
if (isJunctionBodyTimeseriesResource(resource?.name)) {
|
|
8163
|
+
return readJunctionWebhookDataTimestampRange(data, resource?.name)?.firstTimestamp ?? null;
|
|
8164
|
+
}
|
|
6679
8165
|
const rootTimestamp = extractJunctionWebhookRootTimestamp(data);
|
|
6680
8166
|
if (rootTimestamp) {
|
|
6681
8167
|
return rootTimestamp;
|
|
@@ -6684,16 +8170,21 @@ function extractJunctionWebhookOccurredAt(data) {
|
|
|
6684
8170
|
if (nestedTimestamp) {
|
|
6685
8171
|
return nestedTimestamp;
|
|
6686
8172
|
}
|
|
6687
|
-
return readJunctionWebhookDataTimestampRange(data)?.firstTimestamp ?? null;
|
|
8173
|
+
return readJunctionWebhookDataTimestampRange(data, resource?.name)?.firstTimestamp ?? null;
|
|
6688
8174
|
}
|
|
6689
8175
|
function buildJunctionWebhookWindow(data, occurredAt, now, resource) {
|
|
8176
|
+
const bodyTimeseriesResource = isJunctionBodyTimeseriesResource(resource?.name);
|
|
6690
8177
|
const explicitStart = toJunctionWebhookWindowBoundaryTimestampIfValid(data?.window_start, "start")
|
|
6691
8178
|
?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.start_date, "start")
|
|
6692
|
-
??
|
|
8179
|
+
?? (bodyTimeseriesResource
|
|
8180
|
+
? null
|
|
8181
|
+
: toJunctionWebhookWindowBoundaryTimestampIfValid(data?.start, "start"))
|
|
6693
8182
|
?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.from, "start");
|
|
6694
8183
|
const explicitEnd = toJunctionWebhookWindowBoundaryTimestampIfValid(data?.window_end, "end")
|
|
6695
8184
|
?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.end_date, "end")
|
|
6696
|
-
??
|
|
8185
|
+
?? (bodyTimeseriesResource
|
|
8186
|
+
? null
|
|
8187
|
+
: toJunctionWebhookWindowBoundaryTimestampIfValid(data?.end, "end"))
|
|
6697
8188
|
?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.to, "end");
|
|
6698
8189
|
if (explicitStart && explicitEnd) {
|
|
6699
8190
|
return {
|
|
@@ -6701,7 +8192,7 @@ function buildJunctionWebhookWindow(data, occurredAt, now, resource) {
|
|
|
6701
8192
|
windowEnd: minIsoTimestamp(explicitEnd, now),
|
|
6702
8193
|
};
|
|
6703
8194
|
}
|
|
6704
|
-
const dataTimestampRange = readJunctionWebhookDataTimestampRange(data);
|
|
8195
|
+
const dataTimestampRange = readJunctionWebhookDataTimestampRange(data, resource?.name);
|
|
6705
8196
|
if (dataTimestampRange) {
|
|
6706
8197
|
const windowStart = floorUtcDayTimestamp(dataTimestampRange.firstTimestamp);
|
|
6707
8198
|
const windowEnd = minIsoTimestamp(addMilliseconds(floorUtcDayTimestamp(dataTimestampRange.lastTimestamp), TIMESERIES_CHUNK_MS), now);
|
|
@@ -6743,11 +8234,13 @@ function toJunctionWebhookWindowBoundaryTimestampIfValid(value, boundary) {
|
|
|
6743
8234
|
}
|
|
6744
8235
|
return toIsoTimestampIfValid(normalized);
|
|
6745
8236
|
}
|
|
6746
|
-
function readJunctionWebhookDataTimestampRange(data) {
|
|
8237
|
+
function readJunctionWebhookDataTimestampRange(data, resource) {
|
|
6747
8238
|
const record = data ? readPlainObject(data) : null;
|
|
6748
8239
|
const timestamps = record
|
|
6749
|
-
? expandJunctionWebhookTimeseriesDataRecords(record)
|
|
6750
|
-
|
|
8240
|
+
? expandJunctionWebhookTimeseriesDataRecords(record)
|
|
8241
|
+
.slice(isJunctionBodyTimeseriesResource(resource) ? 0 : 1)
|
|
8242
|
+
.flatMap((entry) => {
|
|
8243
|
+
const timestamp = resolveJunctionTimeseriesRecordTimestampForResource(resource ?? "", entry);
|
|
6751
8244
|
return timestamp ? [timestamp] : [];
|
|
6752
8245
|
})
|
|
6753
8246
|
: [];
|
|
@@ -7064,7 +8557,9 @@ async function projectJunctionSources(context, providers, options = {}) {
|
|
|
7064
8557
|
&& historicalState.status !== "complete"));
|
|
7065
8558
|
const preserveHistoricalReconnectProviderSlugs = options.preserveHistoricalReconnectProviderSlugs === undefined
|
|
7066
8559
|
? null
|
|
7067
|
-
: new Set(options.preserveHistoricalReconnectProviderSlugs
|
|
8560
|
+
: new Set(options.preserveHistoricalReconnectProviderSlugs
|
|
8561
|
+
.map(canonicalizeJunctionProviderSlug)
|
|
8562
|
+
.filter((providerSlug) => providerSlug !== null));
|
|
7068
8563
|
const projectedSources = projectJunctionSourcesByProviderSlug(context.account.id, providers);
|
|
7069
8564
|
if (projectedSources.length > JUNCTION_MAX_USER_PROVIDERS) {
|
|
7070
8565
|
throw deviceSyncError({
|
|
@@ -7099,6 +8594,10 @@ async function projectJunctionSources(context, providers, options = {}) {
|
|
|
7099
8594
|
&& existing.lastErrorCode === DEVICE_SYNC_HISTORICAL_DATA_RECONNECT_REQUIRED_ERROR_CODE
|
|
7100
8595
|
: requiresHistoricalResetDeviceSyncSource(existing));
|
|
7101
8596
|
const historicalReconnectError = keepHistoricalReconnect ? existing : null;
|
|
8597
|
+
const historicalBackfillCompletedAt = source.sourceProviderSlug === options.historicalBackfillCompletedProviderSlug
|
|
8598
|
+
? context.now
|
|
8599
|
+
: existing?.resourceAvailabilitySummary?.[DEVICE_SYNC_SOURCE_HISTORICAL_BACKFILL_COMPLETED_AT_KEY];
|
|
8600
|
+
const existingSourceMetadata = Object.fromEntries(Object.entries(existing?.resourceAvailabilitySummary ?? {}).filter(([key]) => isDeviceSyncSourceResourceAvailabilityMetadataKey(key)));
|
|
7102
8601
|
const persistedSource = await context.upsertConnectionSource({
|
|
7103
8602
|
sourceInstanceKey: accountSourceIdentity?.sourceInstanceKey ?? source.sourceInstanceKey,
|
|
7104
8603
|
sourceProviderSlug: accountSourceIdentity?.sourceProviderSlug ?? source.sourceProviderSlug,
|
|
@@ -7107,7 +8606,15 @@ async function projectJunctionSources(context, providers, options = {}) {
|
|
|
7107
8606
|
status: keepHistoricalReconnect
|
|
7108
8607
|
? "error"
|
|
7109
8608
|
: source.status,
|
|
7110
|
-
resourceAvailabilitySummary:
|
|
8609
|
+
resourceAvailabilitySummary: {
|
|
8610
|
+
...source.resourceAvailabilitySummary,
|
|
8611
|
+
...existingSourceMetadata,
|
|
8612
|
+
...(typeof historicalBackfillCompletedAt === "string"
|
|
8613
|
+
? {
|
|
8614
|
+
[DEVICE_SYNC_SOURCE_HISTORICAL_BACKFILL_COMPLETED_AT_KEY]: historicalBackfillCompletedAt,
|
|
8615
|
+
}
|
|
8616
|
+
: {}),
|
|
8617
|
+
},
|
|
7111
8618
|
// Only assert error fields when this projection saw an errored entry;
|
|
7112
8619
|
// omitting the keys lets the store preserve existing detail while the
|
|
7113
8620
|
// status stays "error" and auto-clear it once the status recovers.
|
|
@@ -7130,11 +8637,138 @@ async function projectJunctionSources(context, providers, options = {}) {
|
|
|
7130
8637
|
}
|
|
7131
8638
|
}
|
|
7132
8639
|
}
|
|
8640
|
+
function buildJunctionGoogleHealthCanonicalCoverageFence(sources) {
|
|
8641
|
+
const legacySources = sources.filter((source) => normalizeProviderSlug(source.sourceProviderSlug) === JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG);
|
|
8642
|
+
if (legacySources.length === 0
|
|
8643
|
+
|| legacySources.some((source) => !isGoogleHealthFitbitMigrationLegacyTerminal(source))) {
|
|
8644
|
+
return undefined;
|
|
8645
|
+
}
|
|
8646
|
+
const coverageBoundaryByResource = {};
|
|
8647
|
+
for (const resource of [
|
|
8648
|
+
...JUNCTION_ALLOWED_SUMMARY_RESOURCES,
|
|
8649
|
+
...JUNCTION_ALLOWED_TIMESERIES_RESOURCES,
|
|
8650
|
+
]) {
|
|
8651
|
+
const coverageKey = buildDeviceSyncSourceCanonicalCoverageBoundaryKey(resource);
|
|
8652
|
+
if (!coverageKey) {
|
|
8653
|
+
continue;
|
|
8654
|
+
}
|
|
8655
|
+
const summariesWithCoverage = legacySources.flatMap((source) => {
|
|
8656
|
+
const summary = source.resourceAvailabilitySummary;
|
|
8657
|
+
return summary && coverageKey in summary ? [summary] : [];
|
|
8658
|
+
});
|
|
8659
|
+
if (summariesWithCoverage.length === 0) {
|
|
8660
|
+
continue;
|
|
8661
|
+
}
|
|
8662
|
+
const coverageValues = summariesWithCoverage.map((summary) => readDeviceSyncSourceCanonicalCoverageBoundary(summary, resource));
|
|
8663
|
+
if (coverageValues.some((coverage) => coverage === null)) {
|
|
8664
|
+
coverageBoundaryByResource[resource] = null;
|
|
8665
|
+
continue;
|
|
8666
|
+
}
|
|
8667
|
+
const validCoverage = coverageValues.filter((coverage) => coverage !== null);
|
|
8668
|
+
coverageBoundaryByResource[resource] = validCoverage.reduce((latest, coverage) => maxJunctionCanonicalCoverageBoundary(resource, latest, coverage));
|
|
8669
|
+
}
|
|
8670
|
+
return Object.keys(coverageBoundaryByResource).length > 0
|
|
8671
|
+
? {
|
|
8672
|
+
coverageBoundaryByResource,
|
|
8673
|
+
sourceProviderSlug: JUNCTION_GOOGLE_HEALTH_PROVIDER_SLUG,
|
|
8674
|
+
}
|
|
8675
|
+
: undefined;
|
|
8676
|
+
}
|
|
8677
|
+
function readJunctionSourceLifecycleEpoch(source) {
|
|
8678
|
+
const lifecycleEpoch = source.lifecycleEpoch;
|
|
8679
|
+
return typeof lifecycleEpoch === "number" && Number.isSafeInteger(lifecycleEpoch) && lifecycleEpoch >= 1
|
|
8680
|
+
? lifecycleEpoch
|
|
8681
|
+
: 1;
|
|
8682
|
+
}
|
|
8683
|
+
function captureJunctionSourceLifecycleFence(sources) {
|
|
8684
|
+
return new Map([...buildJunctionCurrentLifecycleSourceMap(sources)]
|
|
8685
|
+
.map(([sourceProviderSlug, sources]) => [
|
|
8686
|
+
sourceProviderSlug,
|
|
8687
|
+
readJunctionSourceLifecycleEpoch(sources[0]),
|
|
8688
|
+
]));
|
|
8689
|
+
}
|
|
8690
|
+
function isJunctionSourceLifecycleFenceCurrent(expected, currentSources) {
|
|
8691
|
+
const current = captureJunctionSourceLifecycleFence(currentSources);
|
|
8692
|
+
return current.size === expected.size
|
|
8693
|
+
&& [...expected].every(([sourceProviderSlug, lifecycleEpoch]) => current.get(sourceProviderSlug) === lifecycleEpoch);
|
|
8694
|
+
}
|
|
8695
|
+
function junctionTimeseriesSourceLifecycleSuperseded() {
|
|
8696
|
+
return deviceSyncError({
|
|
8697
|
+
code: "JUNCTION_TIMESERIES_SOURCE_LIFECYCLE_SUPERSEDED",
|
|
8698
|
+
message: "Junction source authorization changed during collection. Retry shortly.",
|
|
8699
|
+
retryable: true,
|
|
8700
|
+
httpStatus: 503,
|
|
8701
|
+
});
|
|
8702
|
+
}
|
|
8703
|
+
function buildJunctionCurrentLifecycleSourceMap(sources) {
|
|
8704
|
+
const currentByProviderSlug = new Map();
|
|
8705
|
+
for (const source of sources) {
|
|
8706
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(source.sourceProviderSlug);
|
|
8707
|
+
if (!sourceProviderSlug) {
|
|
8708
|
+
continue;
|
|
8709
|
+
}
|
|
8710
|
+
const lifecycleEpoch = readJunctionSourceLifecycleEpoch(source);
|
|
8711
|
+
const current = currentByProviderSlug.get(sourceProviderSlug);
|
|
8712
|
+
const currentLifecycleEpoch = current?.[0]
|
|
8713
|
+
? readJunctionSourceLifecycleEpoch(current[0])
|
|
8714
|
+
: 0;
|
|
8715
|
+
if (!current || lifecycleEpoch > currentLifecycleEpoch) {
|
|
8716
|
+
currentByProviderSlug.set(sourceProviderSlug, [source]);
|
|
8717
|
+
}
|
|
8718
|
+
else if (lifecycleEpoch === currentLifecycleEpoch) {
|
|
8719
|
+
current.push(source);
|
|
8720
|
+
}
|
|
8721
|
+
}
|
|
8722
|
+
return currentByProviderSlug;
|
|
8723
|
+
}
|
|
8724
|
+
function selectCurrentJunctionLifecycleSources(sources, sourceProviderSlug) {
|
|
8725
|
+
const canonicalSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
|
|
8726
|
+
if (!canonicalSourceProviderSlug) {
|
|
8727
|
+
return [];
|
|
8728
|
+
}
|
|
8729
|
+
return buildJunctionCurrentLifecycleSourceMap(sources).get(canonicalSourceProviderSlug) ?? [];
|
|
8730
|
+
}
|
|
8731
|
+
function isJunctionLifecycleSourceGroupAdmitted(sources, sourceStatusRequirement) {
|
|
8732
|
+
if (sources.length === 0 || sources.some(isDeviceSyncSourceDisconnectFenced)) {
|
|
8733
|
+
return false;
|
|
8734
|
+
}
|
|
8735
|
+
const status = sources
|
|
8736
|
+
.map((source) => source.status)
|
|
8737
|
+
.reduce(mergeJunctionSourceStatus);
|
|
8738
|
+
return sourceStatusRequirement === "connected"
|
|
8739
|
+
? status === "connected"
|
|
8740
|
+
: status !== "disconnected";
|
|
8741
|
+
}
|
|
7133
8742
|
function isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnlistedSources = true, sourceStatusRequirement = "not_disconnected") {
|
|
7134
|
-
const normalizedSourceProviderSlug =
|
|
8743
|
+
const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
|
|
7135
8744
|
if (!normalizedSourceProviderSlug) {
|
|
7136
8745
|
return true;
|
|
7137
8746
|
}
|
|
8747
|
+
// Junction does not deduplicate equivalent Fitbit and Google Health records.
|
|
8748
|
+
// During migration, keep legacy Fitbit as the sole canonical writer until its
|
|
8749
|
+
// access is explicitly terminal through local cutover or provider-confirmed absence.
|
|
8750
|
+
// A disconnect-in-progress fence blocks both sides so a failed revoke cannot
|
|
8751
|
+
// leave a brief dual-writer interval before the legacy row is restored.
|
|
8752
|
+
if (normalizedSourceProviderSlug === JUNCTION_GOOGLE_HEALTH_PROVIDER_SLUG) {
|
|
8753
|
+
const legacySources = sources.filter((source) => normalizeProviderSlug(source.sourceProviderSlug)
|
|
8754
|
+
=== JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG);
|
|
8755
|
+
if (legacySources.length > 0) {
|
|
8756
|
+
const canonicalFence = buildJunctionGoogleHealthCanonicalCoverageFence(sources);
|
|
8757
|
+
const successor = sources.find((source) => normalizeProviderSlug(source.sourceProviderSlug)
|
|
8758
|
+
=== JUNCTION_GOOGLE_HEALTH_PROVIDER_SLUG);
|
|
8759
|
+
if (legacySources.some((source) => !isGoogleHealthFitbitMigrationLegacyTerminal(source))
|
|
8760
|
+
|| !successor
|
|
8761
|
+
|| legacySources.some((source) => !isGoogleHealthFitbitMigrationLegacyCoverageReady({
|
|
8762
|
+
legacyAccessTerminal: true,
|
|
8763
|
+
legacySummary: source.resourceAvailabilitySummary,
|
|
8764
|
+
successorSummary: successor.resourceAvailabilitySummary,
|
|
8765
|
+
}))
|
|
8766
|
+
|| (canonicalFence
|
|
8767
|
+
&& Object.values(canonicalFence.coverageBoundaryByResource).some((boundary) => boundary === null))) {
|
|
8768
|
+
return false;
|
|
8769
|
+
}
|
|
8770
|
+
}
|
|
8771
|
+
}
|
|
7138
8772
|
const accountSourceIdentity = resolveJunctionAccountSourceIdentity(sources, normalizedSourceProviderSlug, allowUnlistedSources);
|
|
7139
8773
|
if (!accountSourceIdentity) {
|
|
7140
8774
|
return false;
|
|
@@ -7150,7 +8784,7 @@ function isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnl
|
|
|
7150
8784
|
&& !isDeviceSyncSourceDisconnectFenced(lifecycleSource);
|
|
7151
8785
|
}
|
|
7152
8786
|
function isJunctionSourceProjectionFenced(sources, sourceProviderSlug) {
|
|
7153
|
-
const normalizedSourceProviderSlug =
|
|
8787
|
+
const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
|
|
7154
8788
|
if (!normalizedSourceProviderSlug) {
|
|
7155
8789
|
return false;
|
|
7156
8790
|
}
|
|
@@ -7163,28 +8797,36 @@ function isJunctionSourceProjectionFenced(sources, sourceProviderSlug) {
|
|
|
7163
8797
|
&& isDeviceSyncSourceDisconnectFenced(sourceState.lifecycleSource);
|
|
7164
8798
|
}
|
|
7165
8799
|
function hasJunctionSourceListing(sources, sourceProviderSlug) {
|
|
7166
|
-
const normalizedSourceProviderSlug =
|
|
8800
|
+
const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
|
|
7167
8801
|
if (!normalizedSourceProviderSlug) {
|
|
7168
8802
|
return true;
|
|
7169
8803
|
}
|
|
7170
|
-
return sources.some((source) =>
|
|
8804
|
+
return sources.some((source) => areJunctionProviderSlugsDataEquivalent(source.sourceProviderSlug, normalizedSourceProviderSlug));
|
|
7171
8805
|
}
|
|
7172
|
-
async function resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug) {
|
|
8806
|
+
async function resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug, expectedLifecycleEpoch) {
|
|
7173
8807
|
const sources = context.listConnectionSources
|
|
7174
|
-
? await context.listConnectionSources()
|
|
8808
|
+
? await context.listConnectionSources({ sourceProviderSlug })
|
|
7175
8809
|
: context.account.sources ?? [];
|
|
7176
|
-
return resolveJunctionCurrentSourceAdmissionFromSources(sources, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only");
|
|
8810
|
+
return resolveJunctionCurrentSourceAdmissionFromSources(sources, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only", expectedLifecycleEpoch);
|
|
7177
8811
|
}
|
|
7178
|
-
function resolveJunctionCurrentSourceAdmissionFromSources(sources, sourceProviderSlug, allowUnlistedSources) {
|
|
8812
|
+
function resolveJunctionCurrentSourceAdmissionFromSources(sources, sourceProviderSlug, allowUnlistedSources, expectedLifecycleEpoch) {
|
|
7179
8813
|
if (isJunctionSourceProjectionFenced(sources, sourceProviderSlug)) {
|
|
7180
8814
|
return "fenced";
|
|
7181
8815
|
}
|
|
8816
|
+
const sourceState = resolveJunctionAccountSourceState(sources, sourceProviderSlug);
|
|
8817
|
+
if (expectedLifecycleEpoch !== undefined
|
|
8818
|
+
&& (sourceState === null
|
|
8819
|
+
? !allowUnlistedSources
|
|
8820
|
+
: readJunctionSourceLifecycleEpoch(sourceState.lifecycleSource)
|
|
8821
|
+
!== expectedLifecycleEpoch)) {
|
|
8822
|
+
return "fenced";
|
|
8823
|
+
}
|
|
7182
8824
|
return isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnlistedSources, "connected")
|
|
7183
8825
|
? "admitted"
|
|
7184
8826
|
: "pending";
|
|
7185
8827
|
}
|
|
7186
8828
|
async function isJunctionCompanionSourceCurrentlyAdmitted(context, sourceProviderSlug) {
|
|
7187
|
-
const normalizedSourceProviderSlug =
|
|
8829
|
+
const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
|
|
7188
8830
|
if (!context.listConnectionSources) {
|
|
7189
8831
|
throw junctionCompanionSourceStateUnavailableError();
|
|
7190
8832
|
}
|
|
@@ -7203,7 +8845,7 @@ async function isJunctionCompanionSourceCurrentlyAdmitted(context, sourceProvide
|
|
|
7203
8845
|
}
|
|
7204
8846
|
throw junctionCompanionSourceStateUnavailableError(error);
|
|
7205
8847
|
}
|
|
7206
|
-
if (
|
|
8848
|
+
if (isJunctionSourceAdmittedForImport(sources, normalizedSourceProviderSlug, true, "connected")) {
|
|
7207
8849
|
return true;
|
|
7208
8850
|
}
|
|
7209
8851
|
if (sources.some((source) => source.lastErrorCode === DEVICE_SYNC_SOURCE_USER_DISCONNECTED_ERROR_CODE)) {
|
|
@@ -7249,7 +8891,8 @@ function projectJunctionSourcesByProviderSlug(connectionId, providers) {
|
|
|
7249
8891
|
sourceProviderSlug: provider.origin.sourceProviderSlug,
|
|
7250
8892
|
sourceInstanceId: provider.origin.sourceInstanceId,
|
|
7251
8893
|
});
|
|
7252
|
-
const sourceProviderSlug =
|
|
8894
|
+
const sourceProviderSlug = canonicalizeJunctionProviderSlug(origin.sourceProviderSlug)
|
|
8895
|
+
?? canonicalizeJunctionProviderSlug(provider.slug);
|
|
7253
8896
|
if (!sourceProviderSlug) {
|
|
7254
8897
|
continue;
|
|
7255
8898
|
}
|
|
@@ -7264,7 +8907,10 @@ function projectJunctionSourcesByProviderSlug(connectionId, providers) {
|
|
|
7264
8907
|
const status = mapJunctionSourceStatus(provider.status);
|
|
7265
8908
|
const lastErrorCode = status === "error"
|
|
7266
8909
|
? truncateJunctionSourceErrorText(provider.errorDetails?.errorType, JUNCTION_SOURCE_ERROR_CODE_MAX_LENGTH)
|
|
7267
|
-
:
|
|
8910
|
+
: status === "disconnected"
|
|
8911
|
+
&& sourceProviderSlug === JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG
|
|
8912
|
+
? DEVICE_SYNC_SOURCE_PROVIDER_DISCONNECTED_ERROR_CODE
|
|
8913
|
+
: null;
|
|
7268
8914
|
const lastErrorMessage = status === "error"
|
|
7269
8915
|
? truncateJunctionSourceErrorText(provider.errorDetails?.errorMessage, JUNCTION_SOURCE_ERROR_MESSAGE_MAX_LENGTH)
|
|
7270
8916
|
: null;
|
|
@@ -7272,11 +8918,17 @@ function projectJunctionSourcesByProviderSlug(connectionId, providers) {
|
|
|
7272
8918
|
if (existing) {
|
|
7273
8919
|
mergeJunctionResourceAvailabilitySummary(existing.resourceAvailabilitySummary, resourceAvailabilitySummary);
|
|
7274
8920
|
existing.status = mergeJunctionSourceStatus(existing.status, status);
|
|
7275
|
-
if (existing.status
|
|
8921
|
+
if (existing.status === "disconnected"
|
|
8922
|
+
&& sourceProviderSlug === JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG) {
|
|
8923
|
+
existing.lastErrorCode = DEVICE_SYNC_SOURCE_PROVIDER_DISCONNECTED_ERROR_CODE;
|
|
8924
|
+
existing.lastErrorMessage = null;
|
|
8925
|
+
}
|
|
8926
|
+
else if (existing.status !== "error") {
|
|
7276
8927
|
existing.lastErrorCode = null;
|
|
7277
8928
|
existing.lastErrorMessage = null;
|
|
7278
8929
|
}
|
|
7279
|
-
else if (existing.lastErrorCode ===
|
|
8930
|
+
else if (existing.lastErrorCode === DEVICE_SYNC_SOURCE_PROVIDER_DISCONNECTED_ERROR_CODE
|
|
8931
|
+
|| (existing.lastErrorCode === null && existing.lastErrorMessage === null)) {
|
|
7280
8932
|
existing.lastErrorCode = lastErrorCode;
|
|
7281
8933
|
existing.lastErrorMessage = lastErrorMessage;
|
|
7282
8934
|
}
|
|
@@ -7318,8 +8970,8 @@ function mergeJunctionResourceAvailabilityValue(existing, next) {
|
|
|
7318
8970
|
return existing;
|
|
7319
8971
|
}
|
|
7320
8972
|
function mergeJunctionSourceStatus(existing, next) {
|
|
7321
|
-
if (existing === "
|
|
7322
|
-
return "
|
|
8973
|
+
if (existing === "disconnected" || next === "disconnected") {
|
|
8974
|
+
return "disconnected";
|
|
7323
8975
|
}
|
|
7324
8976
|
if (existing === "error" || next === "error") {
|
|
7325
8977
|
return "error";
|
|
@@ -7327,7 +8979,7 @@ function mergeJunctionSourceStatus(existing, next) {
|
|
|
7327
8979
|
if (existing === "unavailable" || next === "unavailable") {
|
|
7328
8980
|
return "unavailable";
|
|
7329
8981
|
}
|
|
7330
|
-
return "
|
|
8982
|
+
return "connected";
|
|
7331
8983
|
}
|
|
7332
8984
|
// Derive import identity from the stable Junction user id, never from the
|
|
7333
8985
|
// local device-sync account row id: row ids are re-minted whenever the
|
|
@@ -7496,6 +9148,32 @@ function readProviderSnapshotCanonicalEventExternalRefResourceIds(value) {
|
|
|
7496
9148
|
? resourceIds
|
|
7497
9149
|
: null;
|
|
7498
9150
|
}
|
|
9151
|
+
function readProviderSnapshotJunctionCanonicalCoverage(value) {
|
|
9152
|
+
const entries = readPlainObject(value)?.junctionCanonicalCoverage;
|
|
9153
|
+
if (!Array.isArray(entries)) {
|
|
9154
|
+
return [];
|
|
9155
|
+
}
|
|
9156
|
+
return entries.flatMap((entry) => {
|
|
9157
|
+
const evidence = readPlainObject(entry);
|
|
9158
|
+
const resource = typeof evidence?.resource === "string" ? evidence.resource : "";
|
|
9159
|
+
const coverageBoundary = normalizeJunctionCanonicalCoverageBoundary(resource, evidence?.coverageBoundary);
|
|
9160
|
+
if (!evidence
|
|
9161
|
+
|| !coverageBoundary
|
|
9162
|
+
|| !resource
|
|
9163
|
+
|| typeof evidence.sourceProviderSlug !== "string") {
|
|
9164
|
+
return [];
|
|
9165
|
+
}
|
|
9166
|
+
const coverageFinalizedAt = toIsoTimestampIfValid(evidence.coverageFinalizedAt);
|
|
9167
|
+
return [{
|
|
9168
|
+
coverageBoundary,
|
|
9169
|
+
...(coverageFinalizedAt
|
|
9170
|
+
? { coverageFinalizedAt }
|
|
9171
|
+
: {}),
|
|
9172
|
+
resource,
|
|
9173
|
+
sourceProviderSlug: evidence.sourceProviderSlug,
|
|
9174
|
+
}];
|
|
9175
|
+
});
|
|
9176
|
+
}
|
|
7499
9177
|
function uniqueJunctionProviderRecordIdentities(identities) {
|
|
7500
9178
|
return [...new Set(identities.filter((identity) => identity !== null && JUNCTION_BLOOD_PRESSURE_PROVIDER_RECORD_IDENTITY_PATTERN.test(identity)))].sort();
|
|
7501
9179
|
}
|