@leavepulse/control-sdk 0.3.48 → 0.3.50
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/package.json +1 -1
- package/resources/ControlDcimComponentType.ts +12 -0
- package/runtime/realtime-client.ts +101 -0
- package/types.ts +54 -1
package/package.json
CHANGED
|
@@ -35,6 +35,18 @@ export class ControlDcimComponentType extends Resource<Data> {
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** control.dcim.catalog.component_types.get */
|
|
39
|
+
async controlDcimCatalogComponentTypesGet(): Promise<models.ComponentTypeDTO> {
|
|
40
|
+
return fetchCachedOrThrow<models.ComponentTypeDTO>(
|
|
41
|
+
this.ctx.transport,
|
|
42
|
+
this.ctx.etagStore,
|
|
43
|
+
{
|
|
44
|
+
method: "GET",
|
|
45
|
+
path: `/v1/control/dcim/catalog/component-types/${this.id}`,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
/** control.dcim.catalog.component_types.upsert */
|
|
39
51
|
async dcimCatalogComponentTypesUpsert(
|
|
40
52
|
body: models.UpsertComponentTypeRequest,
|
|
@@ -60,6 +60,51 @@ interface PendingRequest {
|
|
|
60
60
|
|
|
61
61
|
const DEFAULT_MIN_MS = 500;
|
|
62
62
|
const DEFAULT_MAX_MS = 30_000;
|
|
63
|
+
/**
|
|
64
|
+
* How long before a ws-token expires to fetch and send the next one.
|
|
65
|
+
*
|
|
66
|
+
* The gateway accepts an `auth` frame at any point in a session and rebinds the
|
|
67
|
+
* socket, but nothing was ever sending a second one: the token was presented
|
|
68
|
+
* once at connect and then left to rot. A ws-token lives two minutes, so every
|
|
69
|
+
* live subscription was torn down and rebuilt on that cycle — visible on the
|
|
70
|
+
* server as a matched pair of "connection closed" every 120s, and to an
|
|
71
|
+
* operator as live metrics that stall, because the agent's fast-sample tier is
|
|
72
|
+
* driven by subscriber count and dropped to idle in each gap.
|
|
73
|
+
*
|
|
74
|
+
* The margin has to cover fetching a fresh token (a round trip to auth-service)
|
|
75
|
+
* plus clock skew between the browser and the issuer.
|
|
76
|
+
*/
|
|
77
|
+
const AUTH_REFRESH_MARGIN_MS = 20_000;
|
|
78
|
+
/** Never re-arm faster than this, however short-lived the token claims to be. */
|
|
79
|
+
const AUTH_REFRESH_MIN_MS = 5_000;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Milliseconds until a JWT's `exp`, or null if it carries no usable one.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately does not verify the signature — the client cannot, and does not
|
|
85
|
+
* need to: this only decides WHEN to ask for the next token. A malformed or
|
|
86
|
+
* unsigned token yields null and refresh stays off, leaving the previous
|
|
87
|
+
* reconnect behaviour rather than a tight loop.
|
|
88
|
+
*/
|
|
89
|
+
function millisUntilExpiry(token: string): number | null {
|
|
90
|
+
// Destructured rather than indexed: consumers compile this file under their
|
|
91
|
+
// own tsconfig, and one with noUncheckedIndexedAccess types parts[1] as
|
|
92
|
+
// possibly undefined however the length was checked.
|
|
93
|
+
const [header, payload, signature] = token.split(".");
|
|
94
|
+
if (!header || !payload || !signature) return null;
|
|
95
|
+
try {
|
|
96
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
97
|
+
const padded = base64.padEnd(
|
|
98
|
+
base64.length + ((4 - (base64.length % 4)) % 4),
|
|
99
|
+
"=",
|
|
100
|
+
);
|
|
101
|
+
const claims = JSON.parse(atob(padded)) as { exp?: unknown };
|
|
102
|
+
if (typeof claims.exp !== "number") return null;
|
|
103
|
+
return claims.exp * 1000 - Date.now();
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
63
108
|
|
|
64
109
|
/** Decode a JSON payload carried in an Event/Reply `data` byte field. */
|
|
65
110
|
function decodeJson(bytes: Uint8Array): unknown {
|
|
@@ -94,6 +139,9 @@ export class RealtimeClient {
|
|
|
94
139
|
// "Authentication required for private topic" and we needlessly reconnect).
|
|
95
140
|
private welcomed: Promise<void> | null = null;
|
|
96
141
|
private resolveWelcome: (() => void) | null = null;
|
|
142
|
+
// Pending re-auth for the CURRENT socket. Cleared on close so a timer from a
|
|
143
|
+
// dead socket cannot fire an auth frame into its replacement.
|
|
144
|
+
private authRefresh: ReturnType<typeof setTimeout> | null = null;
|
|
97
145
|
|
|
98
146
|
constructor(private readonly opts: RealtimeClientOptions) {}
|
|
99
147
|
|
|
@@ -148,6 +196,9 @@ export class RealtimeClient {
|
|
|
148
196
|
|
|
149
197
|
close(): void {
|
|
150
198
|
this.closedByUser = true;
|
|
199
|
+
// Explicit close bypasses onClose (we drop the socket ourselves), so the
|
|
200
|
+
// refresh timer has to be cancelled here too or it outlives the client.
|
|
201
|
+
this.clearAuthRefresh();
|
|
151
202
|
this.subscriptions.clear();
|
|
152
203
|
for (const p of this.pending.values())
|
|
153
204
|
p.reject(new Error("realtime client closed"));
|
|
@@ -220,6 +271,7 @@ export class RealtimeClient {
|
|
|
220
271
|
body: { case: "auth", value: { token } },
|
|
221
272
|
}),
|
|
222
273
|
);
|
|
274
|
+
this.scheduleAuthRefresh(token);
|
|
223
275
|
}
|
|
224
276
|
// Wait for the gateway's `welcome` (auth applied) before (re)subscribing,
|
|
225
277
|
// so private topics aren't requested on an un-bound socket. The gate is
|
|
@@ -231,7 +283,56 @@ export class RealtimeClient {
|
|
|
231
283
|
this.reconnectAttempts = 0;
|
|
232
284
|
}
|
|
233
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Send a fresh token over the OPEN socket shortly before this one expires.
|
|
288
|
+
*
|
|
289
|
+
* Re-authenticating in place is what keeps a long-lived subscription alive:
|
|
290
|
+
* the alternative is letting the token lapse and being reconnected, which
|
|
291
|
+
* drops every subscription for the duration and, for subscriber-driven
|
|
292
|
+
* sources, stops the upstream feed entirely until it is re-established.
|
|
293
|
+
*/
|
|
294
|
+
private scheduleAuthRefresh(token: string): void {
|
|
295
|
+
this.clearAuthRefresh();
|
|
296
|
+
if (!this.opts.getToken) return;
|
|
297
|
+
const remaining = millisUntilExpiry(token);
|
|
298
|
+
if (remaining === null) return;
|
|
299
|
+
const delay = Math.max(
|
|
300
|
+
AUTH_REFRESH_MIN_MS,
|
|
301
|
+
remaining - AUTH_REFRESH_MARGIN_MS,
|
|
302
|
+
);
|
|
303
|
+
this.authRefresh = setTimeout(() => {
|
|
304
|
+
void this.refreshAuth();
|
|
305
|
+
}, delay);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async refreshAuth(): Promise<void> {
|
|
309
|
+
const socket = this.socket;
|
|
310
|
+
if (!socket || !this.opts.getToken) return;
|
|
311
|
+
let next: string | null = null;
|
|
312
|
+
try {
|
|
313
|
+
next = await this.opts.getToken();
|
|
314
|
+
} catch {
|
|
315
|
+
next = null;
|
|
316
|
+
}
|
|
317
|
+
// Nothing to send, or the socket died while we were fetching: leave it to
|
|
318
|
+
// the reconnect path rather than authenticating a socket that is gone.
|
|
319
|
+
if (!next || this.socket !== socket) return;
|
|
320
|
+
this.sendFrame(
|
|
321
|
+
create(ClientFrameSchema, {
|
|
322
|
+
body: { case: "auth", value: { token: next } },
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
this.scheduleAuthRefresh(next);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private clearAuthRefresh(): void {
|
|
329
|
+
if (this.authRefresh === null) return;
|
|
330
|
+
clearTimeout(this.authRefresh);
|
|
331
|
+
this.authRefresh = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
234
334
|
private onClose(): void {
|
|
335
|
+
this.clearAuthRefresh();
|
|
235
336
|
this.socket = null;
|
|
236
337
|
this.connecting = null;
|
|
237
338
|
this.authenticated = false;
|
package/types.ts
CHANGED
|
@@ -498,7 +498,8 @@ export interface paths {
|
|
|
498
498
|
path?: never;
|
|
499
499
|
cookie?: never;
|
|
500
500
|
};
|
|
501
|
-
|
|
501
|
+
/** GetComponentType */
|
|
502
|
+
get: operations["control.dcim.catalog.component_types.get"];
|
|
502
503
|
put?: never;
|
|
503
504
|
post?: never;
|
|
504
505
|
/** DeleteComponentType */
|
|
@@ -1991,6 +1992,12 @@ export interface components {
|
|
|
1991
1992
|
manufacturer_name: string;
|
|
1992
1993
|
model: string;
|
|
1993
1994
|
notes: string;
|
|
1995
|
+
/** @default */
|
|
1996
|
+
output_connector: string;
|
|
1997
|
+
/** @default 0 */
|
|
1998
|
+
output_ma: number;
|
|
1999
|
+
/** @default 0 */
|
|
2000
|
+
output_mv: number;
|
|
1994
2001
|
part_number: string;
|
|
1995
2002
|
power_w: number;
|
|
1996
2003
|
source: string;
|
|
@@ -3297,6 +3304,12 @@ export interface components {
|
|
|
3297
3304
|
/** @default */
|
|
3298
3305
|
notes?: string;
|
|
3299
3306
|
/** @default */
|
|
3307
|
+
output_connector?: string;
|
|
3308
|
+
/** @default 0 */
|
|
3309
|
+
output_ma?: number;
|
|
3310
|
+
/** @default 0 */
|
|
3311
|
+
output_mv?: number;
|
|
3312
|
+
/** @default */
|
|
3300
3313
|
part_number?: string;
|
|
3301
3314
|
/** @default 0 */
|
|
3302
3315
|
power_w?: number;
|
|
@@ -5155,6 +5168,46 @@ export interface operations {
|
|
|
5155
5168
|
};
|
|
5156
5169
|
};
|
|
5157
5170
|
};
|
|
5171
|
+
"control.dcim.catalog.component_types.get": {
|
|
5172
|
+
parameters: {
|
|
5173
|
+
query?: never;
|
|
5174
|
+
header?: never;
|
|
5175
|
+
path: {
|
|
5176
|
+
component_type_id: string;
|
|
5177
|
+
};
|
|
5178
|
+
cookie?: never;
|
|
5179
|
+
};
|
|
5180
|
+
requestBody?: never;
|
|
5181
|
+
responses: {
|
|
5182
|
+
/** @description Request fulfilled, document follows */
|
|
5183
|
+
200: {
|
|
5184
|
+
headers: {
|
|
5185
|
+
[name: string]: unknown;
|
|
5186
|
+
};
|
|
5187
|
+
content: {
|
|
5188
|
+
"application/json": components["schemas"]["ComponentTypeDTO"];
|
|
5189
|
+
};
|
|
5190
|
+
};
|
|
5191
|
+
/** @description Bad request syntax or unsupported method */
|
|
5192
|
+
400: {
|
|
5193
|
+
headers: {
|
|
5194
|
+
[name: string]: unknown;
|
|
5195
|
+
};
|
|
5196
|
+
content: {
|
|
5197
|
+
"application/json": {
|
|
5198
|
+
detail: string;
|
|
5199
|
+
extra?:
|
|
5200
|
+
| null
|
|
5201
|
+
| {
|
|
5202
|
+
[key: string]: unknown;
|
|
5203
|
+
}
|
|
5204
|
+
| unknown[];
|
|
5205
|
+
status_code: number;
|
|
5206
|
+
};
|
|
5207
|
+
};
|
|
5208
|
+
};
|
|
5209
|
+
};
|
|
5210
|
+
};
|
|
5158
5211
|
"control.dcim.catalog.component_types.delete": {
|
|
5159
5212
|
parameters: {
|
|
5160
5213
|
query?: never;
|