@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/README.md +4 -1
- package/dist/index.js +313 -75
- package/dist/index.js.map +1 -1
- package/dist/lib/control.d.ts +34 -1
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/keepalive.d.ts +53 -8
- package/dist/lib/keepalive.d.ts.map +1 -1
- package/dist/lib/state.d.ts +48 -5
- package/dist/lib/state.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +7 -0
- package/dist/lib/tui/commands.d.ts.map +1 -1
- package/dist/lib/tui/footer.d.ts.map +1 -1
- package/dist/lib/tui/format.d.ts +8 -0
- package/dist/lib/tui/format.d.ts.map +1 -1
- package/lib/control.ts +96 -11
- package/lib/keepalive.ts +364 -89
- package/lib/state.ts +57 -7
- package/lib/tui/commands.tsx +184 -0
- package/lib/tui/footer.tsx +33 -6
- package/lib/tui/format.ts +36 -0
- package/package.json +3 -1
- package/lib/tui/commands.ts +0 -59
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (typeof value.updatedAt !== "number") return undefined
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}.${
|
|
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
|
+
}
|