dsh-server-connector 0.2.3 → 0.3.0

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/config.js ADDED
@@ -0,0 +1,89 @@
1
+ // dsh-server-connector — the global configuration (doc §10), as live data.
2
+ //
3
+ // Why this unit exists
4
+ // --------------------
5
+ // Every key of the plugin's `Config` is `.volatile()` (see `lib/index.js`), and
6
+ // that is what makes the settings page editable at all: DSH projects a plugin
7
+ // row into a settings form only through `volatileForm()`, which keeps a field
8
+ // only when it is marked volatile (`dsh-settings/lib/index.js`). Without a
9
+ // single volatile field the whole schema projects to `undefined`, the namespace
10
+ // never appears in `settings.describe()`, and `ctx.configForms.get(<row id>)`
11
+ // stays `unavailable` — which is exactly why this plugin's settings page used to
12
+ // render nine read-only numbers.
13
+ //
14
+ // A volatile field does not resolve to its value: schemastery wraps it with
15
+ // cosmokit's `createVolatile(default)`, the loader writes an edit **into that
16
+ // same cell** and emits `loader/volatile-update` instead of remounting the
17
+ // plugin. So the config object handed to `apply()` is a live view, and every
18
+ // consumer has to read through {@link readConfig} at the moment it uses a value.
19
+ // Snapshotting it at construction time is the bug this unit exists to prevent.
20
+
21
+ /** The documented §10 defaults, in one place. */
22
+ export const CONFIG_DEFAULTS = Object.freeze({
23
+ connectTimeoutMs: 15000,
24
+ commandTimeoutMs: 60000,
25
+ syncTimeoutMs: 20000,
26
+ maxOutputChars: 200000,
27
+ idleTimeoutMs: 600000,
28
+ auditEnabled: true,
29
+ auditMaxBytes: 0,
30
+ pendingTtlHours: 24,
31
+ maxListedServers: 8,
32
+ })
33
+
34
+ /** The nine key names, in schema order; the settings form and the tests read this. */
35
+ export const CONFIG_KEYS = /** @type {Array<keyof import('./index.js').PluginConfig>} */ (Object.keys(CONFIG_DEFAULTS))
36
+
37
+ /**
38
+ * Unwrap one configuration value.
39
+ *
40
+ * A `.volatile()` schema field resolves to a cosmokit cell (`{get()}`) rather
41
+ * than to its value, and the loader mutates that cell in place when the user
42
+ * edits the field. A plain value — a test's hand-built settings object, or an
43
+ * older host that resolved the schema itself — passes through untouched, so
44
+ * every caller may read through here unconditionally.
45
+ * @param {unknown} value
46
+ * @returns {unknown}
47
+ */
48
+ export function unbox(value) {
49
+ if (value === null || typeof value !== 'object') return value
50
+ const candidate = /** @type {{get?: unknown}} */ (value)
51
+ if (typeof candidate.get !== 'function') return value
52
+ try {
53
+ return /** @type {{get: () => unknown}} */ (candidate).get()
54
+ } catch {
55
+ return undefined
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Read the nine §10 values as plain data.
61
+ *
62
+ * The result is a *snapshot*: never cache it across turns. Call this where the
63
+ * value is used, so a live edit takes effect without a remount.
64
+ *
65
+ * The parameter is deliberately loose (`Record<string, unknown>`): the object a
66
+ * `Config` instance produces holds volatile *cells*, not values, so a stricter
67
+ * signature would make every caller cast.
68
+ * @param {Record<string, unknown>|null|undefined} config - the row config (volatile cells or plain values).
69
+ * @returns {import('./index.js').PluginConfig}
70
+ */
71
+ export function readConfig(config) {
72
+ const source = config ?? {}
73
+ /** @param {keyof import('./index.js').PluginConfig} key */
74
+ const pick = (key) => {
75
+ const value = unbox(source[key])
76
+ return value === undefined || value === null ? CONFIG_DEFAULTS[key] : value
77
+ }
78
+ return {
79
+ connectTimeoutMs: /** @type {number} */ (pick('connectTimeoutMs')),
80
+ commandTimeoutMs: /** @type {number} */ (pick('commandTimeoutMs')),
81
+ syncTimeoutMs: /** @type {number} */ (pick('syncTimeoutMs')),
82
+ maxOutputChars: /** @type {number} */ (pick('maxOutputChars')),
83
+ idleTimeoutMs: /** @type {number} */ (pick('idleTimeoutMs')),
84
+ auditEnabled: /** @type {boolean} */ (pick('auditEnabled')),
85
+ auditMaxBytes: /** @type {number} */ (pick('auditMaxBytes')),
86
+ pendingTtlHours: /** @type {number} */ (pick('pendingTtlHours')),
87
+ maxListedServers: /** @type {number} */ (pick('maxListedServers')),
88
+ }
89
+ }