@govplane/runtime-sdk 0.2.4 → 0.5.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/README.md +400 -216
- package/dist/index.cjs +187 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -14
- package/dist/index.d.ts +65 -14
- package/dist/index.js +187 -91
- package/dist/index.js.map +1 -1
- package/docs/README.md +89 -0
- package/docs/SUMMARY.md +34 -0
- package/docs/installation/GettingStarted.md +474 -0
- package/docs/reference/Configuration.md +198 -0
- package/docs/reference/TypesAndInterfaces.md +373 -0
- package/docs/usage/BundleLifecycle.md +209 -0
- package/docs/usage/ConditionalRules.md +251 -0
- package/docs/usage/ContextPolicy.md +196 -0
- package/docs/usage/CustomEffect.md +217 -0
- package/docs/usage/DecisionTrace.md +289 -0
- package/docs/usage/Effects.md +213 -0
- package/docs/usage/Evaluate.md +164 -0
- package/docs/usage/PolicyDefaults.md +220 -0
- package/package.json +3 -5
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
TypeScript type definitions exported by the SDK — effects, decisions, traces,
|
|
4
|
+
bundles, and engine interfaces.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Types & Interfaces
|
|
8
|
+
|
|
9
|
+
All types are available as named exports from `@govplane/runtime-sdk`.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import type {
|
|
13
|
+
RuntimeClientConfig,
|
|
14
|
+
Decision,
|
|
15
|
+
Effect,
|
|
16
|
+
Target,
|
|
17
|
+
RuntimePolicy,
|
|
18
|
+
RuntimeBundleV1,
|
|
19
|
+
TraceOptions,
|
|
20
|
+
StructuredTraceEvent,
|
|
21
|
+
// ...
|
|
22
|
+
} from "@govplane/runtime-sdk";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Core evaluation types
|
|
28
|
+
|
|
29
|
+
### `Target`
|
|
30
|
+
|
|
31
|
+
Identifies the resource being accessed. All three fields must match exactly for a rule to apply.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
type Target = {
|
|
35
|
+
service: string; // e.g. "api", "payments", "app"
|
|
36
|
+
resource: string; // e.g. "invoices", "feature/checkout-v2"
|
|
37
|
+
action: string; // e.g. "read", "create", "delete"
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `Effect`
|
|
42
|
+
|
|
43
|
+
The effect stored inside a rule or policy default.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
type Effect =
|
|
47
|
+
| { type: "allow" }
|
|
48
|
+
| { type: "deny" }
|
|
49
|
+
| { type: "kill_switch"; killSwitch: { service: string; reason?: string } }
|
|
50
|
+
| { type: "throttle"; throttle: { limit: number; windowSeconds: number; key: string } }
|
|
51
|
+
| { type: "custom"; value: string };
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `Decision`
|
|
55
|
+
|
|
56
|
+
The output of `evaluate()`. Discriminated by `decision`.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
type Decision =
|
|
60
|
+
| { decision: "allow";
|
|
61
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string }
|
|
62
|
+
|
|
63
|
+
| { decision: "deny";
|
|
64
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string }
|
|
65
|
+
|
|
66
|
+
| { decision: "kill_switch";
|
|
67
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
|
|
68
|
+
killSwitch: { service: string; reason?: string } }
|
|
69
|
+
|
|
70
|
+
| { decision: "throttle";
|
|
71
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
|
|
72
|
+
throttle: { limit: number; windowSeconds: number; key: string } }
|
|
73
|
+
|
|
74
|
+
| { decision: "custom";
|
|
75
|
+
reason: "default" | "rule"; policyKey?: string; ruleId?: string;
|
|
76
|
+
value: string; parsedValue?: unknown };
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Field | Description |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `decision` | Effect type that was applied. |
|
|
82
|
+
| `reason` | `"rule"` — a matching rule fired. `"default"` — a policy default or the SDK global deny-by-default was used. |
|
|
83
|
+
| `policyKey` | Policy that produced the decision. `undefined` on global deny-by-default. |
|
|
84
|
+
| `ruleId` | Rule that matched. `undefined` when `reason === "default"`. |
|
|
85
|
+
| `value` | (custom only) Raw string from the bundle. |
|
|
86
|
+
| `parsedValue` | (custom only) JSON-parsed `value`. Present only when `parseCustomEffect: true` and the value is valid JSON. |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Bundle types
|
|
91
|
+
|
|
92
|
+
### `RuntimeBundleV1`
|
|
93
|
+
|
|
94
|
+
The top-level compiled bundle document.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
type RuntimeBundleV1 = {
|
|
98
|
+
schemaVersion: 1;
|
|
99
|
+
orgId: string;
|
|
100
|
+
projectId: string;
|
|
101
|
+
env: string;
|
|
102
|
+
generatedAt: string; // ISO timestamp
|
|
103
|
+
bundleVersion?: number;
|
|
104
|
+
checksum?: string; // e.g. "sha256:..."
|
|
105
|
+
policies: RuntimePolicy[];
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `RuntimePolicy`
|
|
110
|
+
|
|
111
|
+
A single policy within the bundle.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
type RuntimePolicy = {
|
|
115
|
+
policyKey: string;
|
|
116
|
+
activeVersion: number;
|
|
117
|
+
defaults?: PolicyDefault; // fallback effect when no rule matches
|
|
118
|
+
rules: RuntimeRule[];
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `PolicyDefault`
|
|
123
|
+
|
|
124
|
+
The fallback effect applied at the policy level.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
type PolicyDefault =
|
|
128
|
+
| { effect: "allow" }
|
|
129
|
+
| { effect: "deny" }
|
|
130
|
+
| { effect: "kill_switch"; killSwitch: { service: string; reason?: string } }
|
|
131
|
+
| { effect: "throttle"; throttle: { limit: number; windowSeconds: number; key: string } }
|
|
132
|
+
| { effect: "custom"; customEffect: string };
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `RuntimeRule`
|
|
136
|
+
|
|
137
|
+
An individual rule inside a policy.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
type RuntimeRule = {
|
|
141
|
+
id: string;
|
|
142
|
+
status: "active" | "disabled";
|
|
143
|
+
priority: number;
|
|
144
|
+
target: Target;
|
|
145
|
+
when?: WhenAstV1; // condition AST
|
|
146
|
+
thenEffect?: Effect; // effect when when == true (fallback: effect)
|
|
147
|
+
elseEffect?: Effect; // effect when when == false (fallback: skip)
|
|
148
|
+
effect: Effect; // unconditional / fallback effect
|
|
149
|
+
description?: string;
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `WhenAstV1`
|
|
154
|
+
|
|
155
|
+
The condition AST node. Recursive (logical operators contain child nodes).
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
type WhenAstV1 =
|
|
159
|
+
| { op: "and" | "or"; conditions: WhenAstV1[] }
|
|
160
|
+
| { op: "not"; condition: WhenAstV1 }
|
|
161
|
+
| { op: "eq" | "neq" | "gt" | "gte" | "lt" | "lte"; path: string; value: any }
|
|
162
|
+
| { op: "in"; path: string; values: any[] }
|
|
163
|
+
| { op: "exists"; path: string };
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Client types
|
|
169
|
+
|
|
170
|
+
### `RuntimeClientConfig`
|
|
171
|
+
|
|
172
|
+
See the full [Configuration Reference](Configuration.md).
|
|
173
|
+
|
|
174
|
+
### `RuntimeStatus`
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
type RuntimeStatus =
|
|
178
|
+
| { state: "warming_up" }
|
|
179
|
+
| { state: "ok" }
|
|
180
|
+
| {
|
|
181
|
+
state: "degraded";
|
|
182
|
+
consecutiveFailures: number;
|
|
183
|
+
lastError: { message: string; at: string };
|
|
184
|
+
nextRetryAt?: string;
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `BundleMeta`
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
type BundleMeta = {
|
|
192
|
+
etag: string;
|
|
193
|
+
bundleVersion?: number;
|
|
194
|
+
updatedAt?: string;
|
|
195
|
+
};
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `RuntimeCache<TBundle>`
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
type RuntimeCache<TBundle = unknown> = {
|
|
202
|
+
meta?: BundleMeta;
|
|
203
|
+
bundle?: TBundle;
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `RefreshResult<TBundle>`
|
|
208
|
+
|
|
209
|
+
Returned by `refreshNow()` and `onUpdate()` callbacks.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
type RefreshResult<TBundle = unknown> =
|
|
213
|
+
| { changed: false; meta?: BundleMeta }
|
|
214
|
+
| { changed: true; meta: BundleMeta; bundle: TBundle };
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Engine types
|
|
220
|
+
|
|
221
|
+
### `PolicyEngine`
|
|
222
|
+
|
|
223
|
+
The low-level interface returned by `createPolicyEngine()`.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
type PolicyEngine = {
|
|
227
|
+
evaluate(input: { target: Target; context?: Record<string, unknown> }): Decision;
|
|
228
|
+
|
|
229
|
+
evaluateWithTrace(
|
|
230
|
+
input: { target: Target; context?: Record<string, unknown> },
|
|
231
|
+
options?: TraceOptions
|
|
232
|
+
): DecisionWithOptionalTrace;
|
|
233
|
+
|
|
234
|
+
flushTraces(): Promise<void>;
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Trace types
|
|
241
|
+
|
|
242
|
+
### `TraceOptions`
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
type TraceOptions = {
|
|
246
|
+
level?: "off" | "errors" | "sampled" | "full";
|
|
247
|
+
sampling?: number; // 0..1
|
|
248
|
+
force?: boolean;
|
|
249
|
+
budget?: {
|
|
250
|
+
maxTraces: number;
|
|
251
|
+
windowMs: number;
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### `DecisionTraceCompact`
|
|
257
|
+
|
|
258
|
+
Attached to decisions when `level` is `"errors"` or `"sampled"`.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
type DecisionTraceCompact = {
|
|
262
|
+
traceId: string;
|
|
263
|
+
sampled: "forced" | "random";
|
|
264
|
+
evaluatedAt: string;
|
|
265
|
+
target: Target;
|
|
266
|
+
summary: {
|
|
267
|
+
policiesSeen: number;
|
|
268
|
+
rulesSeen: number;
|
|
269
|
+
matched: number;
|
|
270
|
+
considered: {
|
|
271
|
+
kill_switch: number;
|
|
272
|
+
deny: number;
|
|
273
|
+
throttle: number;
|
|
274
|
+
allow: number;
|
|
275
|
+
custom: number;
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
winner?: {
|
|
279
|
+
policyKey: string;
|
|
280
|
+
ruleId: string;
|
|
281
|
+
effectType: string;
|
|
282
|
+
priority: number;
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `DecisionTraceFull`
|
|
288
|
+
|
|
289
|
+
Extends `DecisionTraceCompact` with a per-rule breakdown. Present when `level === "full"`.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
type DecisionTraceFull = DecisionTraceCompact & {
|
|
293
|
+
rules: Array<{
|
|
294
|
+
policyKey: string;
|
|
295
|
+
ruleId: string;
|
|
296
|
+
priority: number;
|
|
297
|
+
effectType?: string;
|
|
298
|
+
matched: boolean;
|
|
299
|
+
discardedReason?: "disabled" | "target_mismatch" | "when_false" | "invalid_effect";
|
|
300
|
+
}>;
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### `StructuredTraceEvent`
|
|
305
|
+
|
|
306
|
+
The object delivered to `onDecisionTrace` / `onDecisionTraceAsync` sinks.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
type StructuredTraceEvent = {
|
|
310
|
+
v: 1;
|
|
311
|
+
ts: string;
|
|
312
|
+
traceId: string;
|
|
313
|
+
sampled: "forced" | "random" | "errors";
|
|
314
|
+
level: TraceLevel;
|
|
315
|
+
target: Target;
|
|
316
|
+
decision: Decision["decision"];
|
|
317
|
+
reason: Decision["reason"];
|
|
318
|
+
winner?: { policyKey: string; ruleId: string; effectType: string; priority: number };
|
|
319
|
+
summary: { /* same as DecisionTraceCompact.summary */ };
|
|
320
|
+
rules?: Array<{ /* same as DecisionTraceFull.rules entry */ }>; // level === "full" only
|
|
321
|
+
};
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### `TraceSink` / `TraceSinkAsync`
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
type TraceSink = (evt: StructuredTraceEvent) => void;
|
|
328
|
+
type TraceSinkAsync = (evt: StructuredTraceEvent) => Promise<void>;
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Error classes
|
|
334
|
+
|
|
335
|
+
### `GovplaneError`
|
|
336
|
+
|
|
337
|
+
Base error class for all SDK errors.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
class GovplaneError extends Error {
|
|
341
|
+
readonly code: string; // "GP_ERROR" by default
|
|
342
|
+
readonly details: unknown;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### `HttpError`
|
|
347
|
+
|
|
348
|
+
Thrown when the bundle fetch returns a non-2xx response.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
class HttpError extends GovplaneError {
|
|
352
|
+
readonly status: number; // HTTP status code
|
|
353
|
+
readonly headers: Record<string, string> | undefined;
|
|
354
|
+
// code = "HTTP_ERROR"
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Context types
|
|
361
|
+
|
|
362
|
+
### `ContextPolicy`
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
type ContextPolicy = {
|
|
366
|
+
allowedKeys: string[];
|
|
367
|
+
maxStringLen: number;
|
|
368
|
+
maxArrayLen: number;
|
|
369
|
+
blockLikelyPiiKeys: boolean;
|
|
370
|
+
};
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The `DEFAULT_CONTEXT_POLICY` export contains the out-of-the-box values. See [Context Policy](../usage/ContextPolicy.md).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
How the SDK fetches and keeps the runtime bundle fresh — polling, backoff,
|
|
4
|
+
degraded mode, and burst refreshes.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Bundle Lifecycle
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The `RuntimeClient` maintains a local in-memory copy of the compiled runtime bundle. All `evaluate()` calls read from this cache — there is no network hop at decision time.
|
|
12
|
+
|
|
13
|
+
The bundle is kept up to date via a background HTTP polling loop with automatic exponential backoff and a degraded-mode watchdog.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌──────────────┐ HEAD (ETag check) ┌──────────────────────┐
|
|
17
|
+
│ │──────────────────────────────────▶│ │
|
|
18
|
+
│ RuntimeClient│ GET (only when ETag changed) │ Govplane Bundle API │
|
|
19
|
+
│ (in-memory) │◀──────────────────────────────────│ │
|
|
20
|
+
└──────────────┘ └──────────────────────┘
|
|
21
|
+
│
|
|
22
|
+
│ cached bundle
|
|
23
|
+
▼
|
|
24
|
+
PolicyEngine.evaluate() ← sub-millisecond, no network
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Startup
|
|
30
|
+
|
|
31
|
+
### `warmStart(opts?)` — recommended
|
|
32
|
+
|
|
33
|
+
Blocks until a valid bundle is cached or the timeout is exceeded. Use this before your server starts accepting traffic.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
await client.warmStart({ timeoutMs: 10_000 });
|
|
37
|
+
client.start(); // then begin background polling
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
type WarmStartOptions = {
|
|
42
|
+
timeoutMs?: number; // default 10 000 ms
|
|
43
|
+
burst?: boolean; // use burstPollMs during warm-up
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If no valid bundle is received within `timeoutMs`, `warmStart()` throws:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Error: warmStart timeout: no valid runtime bundle received
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `start()` — background polling
|
|
54
|
+
|
|
55
|
+
Starts the polling loop. Safe to call multiple times (idempotent).
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
client.start();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Polling behaviour
|
|
64
|
+
|
|
65
|
+
| Phase | Interval |
|
|
66
|
+
|---|---|
|
|
67
|
+
| Normal | `pollMs` (default **5 000 ms**) |
|
|
68
|
+
| Burst mode | `burstPollMs` (default **500 ms**) for `burstDurationMs` (default **30 000 ms**) |
|
|
69
|
+
| Backoff (after failures) | Exponential, capped at `backoffMaxMs` |
|
|
70
|
+
|
|
71
|
+
### Bundle fetch flow
|
|
72
|
+
|
|
73
|
+
Each poll cycle:
|
|
74
|
+
|
|
75
|
+
1. **HEAD** the bundle endpoint to check the current `ETag`.
|
|
76
|
+
2. If `ETag` is unchanged → update `meta`, skip the `GET`.
|
|
77
|
+
3. If `ETag` changed → **GET** the full bundle body, parse, and update the cache.
|
|
78
|
+
4. Notify `onUpdate` listeners.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Backoff & degraded mode
|
|
83
|
+
|
|
84
|
+
After a configurable number of consecutive failures the client enters **degraded mode**. Polling continues but at increasingly long intervals.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const client = new RuntimeClient({
|
|
88
|
+
...
|
|
89
|
+
backoffBaseMs: 500, // initial backoff (default 500 ms)
|
|
90
|
+
backoffMaxMs: 30_000, // maximum backoff (default 30 s)
|
|
91
|
+
backoffJitter: 0.2, // ±20% jitter (default 0.2)
|
|
92
|
+
degradeAfterFailures: 3, // failures before degraded (default 3)
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Backoff formula: `delay = clamp(base × 2^(failures-1), 0, backoffMaxMs) ± jitter%`
|
|
97
|
+
|
|
98
|
+
### `getStatus()`
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
type RuntimeStatus =
|
|
102
|
+
| { state: "warming_up" }
|
|
103
|
+
| { state: "ok" }
|
|
104
|
+
| {
|
|
105
|
+
state: "degraded";
|
|
106
|
+
consecutiveFailures: number;
|
|
107
|
+
lastError: { message: string; at: string };
|
|
108
|
+
nextRetryAt?: string; // ISO timestamp of next scheduled retry
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const status = client.getStatus();
|
|
114
|
+
|
|
115
|
+
if (status.state === "degraded") {
|
|
116
|
+
alerting.trigger("govplane_sdk_degraded", {
|
|
117
|
+
failures: status.consecutiveFailures,
|
|
118
|
+
lastError: status.lastError.message,
|
|
119
|
+
nextRetryAt: status.nextRetryAt,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `onStatus(fn)` — status change subscription
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const unsub = client.onStatus((status) => {
|
|
128
|
+
metrics.gauge("govplane.sdk.degraded", status.state === "degraded" ? 1 : 0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Later, to unsubscribe:
|
|
132
|
+
unsub();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Bundle update subscription
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const unsub = client.onUpdate((result) => {
|
|
141
|
+
// result.changed is always true here (the listener only fires on changes)
|
|
142
|
+
logger.info("Bundle updated", {
|
|
143
|
+
etag: result.meta.etag,
|
|
144
|
+
bundleVersion: result.meta.bundleVersion,
|
|
145
|
+
updatedAt: result.meta.updatedAt,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Manual refresh
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Trigger an immediate refresh
|
|
156
|
+
const result = await client.refreshNow();
|
|
157
|
+
|
|
158
|
+
if (result.changed) {
|
|
159
|
+
console.log("New bundle version:", result.meta.bundleVersion);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Trigger a burst refresh (also enables burst polling for burstDurationMs)
|
|
163
|
+
await client.refreshNow({ burst: true });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Burst mode
|
|
169
|
+
|
|
170
|
+
Burst mode temporarily switches the poll interval from `pollMs` to `burstPollMs`. It is used:
|
|
171
|
+
|
|
172
|
+
- Manually via `refreshNow({ burst: true })`
|
|
173
|
+
- During incident-mode activation (file trigger or signal handler)
|
|
174
|
+
|
|
175
|
+
Burst mode automatically expires after `burstDurationMs` (default 30 s).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Stopping gracefully
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
process.on("SIGTERM", async () => {
|
|
183
|
+
client.stop(); // stop polling timers
|
|
184
|
+
await client.flushTraces(); // drain the async trace queue
|
|
185
|
+
process.exit(0);
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Cache inspection
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const cache = client.getCached();
|
|
195
|
+
|
|
196
|
+
console.log(cache.meta?.etag);
|
|
197
|
+
console.log(cache.meta?.bundleVersion);
|
|
198
|
+
console.log(cache.bundle); // the raw RuntimeBundleV1 object
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
{% content-ref url="../operations/Govplane_Incident_Playbook.md" %}
|
|
204
|
+
[Incident Playbook](../operations/Govplane_Incident_Playbook.md)
|
|
205
|
+
{% endcontent-ref %}
|
|
206
|
+
|
|
207
|
+
{% content-ref url="../reference/Configuration.md" %}
|
|
208
|
+
[Configuration Reference](../reference/Configuration.md)
|
|
209
|
+
{% endcontent-ref %}
|