Circle V2 API Docs
    Preparing search index...

    Per-episode EMR sync (episode-first port of v1 sync-patient): episode-scoped sync, treatment_episodes dual-write (both models maintained during the transition window), documents when reviews are pending, then the chart reviews — docs always precede reviews. Routes to the manual enqueue pipeline when manual is set, otherwise to the triggered pipeline. Returns the auditRunIds from whichever enqueue ran.

    Hierarchy (View Summary)

    • Jobbable<SyncEpisodeJobParams, SyncEpisodeJobResult>
      • SyncEpisodeJobbable
    Index

    Constructors

    Properties

    config: JobConfig = ...
    serviceName: "SyncEpisodeJobbable"

    Stable, human-readable name for the service. Must be set explicitly on each subclass because this.constructor.name gets mangled by minification in production builds, which breaks tracing/observability span names.

    Accessors

    • get logger(): BaseLogger

      Returns BaseLogger

    • get name(): string

      Returns string

    Methods

    • Parameters

      • params: {
            chartReviewJobs?: {
                entityId: EntityId;
                key: string;
                patientId: PatientId;
                templateIds: [AuditTemplateId];
                triggerId: ChartReviewTriggerId;
                ttlHours: number;
            }[];
            drainProxyPdfs?: boolean;
            entityId: EntityId;
            episodeId: TreatmentEpisodeId;
            manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
            readWholeEpisode?: boolean;
            syncDocuments: boolean;
        }
        • OptionalchartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[]
        • OptionaldrainProxyPdfs?: boolean

          Set only by the manual "Sync Episode" action (SyncEpisodeService, run inline so never keyed). No longer consulted for proxy integrations: their PDF fetch chain always hands the episode to the submit drain, which works through the whole episode whether the sync was manual or hourly.

        • entityId: EntityId
        • episodeId: TreatmentEpisodeId
        • Optionalmanual?: { createdBy: UserId; templateIds: AuditTemplateId[] }

          Present when the run is UI-initiated (Sync & Run). Routes to the manual enqueue pipeline.

        • OptionalreadWholeEpisode?: boolean

          Set by the manual "Sync Episode" action and the episode-linking backfill: the proxy reads every document of the episode instead of only those changed since the newest stored one. Keyed apart from the hourly sync of the same episode so neither dedupes the other away.

        • syncDocuments: boolean

      Returns string

    • Derive the queue retentionSeconds from the effective dedup TTL. The queue overloads retentionSeconds — it's BOTH the message lifetime AND the idempotency dedup window (min(retentionSeconds, 24h)). So:

      • take the intended dedup window (ttlHours·3600), capped at 24h (the window can't exceed it);
      • but never go below the message's worst-case time-to-complete (delaySeconds + every retry), else a short TTL would expire the message mid-retry — and the queue hard-rejects delaySeconds > retentionSeconds. ttlHours <= 0 means "no dedup" → undefined: the idempotency key is dropped (see performLater), so there's no dedup window to bound — this just leaves the queue's default message lifetime in place.

      Parameters

      • ttlHours: number
      • delaySeconds: number

      Returns number | undefined

    • Extract an entityId from params for job_executions tracking. Override in subclasses when applicable.

      Parameters

      • params: {
            chartReviewJobs?: {
                entityId: EntityId;
                key: string;
                patientId: PatientId;
                templateIds: [AuditTemplateId];
                triggerId: ChartReviewTriggerId;
                ttlHours: number;
            }[];
            drainProxyPdfs?: boolean;
            entityId: EntityId;
            episodeId: TreatmentEpisodeId;
            manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
            readWholeEpisode?: boolean;
            syncDocuments: boolean;
        }
        • OptionalchartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[]
        • OptionaldrainProxyPdfs?: boolean

          Set only by the manual "Sync Episode" action (SyncEpisodeService, run inline so never keyed). No longer consulted for proxy integrations: their PDF fetch chain always hands the episode to the submit drain, which works through the whole episode whether the sync was manual or hourly.

        • entityId: EntityId
        • episodeId: TreatmentEpisodeId
        • Optionalmanual?: { createdBy: UserId; templateIds: AuditTemplateId[] }

          Present when the run is UI-initiated (Sync & Run). Routes to the manual enqueue pipeline.

        • OptionalreadWholeEpisode?: boolean

          Set by the manual "Sync Episode" action and the episode-linking backfill: the proxy reads every document of the episode instead of only those changed since the newest stored one. Keyed apart from the hourly sync of the same episode so neither dedupes the other away.

        • syncDocuments: boolean

      Returns EntityId

    • NEW key format (deliberately ≠ v1's patient_{id}sync…). The :scheduled token mirrors v1: a pending scheduled review must force a fresh doc sync even when a non-scheduled sync ran inside the TTL window. Manual runs return null so they aren't deduped against an in-flight triggered sync for the same episode (the manual templateIds would be dropped).

      Parameters

      • params: {
            chartReviewJobs?: {
                entityId: EntityId;
                key: string;
                patientId: PatientId;
                templateIds: [AuditTemplateId];
                triggerId: ChartReviewTriggerId;
                ttlHours: number;
            }[];
            drainProxyPdfs?: boolean;
            entityId: EntityId;
            episodeId: TreatmentEpisodeId;
            manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
            readWholeEpisode?: boolean;
            syncDocuments: boolean;
        }
        • OptionalchartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[]
        • OptionaldrainProxyPdfs?: boolean

          Set only by the manual "Sync Episode" action (SyncEpisodeService, run inline so never keyed). No longer consulted for proxy integrations: their PDF fetch chain always hands the episode to the submit drain, which works through the whole episode whether the sync was manual or hourly.

        • entityId: EntityId
        • episodeId: TreatmentEpisodeId
        • Optionalmanual?: { createdBy: UserId; templateIds: AuditTemplateId[] }

          Present when the run is UI-initiated (Sync & Run). Routes to the manual enqueue pipeline.

        • OptionalreadWholeEpisode?: boolean

          Set by the manual "Sync Episode" action and the episode-linking backfill: the proxy reads every document of the episode instead of only those changed since the newest stored one. Keyed apart from the hourly sync of the same episode so neither dedupes the other away.

        • syncDocuments: boolean

      Returns string | null

    • Parameters

      • params: {
            chartReviewJobs?: {
                entityId: EntityId;
                key: string;
                patientId: PatientId;
                templateIds: [AuditTemplateId];
                triggerId: ChartReviewTriggerId;
                ttlHours: number;
            }[];
            drainProxyPdfs?: boolean;
            entityId: EntityId;
            episodeId: TreatmentEpisodeId;
            manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
            readWholeEpisode?: boolean;
            syncDocuments: boolean;
        }
        • OptionalchartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[]
        • OptionaldrainProxyPdfs?: boolean

          Set only by the manual "Sync Episode" action (SyncEpisodeService, run inline so never keyed). No longer consulted for proxy integrations: their PDF fetch chain always hands the episode to the submit drain, which works through the whole episode whether the sync was manual or hourly.

        • entityId: EntityId
        • episodeId: TreatmentEpisodeId
        • Optionalmanual?: { createdBy: UserId; templateIds: AuditTemplateId[] }

          Present when the run is UI-initiated (Sync & Run). Routes to the manual enqueue pipeline.

        • OptionalreadWholeEpisode?: boolean

          Set by the manual "Sync Episode" action and the episode-linking backfill: the proxy reads every document of the episode instead of only those changed since the newest stored one. Keyed apart from the hourly sync of the same episode so neither dedupes the other away.

        • syncDocuments: boolean

      Returns Promise<Safe<SyncEpisodeJobResult>>

    • Enqueue this job for async processing via the configured queue driver (VQS_DRIVER). Checks for deduplication via job_executions before sending.

      Parameters

      • params: {
            chartReviewJobs?: {
                entityId: EntityId;
                key: string;
                patientId: PatientId;
                templateIds: [AuditTemplateId];
                triggerId: ChartReviewTriggerId;
                ttlHours: number;
            }[];
            drainProxyPdfs?: boolean;
            entityId: EntityId;
            episodeId: TreatmentEpisodeId;
            manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
            readWholeEpisode?: boolean;
            syncDocuments: boolean;
        }
        • OptionalchartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[]
        • OptionaldrainProxyPdfs?: boolean

          Set only by the manual "Sync Episode" action (SyncEpisodeService, run inline so never keyed). No longer consulted for proxy integrations: their PDF fetch chain always hands the episode to the submit drain, which works through the whole episode whether the sync was manual or hourly.

        • entityId: EntityId
        • episodeId: TreatmentEpisodeId
        • Optionalmanual?: { createdBy: UserId; templateIds: AuditTemplateId[] }

          Present when the run is UI-initiated (Sync & Run). Routes to the manual enqueue pipeline.

        • OptionalreadWholeEpisode?: boolean

          Set by the manual "Sync Episode" action and the episode-linking backfill: the proxy reads every document of the episode instead of only those changed since the newest stored one. Keyed apart from the hourly sync of the same episode so neither dedupes the other away.

        • syncDocuments: boolean
      • Optionaloptions: PerformLaterOptions

      Returns Promise<EnqueueResult>

    • Parameters

      • params: {
            chartReviewJobs?: {
                entityId: EntityId;
                key: string;
                patientId: PatientId;
                templateIds: [AuditTemplateId];
                triggerId: ChartReviewTriggerId;
                ttlHours: number;
            }[];
            drainProxyPdfs?: boolean;
            entityId: EntityId;
            episodeId: TreatmentEpisodeId;
            manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
            readWholeEpisode?: boolean;
            syncDocuments: boolean;
        }
        • OptionalchartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[]
        • OptionaldrainProxyPdfs?: boolean

          Set only by the manual "Sync Episode" action (SyncEpisodeService, run inline so never keyed). No longer consulted for proxy integrations: their PDF fetch chain always hands the episode to the submit drain, which works through the whole episode whether the sync was manual or hourly.

        • entityId: EntityId
        • episodeId: TreatmentEpisodeId
        • Optionalmanual?: { createdBy: UserId; templateIds: AuditTemplateId[] }

          Present when the run is UI-initiated (Sync & Run). Routes to the manual enqueue pipeline.

        • OptionalreadWholeEpisode?: boolean

          Set by the manual "Sync Episode" action and the episode-linking backfill: the proxy reads every document of the episode instead of only those changed since the newest stored one. Keyed apart from the hourly sync of the same episode so neither dedupes the other away.

        • syncDocuments: boolean

      Returns Promise<SyncEpisodeJobResult>

    • Run the job with status tracking. Used by the queue handler. Errors propagate (not caught by safe()) so the RetryHandler can decide.

      parentSpanContext (the enqueuer's span, recovered from the queue envelope) is attached as a span LINK and the run span is started as a new trace root, so each job runs in its own trace rather than collapsing a fan-out into one unbounded trace.

      Parameters

      • params: {
            chartReviewJobs?: {
                entityId: EntityId;
                key: string;
                patientId: PatientId;
                templateIds: [AuditTemplateId];
                triggerId: ChartReviewTriggerId;
                ttlHours: number;
            }[];
            drainProxyPdfs?: boolean;
            entityId: EntityId;
            episodeId: TreatmentEpisodeId;
            manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
            readWholeEpisode?: boolean;
            syncDocuments: boolean;
        }
        • OptionalchartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[]
        • OptionaldrainProxyPdfs?: boolean

          Set only by the manual "Sync Episode" action (SyncEpisodeService, run inline so never keyed). No longer consulted for proxy integrations: their PDF fetch chain always hands the episode to the submit drain, which works through the whole episode whether the sync was manual or hourly.

        • entityId: EntityId
        • episodeId: TreatmentEpisodeId
        • Optionalmanual?: { createdBy: UserId; templateIds: AuditTemplateId[] }

          Present when the run is UI-initiated (Sync & Run). Routes to the manual enqueue pipeline.

        • OptionalreadWholeEpisode?: boolean

          Set by the manual "Sync Episode" action and the episode-linking backfill: the proxy reads every document of the episode instead of only those changed since the newest stored one. Keyed apart from the hourly sync of the same episode so neither dedupes the other away.

        • syncDocuments: boolean
      • metadata: MessageMetadata
      • OptionalparentSpanContext: SpanContext

      Returns Promise<void>

    • Optional hook for subclasses to attach extra non-PII span attributes derived from params. The returned attributes appear on the service.${serviceName} span (and, for jobs, on job.enqueue.* and job.run.* spans too).

      Safe ID-like keys on params (e.g. patientId, templateIds, limit, cursor) are already auto-extracted via toSpanAttributes("service.input", params) — you don't need to enumerate them here. Use this hook for derived/computed attributes (counts, flags, computed names) or for fields you've explicitly verified safe but that aren't in the global allowlist. Hook output overrides auto-extracted attributes on key collision.

      Safe by default: returns {} when not overridden. Only declare keys you've confirmed are non-PII. Never include emails, names, dob, free-text, etc.

      Parameters

      • _params: {
            chartReviewJobs?: {
                entityId: EntityId;
                key: string;
                patientId: PatientId;
                templateIds: [AuditTemplateId];
                triggerId: ChartReviewTriggerId;
                ttlHours: number;
            }[];
            drainProxyPdfs?: boolean;
            entityId: EntityId;
            episodeId: TreatmentEpisodeId;
            manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
            readWholeEpisode?: boolean;
            syncDocuments: boolean;
        }
        • OptionalchartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[]
        • OptionaldrainProxyPdfs?: boolean

          Set only by the manual "Sync Episode" action (SyncEpisodeService, run inline so never keyed). No longer consulted for proxy integrations: their PDF fetch chain always hands the episode to the submit drain, which works through the whole episode whether the sync was manual or hourly.

        • entityId: EntityId
        • episodeId: TreatmentEpisodeId
        • Optionalmanual?: { createdBy: UserId; templateIds: AuditTemplateId[] }

          Present when the run is UI-initiated (Sync & Run). Routes to the manual enqueue pipeline.

        • OptionalreadWholeEpisode?: boolean

          Set by the manual "Sync Episode" action and the episode-linking backfill: the proxy reads every document of the episode instead of only those changed since the newest stored one. Keyed apart from the hourly sync of the same episode so neither dedupes the other away.

        • syncDocuments: boolean

      Returns Attributes

    • Optional hook for subclasses to attach extra non-PII span attributes derived from the successful service result. Only called when run resolves successfully; on error, the span is marked via markSpanError instead.

      Safe by default: returns {} when not overridden. Only declare keys you've confirmed are non-PII (e.g. counts, status flags, computed summaries). Never include emails, names, dob, free-text, etc.

      Parameters

      • _result: SyncEpisodeJobResult

      Returns Attributes

    • Parameters

      • params: unknown

      Returns params is {
          chartReviewJobs?: {
              entityId: EntityId;
              key: string;
              patientId: PatientId;
              templateIds: [AuditTemplateId];
              triggerId: ChartReviewTriggerId;
              ttlHours: number;
          }[];
          drainProxyPdfs?: boolean;
          entityId: EntityId;
          episodeId: TreatmentEpisodeId;
          manual?: { createdBy: UserId; templateIds: AuditTemplateId[] };
          readWholeEpisode?: boolean;
          syncDocuments: boolean;
      }