/** * Dispatch priorities: the MM table behind the GraphQL roots * `erpWorkOrderOperationDispatchPriorities` / `erpWorkOrderOperationDispatchPriority`. * * An ordering authority (a scheduler such as PFM) ranks work order operations within a * **scope** — an ERP resource id for a machine or a machine group. The unit of validity is * the scope: a list is complete and consistent for its scope, so ingest is a **scope-level * replace** (delete every row with that `scopeResourceId`, insert the new list) in one * transaction — never a per-row upsert, which would leave rows behind for operations that * left the list. * * This is the only SDK write that goes through the MM GraphQL proxy instead of erp-api * (`/erp/v1/...`): erp-api has no endpoint for the table, and a Hasura mutation document * runs its delete + insert atomically. Because the write never enters `_sendToAPI`, the * local dedupe cache is not consulted — intentional, since an unchanged scope must still * be re-asserted and a replace that drops rows must never be skipped as a "duplicate". * * Verified against the live schema (2026-09-14): * - Primary key is a surrogate `dispatchPriorityRef` (bigint). * - Unique constraint `work_order_operation_dispatch_priority_uq` covers * (companyId, scopeResourceId, workOrderId, sequenceNumber, lot, split, sub) — MM's * operation identity per scope. `authorityOperationId` is NOT part of it (and is nullable * in MM), so two authority rows resolving to the same MM operation in one scope would * fail the whole replace; {@link dedupeDispatchPriorityRows} keeps the better-ranked one * (the earlier one only on an exact tie). * - `sequenceNumber` and `priority` are float32 (`real`) columns, and so is the * `sequence_number` they join to on `erpWorkOrderOperations`. The SDK therefore compares * these values *in float32*, the way MM does: the identity key holds the sequence number * as MM will store it, and the rank check compares priorities as MM will store them. That * is what makes the double-vs-float32 seam disappear rather than move. The bounds in * `dispatch-priority-schemas.ts` are domain sanity on top, not what correctness rests on. * - `lot`, `split`, `sub` are NOT NULL with default `''`. The `workOrderOperation` * relationship joins on workOrderId + sequenceNumber + lot + split + sub, so rows must * carry MM's own values for those columns to resolve. * - No foreign keys: rows whose operation or resource MM does not have are accepted and * their relationships resolve to null (queryable with `unresolvedOnly`). * - `companyId` is a session preset; it is not part of the insert input. * * **Single-writer assumption.** These writes assume MachineMetrics' deployment model: one * connector writes a given company's data, single-threaded and in sequence. Under that * assumption two replacements of the same scope never overlap, and the atomic delete + * insert is sufficient on its own. * * The assumption is load-bearing rather than incidental, so it is worth stating. Nothing in * the database serializes concurrent replacements of one scope: both transactions can * delete, then insert disjoint operation identities, leaving the scope holding the union of * two lists that were each meant to be complete. Measured against the live tenant, three * concurrent writers on one scope merged in 6 of 8 attempts, and the failure is silent — * every mutation reports success. Anything that breaks the assumption therefore corrupts a * scope quietly: a second connector for the company, a sweep that overlaps its predecessor, * or a manual tool run alongside a cycle. * * There is no defence available at this layer if it is ever broken — all 98 mutation roots * on the proxy are plain CRUD, with no advisory lock or serializing action — so concurrent * writers would need an MM-side endpoint or backend function that serializes the replace. */ import { MMReceiveDispatchPriority } from "./types/receive-types.js"; import type { MMSendDispatchPriority } from "./types/send-types.js"; import { type GraphqlOperation } from "./graphql-helpers.js"; export declare const DISPATCH_PRIORITIES_QUERY_ROOT = "erpWorkOrderOperationDispatchPriorities"; export declare const DISPATCH_PRIORITIES_INSERT_ROOT = "insertErpWorkOrderOperationDispatchPriorities"; export declare const DISPATCH_PRIORITIES_DELETE_ROOT = "deleteErpWorkOrderOperationDispatchPriorities"; export declare const DISPATCH_PRIORITIES_INSERT_INPUT_TYPE = "erpWorkOrderOperationDispatchPriorities_insert_input"; export declare const DISPATCH_PRIORITIES_BOOL_EXP_TYPE = "erpWorkOrderOperationDispatchPriorities_bool_exp"; /** Field list for the dispatch-priorities GraphQL reads. */ export declare const DISPATCH_PRIORITY_GRAPHQL_FIELDS = "dispatchPriorityRef companyId authorityOperationId scopeResourceId workOrderId sequenceNumber lot split sub preallocatedMachineResourceId priority createdAt updatedAt customFieldsSchemaRef customFields"; /** The columns MM's unique constraint compares (besides the session `companyId`). */ export interface DispatchPriorityOperationIdentity { workOrderId: string; sequenceNumber: string | number; lot: string; split: string; sub: string; } /** * Canonical **display** form of a sequence number: `"010"`, `10` and `"10"` all render as * `"10"`. Finite values are rendered through `Number`; anything else is kept as trimmed * text so a malformed value still produces a stable (and visibly odd) string rather than * throwing here. * * This is what {@link MMReceiveDispatchPriority.sequenceNumber} exposes, matching * {@link MMReceiveWorkOrderOperation.sequenceNumber}, and it is what the identity key uses. */ export declare function normalizeDispatchSequenceNumber(value: string | number | null | undefined): string; /** * Key over the unique-constraint columns, as MM will see them after * `MMSendDispatchPriority.toGraphqlInsertInput()` trims the text columns. * * Parts are `|`-joined with `\` and `|` escaped in each part, so the encoding is * unambiguous: a literal `|` inside a work order id or lot cannot shift the part * boundaries and make two different operations share a key (which would silently drop a * valid row from a scope's list). * * The sequence number is keyed **as MM stores it**: `String(Math.fround(x))`. MM compares * this column as float32, so comparing it any other way lets two rows that MM treats as one * key apart, and the insert then fails the unique constraint instead of the collision being * reported as a duplicate. A double's string form is unique per value, so two rows share a * key exactly when MM would store the same number — no more and no less. The part is exact * rather than pretty (`10.1` keys as `10.100000381469727`); treat the key as opaque and use * {@link normalizeDispatchSequenceNumber} for anything a person reads. * * Text parts are trimmed, because that is the form the SDK writes. A row MM holds with * untrimmed text — which only another writer could have produced — therefore keys as its * trimmed form. The SDK's own replace deletes every row of the scope regardless, so such * a row does not survive a cycle either way. * * The key omits `scopeResourceId` on purpose: it identifies an operation *within* a scope, * and {@link dedupeDispatchPriorityRows} is defined per scope. Do not feed it a list that * spans scopes. * * Deliberately **not** `normalizeWorkOrderOperationKey` (the reconciliation key): that one * upper-cases, which is right for matching ERP keys against MM rows but wrong here — MM's * unique index compares text exactly, so two rows differing only in case are two rows to * MM, and folding them would drop a row MM would have accepted. */ export declare function dispatchPriorityIdentityKey(row: DispatchPriorityOperationIdentity): string; export interface DispatchPriorityDroppedDuplicate { /** The row that was dropped (its list position is `index`). */ row: MMSendDispatchPriority; index: number; /** The row kept instead: better-ranked, or the earlier of the two on a tie. */ duplicateOf: MMSendDispatchPriority; identityKey: string; } export interface DedupeDispatchPriorityRowsResult { kept: MMSendDispatchPriority[]; dropped: DispatchPriorityDroppedDuplicate[]; } /** * Drops rows that would violate `work_order_operation_dispatch_priority_uq` within one scope. * * Selection is **by rank, not position**: of two rows naming the same MM operation, the one * with the lower `priority` survives wherever it sits in the list — compared as MM will * store it, so two ranks MM cannot tell apart are a tie here too. Nothing requires the * caller to pass rows in rank order — this helper does not reorder them, and MM re-sorts by * `priority` on read — so position carries no meaning for choosing between different ranks; * a caller that builds its list by iterating a map would otherwise have an arbitrary row * survive. * * Position is used for exactly one thing: when two ranks are **equal**, the earlier row is * kept, so the result is deterministic rather than depending on map iteration order. * * `kept` keeps the caller's ordering; sorting is the write path's job, which puts the * survivors in rank order with {@link sortByRank} after selection is over. The caller * decides how loudly to report `dropped`. */ export declare function dedupeDispatchPriorityRows(rows: readonly MMSendDispatchPriority[]): DedupeDispatchPriorityRowsResult; /** * Fails fast — before any network call — on rows MM would store with a meaning the caller * did not intend. The field rules live in `dispatch-priority-schemas.ts`; this adds the one * cross-field rule the schema cannot see, that a row must not name a different scope. * * Nothing in the table enforces `authorityOperationId` (nullable, non-unique), so the SDK * requires it: a dispatch row without the authority's operation id cannot be traced back to * the ordering it came from. * * @throws {Error} naming the offending row and field */ export declare function assertValidDispatchPriorityRows(scopeResourceId: string, rows: readonly MMSendDispatchPriority[]): void; /** * The rows in the order they should be **sent**: by rank, then by the caller's position. * * Hasura assigns `dispatchPriorityRef` in insert order and every SDK read tie-breaks on it, * so sending in rank order lets the authority's ordering survive even where two of its ranks * land on one value in the float32 column: MM cannot separate them, but it returns them in * the order they arrived. * * Sorting on the **raw** rank is what makes that work. `Math.fround` is monotonic, so the * raw values already order the stored ones correctly, and they still carry the distinction * the column is about to lose — rounding before comparing would throw that away and let the * caller's array order decide, which is the thing this function exists to stop mattering. */ export declare function sortByRank(rows: readonly MMSendDispatchPriority[]): MMSendDispatchPriority[]; /** * Ranks that MM will store as one value although the authority made them different. * * `priority` is a `real` column, which stops separating neighbouring values long before it * overflows: at around 1.7e12 — the magnitude of epoch milliseconds — its granularity is * about 131072, so ranks one millisecond apart collapse into one. Within MM the tie is then * broken by `dispatchPriorityRef`, which is why {@link sortByRank} sends rows in rank * order: the authority's order still comes back out. This reports the collapse so an * operator can see that ranks at this magnitude are not distinct to MM. It is not fatal — * failing the write would leave the scope's *previous* list standing indefinitely, a worse * outcome than a tie MM breaks in the right order. * * Equal ranks are not collisions; this fires only when ranks that were different would stop * being different. Run it on the rows that will actually be sent, after deduplication. * * One entry per collapse, each naming just the two ranks and the stored value: a scope * ranked by epoch milliseconds collapses on nearly every row, so callers should report the * count and log a capped sample rather than a line apiece. */ export declare function ranksCollapsedByStorage(rows: readonly MMSendDispatchPriority[]): string[]; /** * One document, one transaction: delete the scope's rows, then insert the new list. * Hasura runs a mutation document's top-level fields sequentially inside a single * transaction and rolls the whole document back if any field fails (verified: a * uniqueness violation in the insert left the previously deleted rows in place). * An empty `objects` list is accepted and clears the scope (verified: 200, 0 rows). */ export declare function buildReplaceDispatchPriorityScopeMutation(scopeResourceId: string, objects: Record[]): GraphqlOperation; export declare function buildRemoveDispatchPriorityScopeMutation(scopeResourceId: string): GraphqlOperation; export interface FetchDispatchPrioritiesFilter { /** Exact `scopeResourceId` match. Combined with `scopeResourceIds` as one `_in` list. */ scopeResourceId?: string; scopeResourceIds?: readonly string[]; /** Exact `workOrderId` match. Combined with `workOrderIds` as one `_in` list. */ workOrderId?: string; workOrderIds?: readonly string[]; /** * Only rows whose `workOrderOperation` relationship is null — i.e. rows that resolve to * no MM operation on (workOrderId, sequenceNumber, lot, split, sub). Validation tooling. */ unresolvedOnly?: boolean; /** Rows per GraphQL page (default {@link DEFAULT_GRAPHQL_PAGE_SIZE}). */ pageSize?: number; } /** * Hasura `_bool_exp` for {@link FetchDispatchPrioritiesFilter}; `{}` matches every row of * the company. Returned as a variable value, never interpolated, so ids need no escaping. * Returns `null` when a filter was given (a value, a list, or `null` from an untyped * caller) but nothing survives trimming — the caller must short-circuit to "no rows" * rather than query, because an empty `_in` would otherwise be dropped and match everything. */ export declare function buildDispatchPrioritiesWhere(filter: FetchDispatchPrioritiesFilter): Record | null; export declare function buildDispatchPrioritiesPageQuery(where: Record, pageSize: number, offset: number): GraphqlOperation; /** * The distinct scopes currently holding rows, one page. Used to find scopes an authority has * stopped issuing, without reading every row of every scope. * * `order_by` must lead with the `distinct_on` column: Hasura rejects the query outright * otherwise ("distinct_on columns must match initial order_by columns"), so this ordering is * load-bearing rather than cosmetic. */ export declare function buildDispatchPriorityScopesPageQuery(pageSize: number, offset: number): GraphqlOperation; /** * Maps one GraphQL row to {@link MMReceiveDispatchPriority}. Hasura renders `bigint` as a * string or number depending on configuration, and `Float` as a number or string depending * on the value; the schema accepts either. `sequenceNumber` is exposed as the string form * the other receive models use. * * @throws {Error} naming the field when the row is not the shape MM documents */ export declare function parseDispatchPriorityRow(raw: unknown): MMReceiveDispatchPriority; export interface ReplaceDispatchPriorityScopeResult { scopeResourceId: string; /** Rows MM deleted for the scope before inserting (the scope's previous list). */ deletedRows: number; /** Rows inserted (after duplicate dropping). */ insertedRows: number; /** Rows dropped because another row named the same MM operation and was kept. */ droppedDuplicates: DispatchPriorityDroppedDuplicate[]; } export interface RemoveDispatchPriorityScopeResult { scopeResourceId: string; deletedRows: number; } /** * Reads `affected_rows` from a mutation response field. A field that is missing, null, or * not a number is a malformed response, not "zero rows affected". */ export declare function readAffectedRows(response: unknown, field: string, operation: string): number; //# sourceMappingURL=dispatch-priorities.d.ts.map