UNPKG

@lifi/compose-spec

Version:

Public wire-format types and schemas for Compose flows

83 lines (74 loc) 3.38 kB
/** * Compose contract version compatibility. * * The version triple carries the compatibility class, and the rule is * permanent: 1.0 does not change it. A patch bump is additive: new ops, edge * families, materialisers, guards, preconditions, or new optional config * fields. A minor or major bump is breaking: anything removed, any existing * schema changed, or a semantics change under an unchanged wire shape. Both * sides of the handshake (see `headers.ts`) decide with `checkCompatibility`, * so the rule lives in exactly one place. */ export interface ComposeVersion { readonly major: number; readonly minor: number; readonly patch: number; } export type VersionCompatibility = 'compatible' | 'sdk_outdated' | 'server_outdated'; const SEMANTIC_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; /** * Parses the numeric triple of a semantic version. Prerelease and build * suffixes are accepted and ignored: `0.5.1-staging.3` compares as `0.5.1`. * Returns `undefined` for anything that is not a complete semver string, * including the `dev` sentinel the SDK sends when it runs unbundled. */ export const parseVersion = (version: string): ComposeVersion | undefined => { const match = SEMANTIC_VERSION_PATTERN.exec(version); if (!match) return undefined; const major = Number(match[1]); const minor = Number(match[2]); const patch = Number(match[3]); if (![major, minor, patch].every(Number.isSafeInteger)) return undefined; return { major, minor, patch }; }; const compareVersions = (a: ComposeVersion, b: ComposeVersion): number => a.major !== b.major ? a.major - b.major : a.minor !== b.minor ? a.minor - b.minor : a.patch - b.patch; /** * Decides whether a client built against `client` may talk to a server that * serves `server` and accepts clients down to `minimum`. * * - A client ahead of the server in any component is `server_outdated`. Even * a patch-ahead client may send a field the server has never seen, and the * guarantee must not depend on every server schema staying strict. This is * the rollout window between an npm publish and the prod image bump. * - A client below `minimum` is `sdk_outdated`: the server no longer serves * the contract that client speaks. * - Anything in between is compatible. Patch releases only add, so a client * at or behind the server's patch never emits anything the server lacks; * a client on an older minor is served because the server still emits the * shapes that minor reads. * * `minimum` defaults to the server's own `major.minor.0`, which makes every * minor bump a hard cutover. The backend passes its explicit floor so a minor * bump can ship with the previous line still accepted; the SDK passes no floor * because the server, not the client, decides how far back it serves. */ export const checkCompatibility = ( client: ComposeVersion, server: ComposeVersion, minimum: ComposeVersion = { major: server.major, minor: server.minor, patch: 0, }, ): VersionCompatibility => { if (compareVersions(client, server) > 0) return 'server_outdated'; if (compareVersions(client, minimum) < 0) return 'sdk_outdated'; return 'compatible'; };