File size: 3,202 Bytes
6c3af4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b4ae63
6c3af4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b4ae63
6c3af4e
 
4b4ae63
 
 
 
6c3af4e
 
 
4b4ae63
6c3af4e
 
 
 
 
 
 
4b4ae63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Runtime-resolved server configuration.
 *
 * Fetches /api/config on first call so the published Docker image is
 * fully configurable via environment variables at runtime instead of
 * being locked to build-time defaults.
 */

import {
  DEFAULT_INFERENCE_TYPE,
  DEFAULT_INTERNAL_API_NAME,
  DEFAULT_WLLAMA_MODEL_ID,
  type ServerConfig,
} from "@shared/serverConfig";

export type { ServerConfig };

/**
 * Config to assume only where running without the server's values is harmless.
 * It is deliberately not used for `accessKeysEnabled`: treating an unreachable
 * server as "access keys are off" would skip the access key page entirely.
 *
 * `searchToken` has no harmless stand-in, so it is empty here. Anything that
 * needs it goes through `getConfig`, which rejects rather than handing back a
 * token the server would turn away.
 */
export const FALLBACK_CONFIG: ServerConfig = {
  accessKeysEnabled: false,
  accessKeyTimeoutHours: 0,
  wllamaDefaultModelId: DEFAULT_WLLAMA_MODEL_ID,
  internalApiEnabled: false,
  internalApiName: DEFAULT_INTERNAL_API_NAME,
  defaultInferenceType: DEFAULT_INFERENCE_TYPE,
  searchToken: "",
};

const FETCH_TIMEOUT_MS = 5000;

let cachedConfig: ServerConfig | null = null;
let pendingFetch: Promise<ServerConfig> | null = null;
let configGeneration = 0;

async function fetchConfig(): Promise<ServerConfig> {
  const response = await fetch("/api/config", {
    signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
  });

  if (!response.ok) {
    throw new Error(`/api/config responded with status ${response.status}`);
  }

  return (await response.json()) as ServerConfig;
}

/**
 * Returns the runtime server config, fetching /api/config on the first call and
 * caching a successful response.
 *
 * Rejects when the config cannot be retrieved, leaving each caller to decide
 * whether continuing without it is safe. A failed attempt is not cached, so a
 * transient blip does not lock the session out of its real configuration.
 */
export async function getConfig(): Promise<ServerConfig> {
  if (cachedConfig) return cachedConfig;

  if (!pendingFetch) {
    const generation = configGeneration;
    pendingFetch = fetchConfig().then(
      (config) => {
        if (generation === configGeneration) {
          cachedConfig = config;
          pendingFetch = null;
        }
        return config;
      },
      (error) => {
        if (generation === configGeneration) pendingFetch = null;
        throw error;
      },
    );
  }

  return pendingFetch;
}

/**
 * Drops the cached config so the next `getConfig` call goes back to
 * /api/config.
 *
 * Each server process mints its own search token, so a restart leaves an open
 * page holding a token the server never issued. The 401 handler in
 * `search.ts` calls this before asking for the token again, which is the only
 * way a page that is already loaded can find out the token rotated.
 *
 * A fetch that is already in flight was started against the values this call
 * discards, so it is left to resolve its own callers without repopulating the
 * cache behind them.
 */
export function invalidateConfig(): void {
  cachedConfig = null;
  pendingFetch = null;
  configGeneration += 1;
}