All files / src/api flowsheetRevisionScope.ts

91.42% Statements 32/35
83.78% Branches 31/37
100% Functions 0/0
91.17% Lines 31/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165    103x               103x                     482012x     269493x 269493x               486941x 486941x 486941x 486941x                                 268507x               268511x 268511x 268511x             94x               12452x               12993x                                   12997x   7807x     94x         12997x                       12993x           8348x     4645x 4645x 4645x 4645x 4645x           220881x 4810x                         255514x 255514x        
import { defaultSerializeQueryArgs } from "@reduxjs/toolkit/query";
 
const SAFE_REQUEST_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
 
export type RevisionRouteScope = {
  isPresent: boolean;
  rawValue: string | null;
  revisionStateId: number | null;
};
 
export const CURRENT_REVISION_ROUTE_SCOPE: RevisionRouteScope = {
  isPresent: false,
  rawValue: null,
  revisionStateId: null,
};
 
/** Normalize an optional route revision into one positive state identifier. */
export function normalizeRevisionStateId(
  value: string | null | undefined,
): number | null {
  if (!value || !/^\d+$/.test(value)) {
    return null;
  }
 
  const revisionStateId = Number(value);
  return Number.isSafeInteger(revisionStateId) && revisionStateId > 0
    ? revisionStateId
    : null;
}
 
/** Read a normalized revision identifier from an absolute or relative URL. */
export function getRevisionRouteScopeFromUrl(url: string): RevisionRouteScope {
  try {
    const parsedUrl = new URL(url, "http://localhost");
    const isPresent = parsedUrl.searchParams.has("revision");
    const rawValue = isPresent ? parsedUrl.searchParams.get("revision") : null;
    return {
      isPresent,
      rawValue,
      revisionStateId: normalizeRevisionStateId(rawValue),
    };
  } catch {
    return CURRENT_REVISION_ROUTE_SCOPE;
  }
}
 
/** Read a normalized revision identifier from an absolute or relative URL. */
export function getRevisionStateIdFromUrl(url: string): number | null {
  return getRevisionRouteScopeFromUrl(url).revisionStateId;
}
 
/** Read the active route scope without retaining mutable browser state. */
export function getCurrentRevisionRouteScope(): RevisionRouteScope {
  return typeof document === "undefined"
    ? CURRENT_REVISION_ROUTE_SCOPE
    : getRevisionRouteScopeFromUrl(document.URL);
}
 
/** Resolve the stable flowsheet identity only on a flowsheet project route. */
export function getFlowsheetIdFromUrl(url: string): number | null {
  try {
    const pathname = new URL(url, "http://localhost").pathname;
    const match = pathname.match(/^\/project\/(\d+)(?:\/|$)/);
    return match ? normalizeRevisionStateId(match[1]) : null;
  } catch {
    return null;
  }
}
 
export function isSafeRequestMethod(method: string): boolean {
  return SAFE_REQUEST_METHODS.has(method.toUpperCase());
}
 
/** Return whether a client request must be stopped before leaving the browser. */
export function isHistoricalRevisionMutation(
  method: string,
  revisionScope: RevisionRouteScope,
): boolean {
  return revisionScope.isPresent && !isSafeRequestMethod(method);
}
 
/** Identify the explicit lifecycle action that restores Current from history. */
export function isRevisionRestoreRequest(
  method: string,
  pathname: string,
): boolean {
  return (
    method.toUpperCase() === "POST" &&
    /^\/api\/core\/flowsheets\/[^/]+\/revisions\/[^/]+\/restore\/?$/.test(
      pathname,
    )
  );
}
 
/** Add the stable flowsheet and immutable read-state scope to a request URL. */
export function scopeFlowsheetRequestUrl({
  url,
  flowsheetId,
  revisionScope,
}: {
  url: URL;
  flowsheetId: number | null;
  revisionScope: RevisionRouteScope;
}): URL {
  const scopedUrl = new URL(url.toString());
  if (flowsheetId !== null && !scopedUrl.searchParams.has("flowsheet")) {
    scopedUrl.searchParams.set("flowsheet", flowsheetId.toString());
  }
  if (flowsheetId !== null && revisionScope.isPresent) {
    scopedUrl.searchParams.set(
      "revision",
      revisionScope.revisionStateId?.toString() ?? revisionScope.rawValue ?? "",
    );
  }
  return scopedUrl;
}
 
/**
 * Move an explicitly supplied flowsheet parameter into the URL before route
 * scope is applied. RTK Query otherwise appends `params` after the base query
 * has injected the active flowsheet, producing two conflicting parameters.
 */
export function canonicalizeFlowsheetQueryParam(
  url: URL,
  params: Record<string, unknown> | undefined,
): { url: URL; params: Record<string, unknown> | undefined } {
  const explicitFlowsheet = params?.flowsheet;
  if (
    explicitFlowsheet === undefined ||
    explicitFlowsheet === null ||
    explicitFlowsheet === ""
  ) {
    return { url, params };
  }
 
  const canonicalUrl = new URL(url.toString());
  canonicalUrl.searchParams.set("flowsheet", String(explicitFlowsheet));
  const remainingParams = { ...params };
  delete remainingParams.flowsheet;
  return { url: canonicalUrl, params: remainingParams };
}
 
export function getRevisionReadIdentity(
  revisionScope: RevisionRouteScope,
): string {
  if (!revisionScope.isPresent) return "current";
  return revisionScope.revisionStateId === null
    ? "revision:invalid"
    : `revision:${revisionScope.revisionStateId}`;
}
 
/** Keep otherwise-identical RTK Query results separate for each read state. */
export function serializeQueryArgsWithRevision(
  query: Parameters<typeof defaultSerializeQueryArgs>[0],
  revisionScope = getCurrentRevisionRouteScope(),
  flowsheetId = typeof document === "undefined"
    ? null
    : getFlowsheetIdFromUrl(document.URL),
): string {
  const defaultKey = defaultSerializeQueryArgs(query);
  return flowsheetId === null || !revisionScope.isPresent
    ? defaultKey
    : `${defaultKey}|readState=${getRevisionReadIdentity(revisionScope)}`;
}