@vikrant82/opencode-cache-keepalive 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/control.ts CHANGED
@@ -1,37 +1,122 @@
1
+ import { randomUUID } from "node:crypto"
1
2
  import { readFileSync } from "node:fs"
2
3
  import { mkdir, rename, writeFile } from "node:fs/promises"
3
4
  import { dirname } from "node:path"
4
5
  import { controlFilePath } from "./paths"
5
6
 
7
+ /** Smallest accepted runtime ping interval; below this the 15 s scheduler tick dominates. */
8
+ export const MIN_INTERVAL_MS = 60_000
9
+ /** Largest accepted runtime ping interval (sanity bound; no provider cache outlives it). */
10
+ export const MAX_INTERVAL_MS = 24 * 3_600_000
11
+
12
+ /**
13
+ * Per-project runtime overrides written by the TUI and polled by the server plugin.
14
+ * Absent fields mean "use the plugin configuration". Readers ignore unknown fields,
15
+ * so older servers still honour `enabled` and ignore `intervalMs`.
16
+ */
6
17
  export type KeepaliveControl = {
7
18
  version: 1
8
- enabled: boolean
19
+ /** Runtime on/off override. */
20
+ enabled?: boolean
21
+ /** Runtime ping interval override in milliseconds. */
22
+ intervalMs?: number
23
+ /** Epoch ms of the last write; strictly increasing so every write is observed. */
9
24
  updatedAt: number
10
25
  }
11
26
 
27
+ /** Changes to apply to the control file; `intervalMs: null` clears the override. */
28
+ export type ControlPatch = {
29
+ enabled?: boolean
30
+ intervalMs?: number | null
31
+ }
32
+
33
+ export function isValidInterval(ms: unknown): ms is number {
34
+ return (
35
+ typeof ms === "number" &&
36
+ Number.isFinite(ms) &&
37
+ ms >= MIN_INTERVAL_MS &&
38
+ ms <= MAX_INTERVAL_MS
39
+ )
40
+ }
41
+
42
+ /**
43
+ * Read the directory's control file. Returns undefined when it is missing or
44
+ * malformed; an out-of-range interval override is dropped rather than rejecting
45
+ * the whole file. Never throws.
46
+ */
12
47
  export function readControl(directory: string): KeepaliveControl | undefined {
13
48
  try {
14
- const value = JSON.parse(
15
- readFileSync(controlFilePath(directory), "utf8"),
16
- ) as Partial<KeepaliveControl>
17
- if (value.version !== 1 || typeof value.enabled !== "boolean") return undefined
18
- if (typeof value.updatedAt !== "number") return undefined
19
- return value as KeepaliveControl
49
+ const value = JSON.parse(readFileSync(controlFilePath(directory), "utf8")) as Record<
50
+ string,
51
+ unknown
52
+ >
53
+ if (value?.version !== 1 || typeof value.updatedAt !== "number") return undefined
54
+ if (value.enabled !== undefined && typeof value.enabled !== "boolean") return undefined
55
+ return {
56
+ version: 1,
57
+ updatedAt: value.updatedAt,
58
+ ...(typeof value.enabled === "boolean" ? { enabled: value.enabled } : {}),
59
+ ...(isValidInterval(value.intervalMs) ? { intervalMs: value.intervalMs } : {}),
60
+ }
20
61
  } catch {
21
62
  return undefined
22
63
  }
23
64
  }
24
65
 
25
- export async function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl> {
66
+ /**
67
+ * Merge `patch` into the directory's control file and write it atomically. Fields
68
+ * not in the patch keep their current value.
69
+ *
70
+ * @throws RangeError when `patch.intervalMs` is outside
71
+ * [MIN_INTERVAL_MS, MAX_INTERVAL_MS]; filesystem errors propagate.
72
+ */
73
+ export async function updateControl(
74
+ directory: string,
75
+ patch: ControlPatch,
76
+ ): Promise<KeepaliveControl> {
77
+ if (
78
+ patch.intervalMs !== undefined &&
79
+ patch.intervalMs !== null &&
80
+ !isValidInterval(patch.intervalMs)
81
+ ) {
82
+ throw new RangeError(
83
+ `Ping interval must be between ${MIN_INTERVAL_MS / 1000}s and ${MAX_INTERVAL_MS / 3_600_000}h`,
84
+ )
85
+ }
86
+
87
+ // Serialize read-merge-write within this process so concurrent updates cannot
88
+ // drop each other's fields. Across processes the atomic rename keeps the file
89
+ // valid; the last writer wins.
90
+ const run = updateQueue.then(() => writeMerged(directory, patch))
91
+ updateQueue = run.then(
92
+ () => undefined,
93
+ () => undefined,
94
+ )
95
+ return run
96
+ }
97
+
98
+ let updateQueue: Promise<void> = Promise.resolve()
99
+
100
+ async function writeMerged(directory: string, patch: ControlPatch): Promise<KeepaliveControl> {
101
+ const current = readControl(directory)
102
+ const enabled = patch.enabled ?? current?.enabled
103
+ const intervalMs =
104
+ patch.intervalMs === null ? undefined : (patch.intervalMs ?? current?.intervalMs)
26
105
  const value: KeepaliveControl = {
27
106
  version: 1,
28
- enabled,
29
- updatedAt: Date.now(),
107
+ ...(enabled !== undefined ? { enabled } : {}),
108
+ ...(intervalMs !== undefined ? { intervalMs } : {}),
109
+ updatedAt: Math.max(Date.now(), (current?.updatedAt ?? 0) + 1),
30
110
  }
31
111
  const path = controlFilePath(directory)
32
- const tmp = `${path}.${process.pid}.${value.updatedAt}.tmp`
112
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`
33
113
  await mkdir(dirname(path), { recursive: true })
34
114
  await writeFile(tmp, `${JSON.stringify(value)}\n`, "utf8")
35
115
  await rename(tmp, path)
36
116
  return value
37
117
  }
118
+
119
+ /** Set the runtime on/off override, preserving any interval override. */
120
+ export function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl> {
121
+ return updateControl(directory, { enabled })
122
+ }