@stdray-npm/petbox-client 0.1.0-ci.199
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/dist/client.d.ts +60 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +187 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +120 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +36 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
- package/src/client.ts +210 -0
- package/src/config.ts +114 -0
- package/src/index.ts +10 -0
- package/src/types.ts +80 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ResolvedConfig } from "./config.js";
|
|
2
|
+
import { TypedEmitter, type PetBoxConfigClientEvents, type PetBoxConfigClientOptions } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* TypeScript SDK client for PetBox config.
|
|
5
|
+
*
|
|
6
|
+
* Fetches resolved config from /v1/conf with ETag-aware polling. Supports all four
|
|
7
|
+
* response templates (flat, dotnet, envvar, envvar-deep).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const client = new PetBoxConfigClient({
|
|
12
|
+
* endpoint: 'https://petbox.3po.su',
|
|
13
|
+
* apiKey: process.env.PETBOX_API_KEY!,
|
|
14
|
+
* tags: { env: 'prod', project: 'kpvotes' },
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const config = await client.start();
|
|
18
|
+
* console.log(config.get('db.host'));
|
|
19
|
+
*
|
|
20
|
+
* client.on('change', (cfg) => console.log('config updated', cfg.data));
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class PetBoxConfigClient extends TypedEmitter<PetBoxConfigClientEvents> {
|
|
24
|
+
private readonly options;
|
|
25
|
+
private readonly fetchImpl;
|
|
26
|
+
private currentConfig;
|
|
27
|
+
private etag;
|
|
28
|
+
private timer;
|
|
29
|
+
private disposed;
|
|
30
|
+
constructor(options: PetBoxConfigClientOptions);
|
|
31
|
+
/** The most recently fetched config, or null if never fetched. */
|
|
32
|
+
get current(): ResolvedConfig | null;
|
|
33
|
+
/**
|
|
34
|
+
* One-shot fetch. Does NOT start background polling.
|
|
35
|
+
* Throws on auth errors, network errors, and 409 conflicts (unless optional=true).
|
|
36
|
+
*/
|
|
37
|
+
fetch(): Promise<ResolvedConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* Initial fetch + start background ETag polling. Returns the first resolved config.
|
|
40
|
+
* Fires 'change' events on subsequent updates, 'error' on polling failures.
|
|
41
|
+
*/
|
|
42
|
+
start(): Promise<ResolvedConfig>;
|
|
43
|
+
/** Stop background polling. Does NOT clear the last config. */
|
|
44
|
+
stop(): void;
|
|
45
|
+
/** Stop polling and remove all listeners. */
|
|
46
|
+
dispose(): void;
|
|
47
|
+
private ensureNotDisposed;
|
|
48
|
+
private buildUrl;
|
|
49
|
+
private fetchOnce;
|
|
50
|
+
private startPolling;
|
|
51
|
+
private poll;
|
|
52
|
+
private stripEtag;
|
|
53
|
+
private errorMessage;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Convenience: one-shot fetch without creating a client instance.
|
|
57
|
+
* Same as `new PetBoxConfigClient(opts).fetch()`.
|
|
58
|
+
*/
|
|
59
|
+
export declare const fetchConfig: (options: PetBoxConfigClientOptions) => Promise<ResolvedConfig>;
|
|
60
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAGN,YAAY,EACZ,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAE9B,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,kBAAmB,SAAQ,YAAY,CAAC,wBAAwB,CAAC;IAC7E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAQtB;IACF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAS;gBAEb,OAAO,EAAE,yBAAyB;IAkB9C,kEAAkE;IAClE,IAAI,OAAO,IAAI,cAAc,GAAG,IAAI,CAEnC;IAED;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,cAAc,CAAC;IAatC;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,cAAc,CAAC;IAMtC,+DAA+D;IAC/D,IAAI,IAAI,IAAI;IAOZ,6CAA6C;IAC7C,OAAO,IAAI,IAAI;IAOf,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,QAAQ;YAUF,SAAS;IAqCvB,OAAO,CAAC,YAAY;YAUN,IAAI;IAalB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,YAAY;CAQpB;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,YAAa,yBAAyB,KAAG,OAAO,CAAC,cAAc,CAC/C,CAAC"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ResolvedConfig } from "./config.js";
|
|
2
|
+
import { TypedEmitter, PetBoxConfigError, } from "./types.js";
|
|
3
|
+
const DEFAULT_REFRESH_MS = 5 * 60 * 1000;
|
|
4
|
+
/**
|
|
5
|
+
* TypeScript SDK client for PetBox config.
|
|
6
|
+
*
|
|
7
|
+
* Fetches resolved config from /v1/conf with ETag-aware polling. Supports all four
|
|
8
|
+
* response templates (flat, dotnet, envvar, envvar-deep).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const client = new PetBoxConfigClient({
|
|
13
|
+
* endpoint: 'https://petbox.3po.su',
|
|
14
|
+
* apiKey: process.env.PETBOX_API_KEY!,
|
|
15
|
+
* tags: { env: 'prod', project: 'kpvotes' },
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const config = await client.start();
|
|
19
|
+
* console.log(config.get('db.host'));
|
|
20
|
+
*
|
|
21
|
+
* client.on('change', (cfg) => console.log('config updated', cfg.data));
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class PetBoxConfigClient extends TypedEmitter {
|
|
25
|
+
options;
|
|
26
|
+
fetchImpl;
|
|
27
|
+
currentConfig = null;
|
|
28
|
+
etag = null;
|
|
29
|
+
timer = null;
|
|
30
|
+
disposed = false;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
super();
|
|
33
|
+
if (!options.endpoint)
|
|
34
|
+
throw new TypeError("endpoint is required");
|
|
35
|
+
if (!options.apiKey)
|
|
36
|
+
throw new TypeError("apiKey is required");
|
|
37
|
+
if (!options.tags || Object.keys(options.tags).length === 0)
|
|
38
|
+
throw new TypeError("at least one tag is required");
|
|
39
|
+
this.options = {
|
|
40
|
+
endpoint: options.endpoint,
|
|
41
|
+
apiKey: options.apiKey,
|
|
42
|
+
tags: options.tags,
|
|
43
|
+
template: options.template ?? "flat",
|
|
44
|
+
refreshIntervalMs: options.refreshIntervalMs ?? DEFAULT_REFRESH_MS,
|
|
45
|
+
optional: options.optional ?? false,
|
|
46
|
+
fetchImpl: options.fetchImpl,
|
|
47
|
+
};
|
|
48
|
+
this.fetchImpl = options.fetchImpl ?? ((...args) => globalThis.fetch(...args));
|
|
49
|
+
}
|
|
50
|
+
/** The most recently fetched config, or null if never fetched. */
|
|
51
|
+
get current() {
|
|
52
|
+
return this.currentConfig;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One-shot fetch. Does NOT start background polling.
|
|
56
|
+
* Throws on auth errors, network errors, and 409 conflicts (unless optional=true).
|
|
57
|
+
*/
|
|
58
|
+
async fetch() {
|
|
59
|
+
this.ensureNotDisposed();
|
|
60
|
+
try {
|
|
61
|
+
const cfg = await this.fetchOnce();
|
|
62
|
+
this.currentConfig = cfg;
|
|
63
|
+
this.etag = cfg.etag;
|
|
64
|
+
return cfg;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (this.options.optional)
|
|
68
|
+
return new ResolvedConfig({}, null);
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Initial fetch + start background ETag polling. Returns the first resolved config.
|
|
74
|
+
* Fires 'change' events on subsequent updates, 'error' on polling failures.
|
|
75
|
+
*/
|
|
76
|
+
async start() {
|
|
77
|
+
const cfg = await this.fetch();
|
|
78
|
+
this.startPolling();
|
|
79
|
+
return cfg;
|
|
80
|
+
}
|
|
81
|
+
/** Stop background polling. Does NOT clear the last config. */
|
|
82
|
+
stop() {
|
|
83
|
+
if (this.timer !== null) {
|
|
84
|
+
clearInterval(this.timer);
|
|
85
|
+
this.timer = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Stop polling and remove all listeners. */
|
|
89
|
+
dispose() {
|
|
90
|
+
this.disposed = true;
|
|
91
|
+
this.stop();
|
|
92
|
+
}
|
|
93
|
+
// ── internals ──────────────────────────────────────────
|
|
94
|
+
ensureNotDisposed() {
|
|
95
|
+
if (this.disposed)
|
|
96
|
+
throw new Error("PetBoxConfigClient is disposed");
|
|
97
|
+
}
|
|
98
|
+
buildUrl() {
|
|
99
|
+
const base = this.options.endpoint.endsWith("/") ? this.options.endpoint : `${this.options.endpoint}/`;
|
|
100
|
+
const params = new URLSearchParams();
|
|
101
|
+
// Sort for stable URLs (helps caching / log grepping).
|
|
102
|
+
const sortedKeys = Object.keys(this.options.tags).sort();
|
|
103
|
+
for (const k of sortedKeys)
|
|
104
|
+
params.set(k, this.options.tags[k] ?? "");
|
|
105
|
+
if (this.options.template !== "flat")
|
|
106
|
+
params.set("template", this.options.template);
|
|
107
|
+
return `${base}v1/conf?${params.toString()}`;
|
|
108
|
+
}
|
|
109
|
+
async fetchOnce() {
|
|
110
|
+
const url = this.buildUrl();
|
|
111
|
+
const headers = {
|
|
112
|
+
"X-YobaConf-ApiKey": this.options.apiKey,
|
|
113
|
+
};
|
|
114
|
+
if (this.etag !== null)
|
|
115
|
+
headers["If-None-Match"] = `"${this.etag}"`;
|
|
116
|
+
let response;
|
|
117
|
+
try {
|
|
118
|
+
response = await this.fetchImpl(url, { headers });
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
throw new PetBoxConfigError(`Failed to reach PetBox at ${url}: ${String(cause)}`, 0, null);
|
|
122
|
+
}
|
|
123
|
+
// 304 — unchanged. Return last-known-good config.
|
|
124
|
+
if (response.status === 304) {
|
|
125
|
+
const newEtag = this.stripEtag(response.headers.get("ETag"));
|
|
126
|
+
return new ResolvedConfig(this.currentConfig?.data ?? {}, newEtag ?? this.etag);
|
|
127
|
+
}
|
|
128
|
+
const body = await response.text();
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
let parsed = null;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(body);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
/* not JSON */
|
|
136
|
+
}
|
|
137
|
+
throw new PetBoxConfigError(this.errorMessage(response.status, parsed), response.status, parsed);
|
|
138
|
+
}
|
|
139
|
+
const etag = this.stripEtag(response.headers.get("ETag"));
|
|
140
|
+
const data = JSON.parse(body);
|
|
141
|
+
return new ResolvedConfig(data, etag);
|
|
142
|
+
}
|
|
143
|
+
startPolling() {
|
|
144
|
+
if (this.options.refreshIntervalMs <= 0)
|
|
145
|
+
return;
|
|
146
|
+
if (this.timer !== null)
|
|
147
|
+
return;
|
|
148
|
+
this.timer = setInterval(() => {
|
|
149
|
+
this.poll().catch(() => {
|
|
150
|
+
/* error emitted inside poll */
|
|
151
|
+
});
|
|
152
|
+
}, this.options.refreshIntervalMs);
|
|
153
|
+
}
|
|
154
|
+
async poll() {
|
|
155
|
+
try {
|
|
156
|
+
const cfg = await this.fetchOnce();
|
|
157
|
+
if (cfg.etag !== this.etag) {
|
|
158
|
+
this.currentConfig = cfg;
|
|
159
|
+
this.etag = cfg.etag;
|
|
160
|
+
this.emit("change", cfg);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
this.emit("error", err);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
stripEtag(raw) {
|
|
168
|
+
if (raw === null)
|
|
169
|
+
return null;
|
|
170
|
+
// Server sends `"<hex>"` — strip quotes.
|
|
171
|
+
return raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
|
|
172
|
+
}
|
|
173
|
+
errorMessage(status, body) {
|
|
174
|
+
if (body !== null && typeof body === "object" && "error" in body) {
|
|
175
|
+
const b = body;
|
|
176
|
+
const reason = typeof b["reason"] === "string" ? `: ${b["reason"]}` : "";
|
|
177
|
+
return `${b["error"]}${reason}`;
|
|
178
|
+
}
|
|
179
|
+
return `HTTP ${status}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Convenience: one-shot fetch without creating a client instance.
|
|
184
|
+
* Same as `new PetBoxConfigClient(opts).fetch()`.
|
|
185
|
+
*/
|
|
186
|
+
export const fetchConfig = (options) => new PetBoxConfigClient(options).fetch();
|
|
187
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable resolved config from a PetBox fetch.
|
|
3
|
+
*
|
|
4
|
+
* For "flat" template: data is a nested JSON tree; `get("db.host")` traverses it.
|
|
5
|
+
* For "dotnet" / "envvar" / "envvar-deep": data is a flat `Record<string, string>`;
|
|
6
|
+
* `get("DB_HOST")` does a direct key lookup.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ResolvedConfig {
|
|
9
|
+
readonly etag: string | null;
|
|
10
|
+
private readonly raw;
|
|
11
|
+
constructor(raw: unknown, etag: string | null);
|
|
12
|
+
/** The raw parsed JSON response body. */
|
|
13
|
+
get data(): unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Returns the string value at the given dotted path (flat template) or direct key (other templates).
|
|
16
|
+
* Returns undefined if the path doesn't exist.
|
|
17
|
+
*/
|
|
18
|
+
get(path: string): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the numeric value at the given path, or undefined if missing / not a number.
|
|
21
|
+
* Accepts JSON numbers and numeric strings.
|
|
22
|
+
*/
|
|
23
|
+
getNumber(path: string): number | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the boolean value at the given path, or undefined if missing.
|
|
26
|
+
* Accepts JSON booleans and the strings "true"/"false" (case-insensitive).
|
|
27
|
+
*/
|
|
28
|
+
getBoolean(path: string): boolean | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the JSON value at the given path (object, array, scalar — whatever the server returned).
|
|
31
|
+
* Returns undefined if the path doesn't exist.
|
|
32
|
+
*/
|
|
33
|
+
getJson<T = unknown>(path: string): T | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Flattens the config into a `Record<string, string>` suitable for process env export.
|
|
36
|
+
* For flat template: expands nested objects to dotted keys. For other templates: returns as-is.
|
|
37
|
+
*/
|
|
38
|
+
toEnv(): Record<string, string>;
|
|
39
|
+
private resolve;
|
|
40
|
+
private flatten;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,qBAAa,cAAc;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAU;gBAElB,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAK7C,yCAAyC;IACzC,IAAI,IAAI,IAAI,OAAO,CAElB;IAED;;;OAGG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAOrC;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAW3C;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAY7C;;;OAGG;IACH,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAIjD;;;OAGG;IACH,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAQ/B,OAAO,CAAC,OAAO;IAoBf,OAAO,CAAC,OAAO;CAUf"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable resolved config from a PetBox fetch.
|
|
3
|
+
*
|
|
4
|
+
* For "flat" template: data is a nested JSON tree; `get("db.host")` traverses it.
|
|
5
|
+
* For "dotnet" / "envvar" / "envvar-deep": data is a flat `Record<string, string>`;
|
|
6
|
+
* `get("DB_HOST")` does a direct key lookup.
|
|
7
|
+
*/
|
|
8
|
+
export class ResolvedConfig {
|
|
9
|
+
etag;
|
|
10
|
+
raw;
|
|
11
|
+
constructor(raw, etag) {
|
|
12
|
+
this.raw = raw;
|
|
13
|
+
this.etag = etag;
|
|
14
|
+
}
|
|
15
|
+
/** The raw parsed JSON response body. */
|
|
16
|
+
get data() {
|
|
17
|
+
return this.raw;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns the string value at the given dotted path (flat template) or direct key (other templates).
|
|
21
|
+
* Returns undefined if the path doesn't exist.
|
|
22
|
+
*/
|
|
23
|
+
get(path) {
|
|
24
|
+
const v = this.resolve(path);
|
|
25
|
+
if (v === undefined || v === null)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof v === "string")
|
|
28
|
+
return v;
|
|
29
|
+
return String(v);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Returns the numeric value at the given path, or undefined if missing / not a number.
|
|
33
|
+
* Accepts JSON numbers and numeric strings.
|
|
34
|
+
*/
|
|
35
|
+
getNumber(path) {
|
|
36
|
+
const v = this.resolve(path);
|
|
37
|
+
if (v === undefined || v === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (typeof v === "number")
|
|
40
|
+
return Number.isFinite(v) ? v : undefined;
|
|
41
|
+
if (typeof v === "string") {
|
|
42
|
+
const n = Number(v);
|
|
43
|
+
return Number.isFinite(n) ? n : undefined;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns the boolean value at the given path, or undefined if missing.
|
|
49
|
+
* Accepts JSON booleans and the strings "true"/"false" (case-insensitive).
|
|
50
|
+
*/
|
|
51
|
+
getBoolean(path) {
|
|
52
|
+
const v = this.resolve(path);
|
|
53
|
+
if (v === undefined || v === null)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (typeof v === "boolean")
|
|
56
|
+
return v;
|
|
57
|
+
if (typeof v === "string") {
|
|
58
|
+
const lower = v.toLowerCase();
|
|
59
|
+
if (lower === "true")
|
|
60
|
+
return true;
|
|
61
|
+
if (lower === "false")
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns the JSON value at the given path (object, array, scalar — whatever the server returned).
|
|
68
|
+
* Returns undefined if the path doesn't exist.
|
|
69
|
+
*/
|
|
70
|
+
getJson(path) {
|
|
71
|
+
return this.resolve(path);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Flattens the config into a `Record<string, string>` suitable for process env export.
|
|
75
|
+
* For flat template: expands nested objects to dotted keys. For other templates: returns as-is.
|
|
76
|
+
*/
|
|
77
|
+
toEnv() {
|
|
78
|
+
if (this.raw === null || this.raw === undefined)
|
|
79
|
+
return {};
|
|
80
|
+
if (typeof this.raw !== "object")
|
|
81
|
+
return {};
|
|
82
|
+
const out = {};
|
|
83
|
+
this.flatten(this.raw, "", out);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
resolve(path) {
|
|
87
|
+
if (this.raw === null || this.raw === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (typeof this.raw !== "object")
|
|
90
|
+
return undefined;
|
|
91
|
+
// If the raw data is a flat dictionary (dotnet/envvar/envvar-deep templates),
|
|
92
|
+
// the keys don't contain dots — do a direct lookup.
|
|
93
|
+
const obj = this.raw;
|
|
94
|
+
if (!path.includes("."))
|
|
95
|
+
return obj[path];
|
|
96
|
+
// Dotted path — traverse nested object (flat template).
|
|
97
|
+
const segments = path.split(".");
|
|
98
|
+
let current = obj;
|
|
99
|
+
for (const seg of segments) {
|
|
100
|
+
if (current === null || current === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (typeof current !== "object")
|
|
103
|
+
return undefined;
|
|
104
|
+
current = current[seg];
|
|
105
|
+
}
|
|
106
|
+
return current;
|
|
107
|
+
}
|
|
108
|
+
flatten(obj, prefix, out) {
|
|
109
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
110
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
111
|
+
if (v !== null && v !== undefined && typeof v === "object" && !Array.isArray(v)) {
|
|
112
|
+
this.flatten(v, key, out);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
out[key] = v === null || v === undefined ? "" : String(v);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { PetBoxConfigClient, fetchConfig } from "./client.js";
|
|
2
|
+
export { ResolvedConfig } from "./config.js";
|
|
3
|
+
export { type TagVector, type Template, type PetBoxConfigClientOptions, type PetBoxConfigClientEvents, PetBoxConfigError, TypedEmitter, } from "./types.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACN,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,iBAAiB,EACjB,YAAY,GACZ,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAKN,iBAAiB,EACjB,YAAY,GACZ,MAAM,YAAY,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Tag-vector — flat key=value pairs sent as query params to /v1/conf. */
|
|
2
|
+
export type TagVector = Readonly<Record<string, string>>;
|
|
3
|
+
/** Response-shape templates (spec §9.1). */
|
|
4
|
+
export type Template = "flat" | "dotnet" | "envvar" | "envvar-deep";
|
|
5
|
+
/** Options for PetBoxConfigClient. */
|
|
6
|
+
export interface PetBoxConfigClientOptions {
|
|
7
|
+
/** Base URL of the PetBox server, e.g. "https://petbox.3po.su". Trailing slash optional. */
|
|
8
|
+
readonly endpoint: string;
|
|
9
|
+
/** Plaintext API key. Sent as X-YobaConf-ApiKey header on every request. */
|
|
10
|
+
readonly apiKey: string;
|
|
11
|
+
/** Tag-vector — every tag the request carries. Resolve finds bindings whose tag-set is a subset. */
|
|
12
|
+
readonly tags: TagVector;
|
|
13
|
+
/** Response template (default: "flat"). Controls the shape of the JSON response. */
|
|
14
|
+
readonly template?: Template;
|
|
15
|
+
/**
|
|
16
|
+
* Polling interval in ms. Each poll uses If-None-Match for cheap 304s.
|
|
17
|
+
* Set to 0 to disable polling (one-shot fetch only). Default: 5 minutes.
|
|
18
|
+
*/
|
|
19
|
+
readonly refreshIntervalMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* When true, initial fetch failures (network, auth, 409) don't throw — the client
|
|
22
|
+
* starts with null config and retries on the next poll. Default: false.
|
|
23
|
+
*/
|
|
24
|
+
readonly optional?: boolean;
|
|
25
|
+
/** Custom fetch implementation (for testing / proxy injection). */
|
|
26
|
+
readonly fetchImpl?: typeof fetch;
|
|
27
|
+
}
|
|
28
|
+
/** Structured error from the PetBox config API. */
|
|
29
|
+
export declare class PetBoxConfigError extends Error {
|
|
30
|
+
readonly status: number;
|
|
31
|
+
readonly body: unknown;
|
|
32
|
+
constructor(message: string, status: number, body: unknown);
|
|
33
|
+
}
|
|
34
|
+
/** Events emitted by PetBoxConfigClient. */
|
|
35
|
+
export interface PetBoxConfigClientEvents {
|
|
36
|
+
/** Fired when config changes (200 response with new data). */
|
|
37
|
+
change: (config: import("./config.js").ResolvedConfig) => void;
|
|
38
|
+
/** Fired on fetch errors during polling. Initial-fetch errors throw (unless optional). */
|
|
39
|
+
error: (err: unknown) => void;
|
|
40
|
+
}
|
|
41
|
+
/** Minimal typed event emitter — avoids node:events dependency for portability. */
|
|
42
|
+
export declare class TypedEmitter<Events extends {
|
|
43
|
+
[K in keyof Events]: (...args: never[]) => void;
|
|
44
|
+
}> {
|
|
45
|
+
private readonly listeners;
|
|
46
|
+
on<E extends keyof Events>(event: E, listener: Events[E]): this;
|
|
47
|
+
off<E extends keyof Events>(event: E, listener: Events[E]): this;
|
|
48
|
+
protected emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): void;
|
|
49
|
+
protected listenerCount(event: keyof Events): number;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEzD,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEpE,sCAAsC;AACtC,MAAM,WAAW,yBAAyB;IACzC,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oGAAoG;IACpG,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC;AAED,mDAAmD;AACnD,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;gBAEX,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;CAM1D;AAED,4CAA4C;AAC5C,MAAM,WAAW,wBAAwB;IACxC,8DAA8D;IAC9D,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,aAAa,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/D,0FAA0F;IAC1F,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9B;AAID,mFAAmF;AACnF,qBAAa,YAAY,CAAC,MAAM,SAAS;KAAG,CAAC,IAAI,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI;CAAE;IAC3F,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI;IAU/D,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI;IAKhE,SAAS,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAItF,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,MAAM,GAAG,MAAM;CAGpD"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Structured error from the PetBox config API. */
|
|
2
|
+
export class PetBoxConfigError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "PetBoxConfigError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Minimal typed event emitter — avoids node:events dependency for portability. */
|
|
13
|
+
export class TypedEmitter {
|
|
14
|
+
listeners = new Map();
|
|
15
|
+
on(event, listener) {
|
|
16
|
+
let set = this.listeners.get(event);
|
|
17
|
+
if (!set) {
|
|
18
|
+
set = new Set();
|
|
19
|
+
this.listeners.set(event, set);
|
|
20
|
+
}
|
|
21
|
+
set.add(listener);
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
off(event, listener) {
|
|
25
|
+
this.listeners.get(event)?.delete(listener);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
emit(event, ...args) {
|
|
29
|
+
for (const fn of this.listeners.get(event) ?? [])
|
|
30
|
+
fn(...args);
|
|
31
|
+
}
|
|
32
|
+
listenerCount(event) {
|
|
33
|
+
return this.listeners.get(event)?.size ?? 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA8BA,mDAAmD;AACnD,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAClC,MAAM,CAAS;IACf,IAAI,CAAU;IAEvB,YAAY,OAAe,EAAE,MAAc,EAAE,IAAa;QACzD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD;AAYD,mFAAmF;AACnF,MAAM,OAAO,YAAY;IACP,SAAS,GAAG,IAAI,GAAG,EAA+B,CAAC;IAEpE,EAAE,CAAyB,KAAQ,EAAE,QAAmB;QACvD,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,CAAC;YACV,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAChC,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,QAAoB,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,GAAG,CAAyB,KAAQ,EAAE,QAAmB;QACxD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,QAAoB,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACb,CAAC;IAES,IAAI,CAAyB,KAAQ,EAAE,GAAG,IAA2B;QAC9E,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE;YAAG,EAA4C,CAAC,GAAG,IAAI,CAAC,CAAC;IAC1G,CAAC;IAES,aAAa,CAAC,KAAmB;QAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC;IAC7C,CAAC;CACD"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stdray-npm/petbox-client",
|
|
3
|
+
"version": "0.1.0-ci.199",
|
|
4
|
+
"description": "TypeScript SDK for PetBox — config client with ETag-aware polling. Data + Log surfaces land in 26.3.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "bun test",
|
|
23
|
+
"lint": "biome check .",
|
|
24
|
+
"lint:fix": "biome check --write ."
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@biomejs/biome": "1.9.4",
|
|
28
|
+
"typescript": "5.7.3"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"petbox",
|
|
32
|
+
"configuration",
|
|
33
|
+
"config"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/stdray/petbox.git",
|
|
40
|
+
"directory": "src/clients-ts/petbox-client"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"registry": "https://registry.npmjs.org",
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ResolvedConfig } from "./config.js";
|
|
2
|
+
import {
|
|
3
|
+
type TagVector,
|
|
4
|
+
type Template,
|
|
5
|
+
TypedEmitter,
|
|
6
|
+
type PetBoxConfigClientEvents,
|
|
7
|
+
type PetBoxConfigClientOptions,
|
|
8
|
+
PetBoxConfigError,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_REFRESH_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* TypeScript SDK client for PetBox config.
|
|
15
|
+
*
|
|
16
|
+
* Fetches resolved config from /v1/conf with ETag-aware polling. Supports all four
|
|
17
|
+
* response templates (flat, dotnet, envvar, envvar-deep).
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const client = new PetBoxConfigClient({
|
|
22
|
+
* endpoint: 'https://petbox.3po.su',
|
|
23
|
+
* apiKey: process.env.PETBOX_API_KEY!,
|
|
24
|
+
* tags: { env: 'prod', project: 'kpvotes' },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* const config = await client.start();
|
|
28
|
+
* console.log(config.get('db.host'));
|
|
29
|
+
*
|
|
30
|
+
* client.on('change', (cfg) => console.log('config updated', cfg.data));
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class PetBoxConfigClient extends TypedEmitter<PetBoxConfigClientEvents> {
|
|
34
|
+
private readonly options: {
|
|
35
|
+
readonly endpoint: string;
|
|
36
|
+
readonly apiKey: string;
|
|
37
|
+
readonly tags: TagVector;
|
|
38
|
+
readonly template: Template;
|
|
39
|
+
readonly refreshIntervalMs: number;
|
|
40
|
+
readonly optional: boolean;
|
|
41
|
+
readonly fetchImpl: typeof fetch | undefined;
|
|
42
|
+
};
|
|
43
|
+
private readonly fetchImpl: typeof fetch;
|
|
44
|
+
private currentConfig: ResolvedConfig | null = null;
|
|
45
|
+
private etag: string | null = null;
|
|
46
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
47
|
+
private disposed = false;
|
|
48
|
+
|
|
49
|
+
constructor(options: PetBoxConfigClientOptions) {
|
|
50
|
+
super();
|
|
51
|
+
if (!options.endpoint) throw new TypeError("endpoint is required");
|
|
52
|
+
if (!options.apiKey) throw new TypeError("apiKey is required");
|
|
53
|
+
if (!options.tags || Object.keys(options.tags).length === 0) throw new TypeError("at least one tag is required");
|
|
54
|
+
|
|
55
|
+
this.options = {
|
|
56
|
+
endpoint: options.endpoint,
|
|
57
|
+
apiKey: options.apiKey,
|
|
58
|
+
tags: options.tags,
|
|
59
|
+
template: options.template ?? "flat",
|
|
60
|
+
refreshIntervalMs: options.refreshIntervalMs ?? DEFAULT_REFRESH_MS,
|
|
61
|
+
optional: options.optional ?? false,
|
|
62
|
+
fetchImpl: options.fetchImpl,
|
|
63
|
+
};
|
|
64
|
+
this.fetchImpl = options.fetchImpl ?? ((...args: Parameters<typeof fetch>) => globalThis.fetch(...args));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The most recently fetched config, or null if never fetched. */
|
|
68
|
+
get current(): ResolvedConfig | null {
|
|
69
|
+
return this.currentConfig;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One-shot fetch. Does NOT start background polling.
|
|
74
|
+
* Throws on auth errors, network errors, and 409 conflicts (unless optional=true).
|
|
75
|
+
*/
|
|
76
|
+
async fetch(): Promise<ResolvedConfig> {
|
|
77
|
+
this.ensureNotDisposed();
|
|
78
|
+
try {
|
|
79
|
+
const cfg = await this.fetchOnce();
|
|
80
|
+
this.currentConfig = cfg;
|
|
81
|
+
this.etag = cfg.etag;
|
|
82
|
+
return cfg;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (this.options.optional) return new ResolvedConfig({}, null);
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Initial fetch + start background ETag polling. Returns the first resolved config.
|
|
91
|
+
* Fires 'change' events on subsequent updates, 'error' on polling failures.
|
|
92
|
+
*/
|
|
93
|
+
async start(): Promise<ResolvedConfig> {
|
|
94
|
+
const cfg = await this.fetch();
|
|
95
|
+
this.startPolling();
|
|
96
|
+
return cfg;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Stop background polling. Does NOT clear the last config. */
|
|
100
|
+
stop(): void {
|
|
101
|
+
if (this.timer !== null) {
|
|
102
|
+
clearInterval(this.timer);
|
|
103
|
+
this.timer = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Stop polling and remove all listeners. */
|
|
108
|
+
dispose(): void {
|
|
109
|
+
this.disposed = true;
|
|
110
|
+
this.stop();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── internals ──────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
private ensureNotDisposed(): void {
|
|
116
|
+
if (this.disposed) throw new Error("PetBoxConfigClient is disposed");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private buildUrl(): string {
|
|
120
|
+
const base = this.options.endpoint.endsWith("/") ? this.options.endpoint : `${this.options.endpoint}/`;
|
|
121
|
+
const params = new URLSearchParams();
|
|
122
|
+
// Sort for stable URLs (helps caching / log grepping).
|
|
123
|
+
const sortedKeys = Object.keys(this.options.tags).sort();
|
|
124
|
+
for (const k of sortedKeys) params.set(k, this.options.tags[k] ?? "");
|
|
125
|
+
if (this.options.template !== "flat") params.set("template", this.options.template);
|
|
126
|
+
return `${base}v1/conf?${params.toString()}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async fetchOnce(): Promise<ResolvedConfig> {
|
|
130
|
+
const url = this.buildUrl();
|
|
131
|
+
const headers: Record<string, string> = {
|
|
132
|
+
"X-YobaConf-ApiKey": this.options.apiKey,
|
|
133
|
+
};
|
|
134
|
+
if (this.etag !== null) headers["If-None-Match"] = `"${this.etag}"`;
|
|
135
|
+
|
|
136
|
+
let response: Response;
|
|
137
|
+
try {
|
|
138
|
+
response = await this.fetchImpl(url, { headers });
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
throw new PetBoxConfigError(`Failed to reach PetBox at ${url}: ${String(cause)}`, 0, null);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 304 — unchanged. Return last-known-good config.
|
|
144
|
+
if (response.status === 304) {
|
|
145
|
+
const newEtag = this.stripEtag(response.headers.get("ETag"));
|
|
146
|
+
return new ResolvedConfig(this.currentConfig?.data ?? {}, newEtag ?? this.etag);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const body = await response.text();
|
|
150
|
+
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
let parsed: unknown = null;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(body);
|
|
155
|
+
} catch {
|
|
156
|
+
/* not JSON */
|
|
157
|
+
}
|
|
158
|
+
throw new PetBoxConfigError(this.errorMessage(response.status, parsed), response.status, parsed);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const etag = this.stripEtag(response.headers.get("ETag"));
|
|
162
|
+
const data: unknown = JSON.parse(body);
|
|
163
|
+
return new ResolvedConfig(data, etag);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private startPolling(): void {
|
|
167
|
+
if (this.options.refreshIntervalMs <= 0) return;
|
|
168
|
+
if (this.timer !== null) return;
|
|
169
|
+
this.timer = setInterval(() => {
|
|
170
|
+
this.poll().catch(() => {
|
|
171
|
+
/* error emitted inside poll */
|
|
172
|
+
});
|
|
173
|
+
}, this.options.refreshIntervalMs);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async poll(): Promise<void> {
|
|
177
|
+
try {
|
|
178
|
+
const cfg = await this.fetchOnce();
|
|
179
|
+
if (cfg.etag !== this.etag) {
|
|
180
|
+
this.currentConfig = cfg;
|
|
181
|
+
this.etag = cfg.etag;
|
|
182
|
+
this.emit("change", cfg);
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
this.emit("error", err);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private stripEtag(raw: string | null): string | null {
|
|
190
|
+
if (raw === null) return null;
|
|
191
|
+
// Server sends `"<hex>"` — strip quotes.
|
|
192
|
+
return raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private errorMessage(status: number, body: unknown): string {
|
|
196
|
+
if (body !== null && typeof body === "object" && "error" in body) {
|
|
197
|
+
const b = body as Record<string, unknown>;
|
|
198
|
+
const reason = typeof b["reason"] === "string" ? `: ${b["reason"]}` : "";
|
|
199
|
+
return `${b["error"]}${reason}`;
|
|
200
|
+
}
|
|
201
|
+
return `HTTP ${status}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Convenience: one-shot fetch without creating a client instance.
|
|
207
|
+
* Same as `new PetBoxConfigClient(opts).fetch()`.
|
|
208
|
+
*/
|
|
209
|
+
export const fetchConfig = (options: PetBoxConfigClientOptions): Promise<ResolvedConfig> =>
|
|
210
|
+
new PetBoxConfigClient(options).fetch();
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable resolved config from a PetBox fetch.
|
|
3
|
+
*
|
|
4
|
+
* For "flat" template: data is a nested JSON tree; `get("db.host")` traverses it.
|
|
5
|
+
* For "dotnet" / "envvar" / "envvar-deep": data is a flat `Record<string, string>`;
|
|
6
|
+
* `get("DB_HOST")` does a direct key lookup.
|
|
7
|
+
*/
|
|
8
|
+
export class ResolvedConfig {
|
|
9
|
+
readonly etag: string | null;
|
|
10
|
+
private readonly raw: unknown;
|
|
11
|
+
|
|
12
|
+
constructor(raw: unknown, etag: string | null) {
|
|
13
|
+
this.raw = raw;
|
|
14
|
+
this.etag = etag;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The raw parsed JSON response body. */
|
|
18
|
+
get data(): unknown {
|
|
19
|
+
return this.raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the string value at the given dotted path (flat template) or direct key (other templates).
|
|
24
|
+
* Returns undefined if the path doesn't exist.
|
|
25
|
+
*/
|
|
26
|
+
get(path: string): string | undefined {
|
|
27
|
+
const v = this.resolve(path);
|
|
28
|
+
if (v === undefined || v === null) return undefined;
|
|
29
|
+
if (typeof v === "string") return v;
|
|
30
|
+
return String(v);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns the numeric value at the given path, or undefined if missing / not a number.
|
|
35
|
+
* Accepts JSON numbers and numeric strings.
|
|
36
|
+
*/
|
|
37
|
+
getNumber(path: string): number | undefined {
|
|
38
|
+
const v = this.resolve(path);
|
|
39
|
+
if (v === undefined || v === null) return undefined;
|
|
40
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : undefined;
|
|
41
|
+
if (typeof v === "string") {
|
|
42
|
+
const n = Number(v);
|
|
43
|
+
return Number.isFinite(n) ? n : undefined;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the boolean value at the given path, or undefined if missing.
|
|
50
|
+
* Accepts JSON booleans and the strings "true"/"false" (case-insensitive).
|
|
51
|
+
*/
|
|
52
|
+
getBoolean(path: string): boolean | undefined {
|
|
53
|
+
const v = this.resolve(path);
|
|
54
|
+
if (v === undefined || v === null) return undefined;
|
|
55
|
+
if (typeof v === "boolean") return v;
|
|
56
|
+
if (typeof v === "string") {
|
|
57
|
+
const lower = v.toLowerCase();
|
|
58
|
+
if (lower === "true") return true;
|
|
59
|
+
if (lower === "false") return false;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the JSON value at the given path (object, array, scalar — whatever the server returned).
|
|
66
|
+
* Returns undefined if the path doesn't exist.
|
|
67
|
+
*/
|
|
68
|
+
getJson<T = unknown>(path: string): T | undefined {
|
|
69
|
+
return this.resolve(path) as T | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Flattens the config into a `Record<string, string>` suitable for process env export.
|
|
74
|
+
* For flat template: expands nested objects to dotted keys. For other templates: returns as-is.
|
|
75
|
+
*/
|
|
76
|
+
toEnv(): Record<string, string> {
|
|
77
|
+
if (this.raw === null || this.raw === undefined) return {};
|
|
78
|
+
if (typeof this.raw !== "object") return {};
|
|
79
|
+
const out: Record<string, string> = {};
|
|
80
|
+
this.flatten(this.raw as Record<string, unknown>, "", out);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private resolve(path: string): unknown {
|
|
85
|
+
if (this.raw === null || this.raw === undefined) return undefined;
|
|
86
|
+
if (typeof this.raw !== "object") return undefined;
|
|
87
|
+
|
|
88
|
+
// If the raw data is a flat dictionary (dotnet/envvar/envvar-deep templates),
|
|
89
|
+
// the keys don't contain dots — do a direct lookup.
|
|
90
|
+
const obj = this.raw as Record<string, unknown>;
|
|
91
|
+
if (!path.includes(".")) return obj[path];
|
|
92
|
+
|
|
93
|
+
// Dotted path — traverse nested object (flat template).
|
|
94
|
+
const segments = path.split(".");
|
|
95
|
+
let current: unknown = obj;
|
|
96
|
+
for (const seg of segments) {
|
|
97
|
+
if (current === null || current === undefined) return undefined;
|
|
98
|
+
if (typeof current !== "object") return undefined;
|
|
99
|
+
current = (current as Record<string, unknown>)[seg];
|
|
100
|
+
}
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private flatten(obj: Record<string, unknown>, prefix: string, out: Record<string, string>): void {
|
|
105
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
106
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
107
|
+
if (v !== null && v !== undefined && typeof v === "object" && !Array.isArray(v)) {
|
|
108
|
+
this.flatten(v as Record<string, unknown>, key, out);
|
|
109
|
+
} else {
|
|
110
|
+
out[key] = v === null || v === undefined ? "" : String(v);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { PetBoxConfigClient, fetchConfig } from "./client.js";
|
|
2
|
+
export { ResolvedConfig } from "./config.js";
|
|
3
|
+
export {
|
|
4
|
+
type TagVector,
|
|
5
|
+
type Template,
|
|
6
|
+
type PetBoxConfigClientOptions,
|
|
7
|
+
type PetBoxConfigClientEvents,
|
|
8
|
+
PetBoxConfigError,
|
|
9
|
+
TypedEmitter,
|
|
10
|
+
} from "./types.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Tag-vector — flat key=value pairs sent as query params to /v1/conf. */
|
|
2
|
+
export type TagVector = Readonly<Record<string, string>>;
|
|
3
|
+
|
|
4
|
+
/** Response-shape templates (spec §9.1). */
|
|
5
|
+
export type Template = "flat" | "dotnet" | "envvar" | "envvar-deep";
|
|
6
|
+
|
|
7
|
+
/** Options for PetBoxConfigClient. */
|
|
8
|
+
export interface PetBoxConfigClientOptions {
|
|
9
|
+
/** Base URL of the PetBox server, e.g. "https://petbox.3po.su". Trailing slash optional. */
|
|
10
|
+
readonly endpoint: string;
|
|
11
|
+
/** Plaintext API key. Sent as X-YobaConf-ApiKey header on every request. */
|
|
12
|
+
readonly apiKey: string;
|
|
13
|
+
/** Tag-vector — every tag the request carries. Resolve finds bindings whose tag-set is a subset. */
|
|
14
|
+
readonly tags: TagVector;
|
|
15
|
+
/** Response template (default: "flat"). Controls the shape of the JSON response. */
|
|
16
|
+
readonly template?: Template;
|
|
17
|
+
/**
|
|
18
|
+
* Polling interval in ms. Each poll uses If-None-Match for cheap 304s.
|
|
19
|
+
* Set to 0 to disable polling (one-shot fetch only). Default: 5 minutes.
|
|
20
|
+
*/
|
|
21
|
+
readonly refreshIntervalMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* When true, initial fetch failures (network, auth, 409) don't throw — the client
|
|
24
|
+
* starts with null config and retries on the next poll. Default: false.
|
|
25
|
+
*/
|
|
26
|
+
readonly optional?: boolean;
|
|
27
|
+
/** Custom fetch implementation (for testing / proxy injection). */
|
|
28
|
+
readonly fetchImpl?: typeof fetch;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Structured error from the PetBox config API. */
|
|
32
|
+
export class PetBoxConfigError extends Error {
|
|
33
|
+
readonly status: number;
|
|
34
|
+
readonly body: unknown;
|
|
35
|
+
|
|
36
|
+
constructor(message: string, status: number, body: unknown) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "PetBoxConfigError";
|
|
39
|
+
this.status = status;
|
|
40
|
+
this.body = body;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Events emitted by PetBoxConfigClient. */
|
|
45
|
+
export interface PetBoxConfigClientEvents {
|
|
46
|
+
/** Fired when config changes (200 response with new data). */
|
|
47
|
+
change: (config: import("./config.js").ResolvedConfig) => void;
|
|
48
|
+
/** Fired on fetch errors during polling. Initial-fetch errors throw (unless optional). */
|
|
49
|
+
error: (err: unknown) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Listener = (...args: never[]) => void;
|
|
53
|
+
|
|
54
|
+
/** Minimal typed event emitter — avoids node:events dependency for portability. */
|
|
55
|
+
export class TypedEmitter<Events extends { [K in keyof Events]: (...args: never[]) => void }> {
|
|
56
|
+
private readonly listeners = new Map<keyof Events, Set<Listener>>();
|
|
57
|
+
|
|
58
|
+
on<E extends keyof Events>(event: E, listener: Events[E]): this {
|
|
59
|
+
let set = this.listeners.get(event);
|
|
60
|
+
if (!set) {
|
|
61
|
+
set = new Set();
|
|
62
|
+
this.listeners.set(event, set);
|
|
63
|
+
}
|
|
64
|
+
set.add(listener as Listener);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
off<E extends keyof Events>(event: E, listener: Events[E]): this {
|
|
69
|
+
this.listeners.get(event)?.delete(listener as Listener);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): void {
|
|
74
|
+
for (const fn of this.listeners.get(event) ?? []) (fn as (...a: Parameters<Events[E]>) => void)(...args);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected listenerCount(event: keyof Events): number {
|
|
78
|
+
return this.listeners.get(event)?.size ?? 0;
|
|
79
|
+
}
|
|
80
|
+
}
|