import { KeyStoreParams, KeyType } from '../../../../modules/key'; import { ILogger } from '../../../logger'; import { ActivityType, Consumes } from '../../../../types/activity'; import { AppVID } from '../../../../types/app'; import { HookRule, HookSignal } from '../../../../types/hook'; import { HotMeshApp, HotMeshApps, HotMeshSettings, ScoutType } from '../../../../types/hotmesh'; import { ProviderClient, ProviderTransaction } from '../../../../types/provider'; import { SymbolSets, StringStringType, StringAnyType, Symbols } from '../../../../types/serializer'; import { IdsData, JobStatsRange, StatsType } from '../../../../types/stats'; import { Transitions } from '../../../../types/transition'; import { JobInterruptOptions } from '../../../../types/job'; import { WorkListTaskType } from '../../../../types/task'; import { ThrottleOptions } from '../../../../types/quorum'; import { StoreService } from '../..'; import { AccumulateWake } from './escalation-accumulate'; import { PostgresClientType } from '../../../../types'; import { KVSQL } from './kvsql'; import { KVTables } from './kvtables'; declare class PostgresStoreService extends StoreService { pgClient: PostgresClientType; kvTables: ReturnType; isScout: boolean; /** Set by HotMesh.init() when `events.publish` is configured. Used by hook.ts Leg1 path. */ eventsPublish?: (event: import('../../../../types/system_events').SystemEvent) => void | Promise; transact(): ProviderTransaction; constructor(storeClient: ProviderClient); init(namespace: string, appId: string, logger: ILogger, guid?: string, role?: string): Promise; isSuccessful(result: any): boolean; delistSignalKey(key: string, target: string): Promise; zAdd(key: string, score: number | string, value: string | number, transaction?: ProviderTransaction): Promise; zRangeByScore(key: string, score: number | string, value: string | number): Promise; mintKey(type: KeyType, params: KeyStoreParams): string; /** * strongly types the transaction or storeClient as KVSQL, * so methods are visible to the compiler/code editor */ kvsql(transaction?: ProviderTransaction): KVSQL; invalidateCache(): void; /** * At any given time only a single engine will * check for and process work items in the * time and signal task queues. */ reserveScoutRole(scoutType: ScoutType, delay?: number): Promise; releaseScoutRole(scoutType: ScoutType): Promise; getSettings(bCreate?: boolean, guid?: string, role?: string): Promise; setSettings(manifest: HotMeshSettings): Promise; registerConnection(guid: string, role: string, version: string): Promise; reserveSymbolRange(target: string, size: number, type: 'JOB' | 'ACTIVITY', tryCount?: number): Promise<[number, number, Symbols]>; getAllSymbols(): Promise; getSymbols(activityId: string): Promise; addSymbols(activityId: string, symbols: Symbols): Promise; seedSymbols(target: string, type: 'JOB' | 'ACTIVITY', startIndex: number): StringStringType; seedJobSymbols(startIndex: number): StringStringType; seedActivitySymbols(startIndex: number, activityId: string): StringStringType; getSymbolValues(): Promise; addSymbolValues(symvals: Symbols): Promise; getSymbolKeys(symbolNames: string[]): Promise; getApp(id: string, refresh?: boolean): Promise; setApp(id: string, version: string): Promise; activateAppVersion(id: string, version: string): Promise; registerAppVersion(appId: string, version: string): Promise; setStats(jobKey: string, jobId: string, dateTime: string, stats: StatsType, appVersion: AppVID, transaction?: ProviderTransaction): Promise; hGetAllResult(result: any): any; getJobStats(jobKeys: string[]): Promise; getJobIds(indexKeys: string[], idRange: [number, number]): Promise; setStatus(collationKeyStatus: number, jobId: string, appId: string, transaction?: ProviderTransaction): Promise; /** * 1) HIGH-LEVEL STORE METHOD (engine/activity-facing) * --------------------------------------------------- * Mirrors setStatus(), but performs the compound Step-2 requirement: * - apply delta to job semaphore (jobs.status) * - compute thresholdHit (0/1) for desired threshold * - persist thresholdHit onto the Leg2 GUID ledger by incrementing the 100B digit (or other weight) * - return thresholdHit (0/1) */ setStatusAndCollateGuid(statusDelta: number, // typically (N - 1) threshold: number, // typically 0 (but supports 0,1,12,...) jobId: string, appId: string, guidField: string, // the jobs_attributes.field for the Leg2 GUID ledger row guidWeight: number, // e.g. 100_000_000_000 for GUID 100B digit transaction?: ProviderTransaction): Promise; getStatus(jobId: string, appId: string): Promise; setState({ ...state }: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, transaction?: ProviderTransaction): Promise; /** * Returns custom search fields and values. * NOTE: The `fields` param should NOT prefix items with an underscore. * NOTE: Literals are allowed if quoted. */ getQueryState(jobId: string, fields: string[]): Promise; getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>; getRaw(jobId: string): Promise; /** * collate is a generic method for incrementing a value in a hash * in order to track their progress during processing. */ collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, transaction?: ProviderTransaction): Promise; /** * Compound Leg2 entry: atomically increments the activity Leg2 entry * counter and seeds the GUID ledger with the ordinal IF NOT EXISTS. * Returns [activityValue, guidValue]. */ collateLeg2Entry(jobId: string, activityId: string, guid: string, dIds: StringStringType, transaction?: ProviderTransaction): Promise<[number, number]>; /** * Synthentic collation affects those activities in the graph * that represent the synthetic DAG that was materialized during compilation; * Synthetic collation distinguishes `re-entry due to failure` from * `purposeful re-entry`. */ collateSynthetic(jobId: string, guid: string, amount: number, transaction?: ProviderTransaction): Promise; setStateNX(jobId: string, appId: string, status?: number, entity?: string, transaction?: ProviderTransaction, originId?: string, parentId?: string): Promise; getSchema(activityId: string, appVersion: AppVID): Promise; getSchemas(appVersion: AppVID): Promise>; setSchemas(schemas: Record, appVersion: AppVID): Promise; setSubscriptions(subscriptions: Record, appVersion: AppVID): Promise; getSubscriptions(appVersion: AppVID): Promise>; getSubscription(topic: string, appVersion: AppVID): Promise; setTransitions(transitions: Record, appVersion: AppVID): Promise; getTransitions(appVersion: AppVID): Promise; setHookRules(hookRules: Record): Promise; getHookRules(): Promise>; /** * Leg1: set hook signal, atomically detecting a pending signal. * * Standalone (no transaction): INSERT ON CONFLICT no-op captures the * current row value via RETURNING, then a follow-up UPDATE overwrites * with the hook value. No explicit transaction — the two statements * are serialized by the single-threaded event loop on the shared * connection. * * In a transaction: queues the setnxex; pending detection deferred. */ setHookSignal(hook: HookSignal, transaction?: ProviderTransaction, redelivery?: { aid: string; topic: string; }): Promise<{ success: boolean; pendingData?: string; }>; /** * Leg2: get hook signal OR atomically set a pending signal. * * When `pendingData` is provided and no hook signal exists, the * pending value is stored so leg1's setHookSignal can detect it. * * INSERT ON CONFLICT no-op captures the current row value via * RETURNING. If a hook exists, returns it without modification. * Otherwise stores $pending for leg1 to consume. * * When `pendingData` is omitted, behaves as a plain read. */ getHookSignal(topic: string, resolved: string, pendingData?: string, pendingExpire?: number): Promise; deleteHookSignal(topic: string, resolved: string): Promise; addTaskQueues(keys: string[]): Promise; getActiveTaskQueue(): Promise; deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string, scrub?: boolean): Promise; processTaskQueue(sourceKey: string, destinationKey: string): Promise; expireJob(jobId: string, inSeconds: number, transaction?: ProviderTransaction): Promise; getDependencies(jobId: string): Promise; /** * Register a time hook by inserting a TIMEHOOK message directly into * engine_streams with a future visible_at. The message is invisible * until the sleep duration elapses, then the engine's normal dequeue * picks it up — no intermediate table, no polling, fully transactional. */ registerTimeHook(jobId: string, gId: string, activityId: string, _type: WorkListTaskType, deletionTime: number, dad: string, transaction?: ProviderTransaction): Promise; /** * Disarms a scheduled timehook (soft delete) for one activity of a * job — the mirror of registerTimeHook, called when the SIGNAL wins * an SLA-gated wait so the armed timeout cannot fire against the * settled workflow. Scoped by the jid index, then narrowed to the * activity via the message metadata. Dimensional addressing is * deliberately NOT matched: a job's waits on one activity are * sequential (cycle N settles before cycle N+1 arms), so at most one * timer per (jid, aid) is armed at a time, and the signal composite's * address can differ from the stored one at cycle offsets. */ expireTimeHook(jobId: string, activityId: string): Promise; getNextTask(_listKey?: string): Promise<[ listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType ] | boolean>; /** * Interrupts a job and sets sets a job error (410), if 'throw'!=false. * This method is called by the engine and not by an activity and is * followed by a call to execute job completion/cleanup tasks * associated with a job completion event. * * Todo: move most of this logic to the engine (too much logic for the store) */ interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise; scrub(jobId: string): Promise; findJobs(queryString?: string, limit?: number, batchSize?: number, cursor?: string): Promise<[string, string[]]>; findJobFields(jobId: string, fieldMatchPattern?: string, limit?: number, batchSize?: number, // Unused in SQL provider cursor?: string): Promise<[string, Record]>; setCancel(jobId: string, appId: string): Promise; setThrottleRate(options: ThrottleOptions): Promise; getThrottleRates(): Promise; getThrottleRate(topic: string): Promise; /** * Deploy time-aware notification triggers and functions */ private deployTimeNotificationTriggers; /** * Fetch and decode a single compressed-symbol field from a job's HASH. * Reads one field with `hmget` (no full-hash scan), then decodes it with * the same rules the serializer uses on write. Powers narrow getters such * as the workflow input arguments without a full export. */ getJobArguments(jobId: string, symbolField: string): Promise; /** * Fetch activity inputs for a workflow. Used by the exporter to enrich * timeline events with activity arguments. */ getActivityInputs(workflowId: string, symbolField: string): Promise<{ byJobId: Map; byNameIndex: Map; }>; /** * Fetch child workflow inputs in batch. Used by the exporter to enrich * child workflow events with their arguments. */ getChildWorkflowInputs(childJobKeys: string[], symbolField: string): Promise>; /** * Fetch job record and attributes by key. Used by the exporter to * reconstruct execution history for expired jobs. */ getJobByKeyDirect(jobKey: string): Promise<{ job: { id: string; key: string; status: number; created_at: Date; updated_at: Date; expired_at?: Date; is_live: boolean; }; attributes: Record; }>; /** * Single indexed lookup of the lineage columns for a job key. Returns the real * spawning parent (never the synthetic collator `$C` job) and the root ancestor. */ getJobLineage(jobKey: string): Promise<{ parent_id: string | null; origin_id: string | null; } | null>; /** * Fetch stream message history for a job from worker_streams. * Returns raw activity input/output data from soft-deleted messages. */ getStreamHistory(jobId: string, options?: { activity?: string; types?: string[]; }): Promise; /** * Parse a HotMesh-encoded value string. * Values may be prefixed with `/s` (JSON), `/d` (number), `/t` or `/f` (boolean), `/n` (null). */ private parseHmshValue; private _escalationInsertSql; private _escalationInsertParams; createEscalation(params: import('../../../../types/hmsh_escalations').CreateEscalationParams): Promise; /** * Enqueues the escalation INSERT into an existing Leg1 transaction so the row * is written atomically with the job state checkpoint. On conflict * (ON CONFLICT DO NOTHING) the command is a no-op, making it safe for * idempotent re-runs after a crash. */ addEscalationToTransaction(params: import('../../../../types/hmsh_escalations').CreateEscalationParams, transaction: import('../../../../types/provider').ProviderTransaction): void; /** * Full-fidelity INSERT for data migration. Preserves the original `id` (UUID), * lifecycle state, and timestamps from the source table. Uses * `ON CONFLICT (id) DO NOTHING` so re-running a migration batch is safe — * rows that already exist are skipped and `null` is returned for them. */ createEscalationForMigration(params: import('../../../../types/hmsh_escalations').MigrateEscalationParams): Promise; getEscalation(id: string, namespace?: string): Promise; getEscalationBySignalKey(signalKey: string, namespace?: string): Promise; /** * Transition a wait's escalation row to `expired` when its resume timer * fires first (`condition(signalId, { ..., timeout })`). Guarded by * `status = 'pending'`: a signal that won the race already resolved the * row and is never touched. An accumulator row (or a batch row with * `partialOnTimeout`) has its `resolver_payload` set to the delivered * collection in the same UPDATE, so row truth and the waiter's value * agree. Returns the expired row plus the row's prior status, so the * caller can tell "a resolve won" (row exists, not pending) from "no * row" (the wait carried no escalation). */ expireEscalationBySignalKey(signalKey: string, namespace?: string, appId?: string): Promise; private _escalationFilterConditions; listEscalations(params?: import('../../../../types/hmsh_escalations').ListEscalationsParams): Promise; countEscalations(params?: import('../../../../types/hmsh_escalations').ListEscalationsParams): Promise; claimEscalation(params: import('../../../../types/hmsh_escalations').ClaimEscalationParams): Promise; claimEscalationByMetadata(params: import('../../../../types/hmsh_escalations').ClaimByMetadataParams): Promise; releaseEscalation(params: import('../../../../types/hmsh_escalations').ReleaseEscalationParams): Promise; /** * Composes the wake as a data-modifying CTE appended to a settle * statement (resolve/cancel). The INSERT fires iff the settle CTE * (`fromCTE`) produced a row whose signal_key matches the wake — one * atomic statement, safe on any connection type (Client or Pool), * with no transaction window for unrelated queries to interleave * into. Appends the wake params to `params` in place. */ private composeEscalationWakeCTE; resolveEscalation(params: import('../../../../types/hmsh_escalations').ResolveEscalationParams, wakeCommand?: import('../../../../types/hmsh_escalations').EscalationWakeCommand, resolutionJson?: string | null): Promise; /** * Non-locking preview of the row `resolveEscalationByMetadata` would * select — used to pre-build the wake command before the resolve * transaction opens. The `forSignalKey` guard on the wake command * handles the race where a different row wins the lock. */ peekEscalationByMetadata(params: import('../../../../types/hmsh_escalations').ResolveByMetadataParams): Promise<{ id: string; signalKey: string | null; topic: string | null; } | null>; resolveEscalationByMetadata(params: import('../../../../types/hmsh_escalations').ResolveByMetadataParams, wakeCommand?: import('../../../../types/hmsh_escalations').EscalationWakeCommand, resolutionJson?: string | null): Promise; /** * Composes the batch-completion wake CTE. Unlike * `composeEscalationWakeCTE`, the signal payload is only knowable inside * the statement (the assembled item collection), so the pre-built message * carries a placeholder that `jsonb_set` overwrites at `{data,data}` — * the exact slot `condition()` unwraps. The jsonb round-trip may reorder * keys; the engine parses the message by key, so this is safe. The INSERT * fires only when the fill CTE resolved the row (last item) and its * signal_key matches the wake. */ private composeBatchWakeCTE; /** * The shared body of both batch-item store ops: guarded fill of one item * into `envelope.batch_items` (with its `batch_filled_at` stamp — the * database clock, row truth), `batch_pending`/`batch_count` recompute, * resolve-on-last-item with the assembled collection as * `resolver_payload`, wake enqueue, and outcome classification — one * atomic statement. `targetCTE` supplies the row selector; it must expose * id, signal_key, topic, status, assigned_to, assigned_until, batch_keys, * batch_pending. */ private _batchItemStatement; private _mapBatchItemRow; /** * Fills one declared item of a batch escalation — see * `_batchItemStatement` for the atomicity contract. Selects the row by * `id` or `signalKey` (exactly one required). `resolutionJson` is the * pre-serialized `$resolution` object merged into the delivered * collection ONLY (never into the stored `resolver_payload`). */ resolveEscalationBatchItem(params: import('../../../../types/hmsh_escalations').ResolveBatchItemParams, wakeCommand?: import('../../../../types/hmsh_escalations').EscalationWakeCommand, resolutionJson?: string | null): Promise; /** * Batch-item fill selecting the row by metadata facet — the highest * priority pending row whose `metadata` contains the key/value, mirroring * `resolveEscalationByMetadata`'s selector. No claim assertion on this * variant, matching the by-metadata precedent. */ resolveEscalationBatchItemByMetadata(params: import('../../../../types/hmsh_escalations').ResolveBatchItemByMetadataParams, wakeCommand?: import('../../../../types/hmsh_escalations').EscalationWakeCommand, resolutionJson?: string | null): Promise; private _accumulateSelector; private _mapAccumulateRows; /** * Appends one item to an accumulator escalation, and the container's id * to the reciprocal row when one is named, in ONE statement: both rows * locked in id order, both guards evaluated before either write, both * writes or neither. A side reaching `max` (with `resolveAtMax`) resolves * with the ordered collection as `resolver_payload` and its wake commits * in the same statement. See `buildAccumulateStatement`. */ accumulateEscalationItem(params: import('../../../../types/hmsh_escalations').AccumulateItemParams, wakes?: AccumulateWake[]): Promise; /** By-metadata form of `accumulateEscalationItem`, mirroring the * `resolveEscalationByMetadata` selector. No claim assertion. */ accumulateEscalationItemByMetadata(params: import('../../../../types/hmsh_escalations').AccumulateItemByMetadataParams, wakes?: AccumulateWake[]): Promise; private _runAccumulate; private _mapRemoveRows; /** * Removes one held item from an accumulator escalation (and the * container's id from the reciprocal row when named) in ONE guarded * statement. Never wakes the waiter and never changes status. See * `buildRemoveStatement`. */ removeAccumulatedEscalationItem(params: import('../../../../types/hmsh_escalations').RemoveAccumulatedItemParams): Promise; /** By-metadata form of `removeAccumulatedEscalationItem`. */ removeAccumulatedEscalationItemByMetadata(params: import('../../../../types/hmsh_escalations').RemoveAccumulatedItemByMetadataParams): Promise; private _runRemove; cancelEscalation(id: string, namespace?: string, wakeCommand?: import('../../../../types/hmsh_escalations').EscalationWakeCommand): Promise; escalateEscalationToRole(params: import('../../../../types/hmsh_escalations').EscalateToRoleParams): Promise; updateEscalation(params: import('../../../../types/hmsh_escalations').UpdateEscalationParams): Promise; appendEscalationMilestones(params: import('../../../../types/hmsh_escalations').AppendMilestonesParams): Promise; claimManyEscalations(params: import('../../../../types/hmsh_escalations').ClaimManyParams): Promise<{ entries: import('../../../../types/hmsh_escalations').EscalationEntry[]; skipped: number; }>; /** * Atomic query-form bulk claim: the selector composes into the UPDATE's own * WHERE via `_escalationFilterConditions`, so selection and claim are one * statement — no SELECT-then-claim window. `status='pending'` and the * claimability guard are forced regardless of the selector. */ claimManyEscalationsByQuery(params: import('../../../../types/hmsh_escalations').ClaimManyByQueryParams): Promise; escalateManyEscalationsToRole(params: import('../../../../types/hmsh_escalations').EscalateManyToRoleParams): Promise; updateManyEscalationsPriority(params: import('../../../../types/hmsh_escalations').UpdateManyPriorityParams): Promise; resolveManyEscalations(params: import('../../../../types/hmsh_escalations').ResolveManyParams): Promise; /** * All-or-none bulk resolve with per-row payloads. One SQL statement: * the FOR UPDATE lock (ordered by id — deterministic acquisition, no * deadlock between overlapping batches), the count-gated UPDATE, and * the per-row wake INSERTs are one atomic unit. When ANY row is * missing, non-pending, or fails the assignee assertion, the gate * count mismatches, the UPDATE matches zero rows, no wake is written, * and the statement degrades to a pure read — the returned snapshot * names each blocking row and why. Same durability contract as * `resolveEscalation`: each wake commits WITH its row's resolve. */ resolveAllOrNoneEscalations(params: import('../../../../types/hmsh_escalations').ResolveAllOrNoneParams, wakeCommands?: import('../../../../types/hmsh_escalations').EscalationWakeCommand[], resolutionJsons?: (string | null)[]): Promise<{ ok: true; entries: import('../../../../types/hmsh_escalations').EscalationEntry[]; wakeCount: number; } | { ok: false; failed: Array<{ id: string; reason: import('../../../../types/hmsh_escalations').ResolveAllOrNoneBlockReason; }>; }>; escalationStats(params?: import('../../../../types/hmsh_escalations').StatsEscalationsParams): Promise; listDistinctEscalationTypes(namespace?: string): Promise; releaseExpiredEscalations(_namespace?: string): Promise; /** * Deletes terminal escalation rows (`resolved`/`cancelled`/`expired`) whose * `updated_at` is older than the given horizon. Terminal rows are inert — * every state transition guards `status = 'pending'` — so pruning them is * the engine-blessed retention path for the audit backlog. * * Single statement: the candidate SELECT (`FOR UPDATE SKIP LOCKED`) and the * DELETE are one atomic unit, so concurrent pruners and readers never block * each other and a row is either fully gone or fully present. `limit` bounds * rows per call to keep vacuum pressure flat; callers loop until `deleted` * returns 0. */ pruneEscalations(params: import('../../../../types/hmsh_escalations').PruneEscalationsParams): Promise; } export { PostgresStoreService };