iotas-ts 0.1.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/LICENSE +674 -0
- package/dist/src/api/iotasClient.d.ts +27 -0
- package/dist/src/api/iotasClient.js +123 -0
- package/dist/src/api/iotasClient.js.map +1 -0
- package/dist/src/api/jwt.d.ts +7 -0
- package/dist/src/api/jwt.js +13 -0
- package/dist/src/api/jwt.js.map +1 -0
- package/dist/src/api/session.d.ts +43 -0
- package/dist/src/api/session.js +137 -0
- package/dist/src/api/session.js.map +1 -0
- package/dist/src/api/transport.d.ts +21 -0
- package/dist/src/api/transport.js +49 -0
- package/dist/src/api/transport.js.map +1 -0
- package/dist/src/cache/featureCache.d.ts +55 -0
- package/dist/src/cache/featureCache.js +180 -0
- package/dist/src/cache/featureCache.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.d.ts +12 -0
- package/dist/src/logger.js +2 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/types.d.ts +148 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils.d.ts +4 -0
- package/dist/src/utils.js +5 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IotasLogger } from '../logger.js';
|
|
2
|
+
import type { Feature, IotasClientOptions, Rooms } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* High-level IOTAS API client.
|
|
5
|
+
*
|
|
6
|
+
* Provides unit resolution, room/device discovery, and feature operations.
|
|
7
|
+
* Delegates authentication to IotasSession and HTTP to IotasTransport.
|
|
8
|
+
*/
|
|
9
|
+
export declare class IotasClient {
|
|
10
|
+
private readonly log;
|
|
11
|
+
private readonly unitName?;
|
|
12
|
+
private readonly transport;
|
|
13
|
+
private readonly reliableUpdateAttempts;
|
|
14
|
+
private readonly reliableUpdateDelayMs;
|
|
15
|
+
private readonly delay;
|
|
16
|
+
private unitRequest;
|
|
17
|
+
private featureIndex;
|
|
18
|
+
private unit;
|
|
19
|
+
constructor(options: IotasClientOptions);
|
|
20
|
+
static withCredentials(log: IotasLogger, email: string, password: string, unitName?: string, options?: Pick<IotasClientOptions, 'fetch' | 'delay'>): IotasClient;
|
|
21
|
+
initialize(): Promise<void>;
|
|
22
|
+
getRooms(): Promise<Rooms>;
|
|
23
|
+
getFeature(featureId: string): Promise<Feature | null>;
|
|
24
|
+
private buildFeatureIndex;
|
|
25
|
+
updateFeature(featureId: string, value: number): Promise<void>;
|
|
26
|
+
updateFeatureReliable(featureId: string, value: number): Promise<void>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { IotasSession } from './session.js';
|
|
2
|
+
import { IotasTransport } from './transport.js';
|
|
3
|
+
const DEFAULT_RELIABLE_UPDATE_ATTEMPTS = 3;
|
|
4
|
+
const DEFAULT_RELIABLE_UPDATE_DELAY_MS = 500;
|
|
5
|
+
const defaultDelay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
/**
|
|
7
|
+
* High-level IOTAS API client.
|
|
8
|
+
*
|
|
9
|
+
* Provides unit resolution, room/device discovery, and feature operations.
|
|
10
|
+
* Delegates authentication to IotasSession and HTTP to IotasTransport.
|
|
11
|
+
*/
|
|
12
|
+
export class IotasClient {
|
|
13
|
+
log;
|
|
14
|
+
unitName;
|
|
15
|
+
transport;
|
|
16
|
+
reliableUpdateAttempts;
|
|
17
|
+
reliableUpdateDelayMs;
|
|
18
|
+
delay;
|
|
19
|
+
unitRequest = null;
|
|
20
|
+
featureIndex = null;
|
|
21
|
+
unit = 0;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.log = options.log;
|
|
24
|
+
this.unitName = options.unitName;
|
|
25
|
+
this.reliableUpdateAttempts = options.reliableUpdate?.attempts ?? DEFAULT_RELIABLE_UPDATE_ATTEMPTS;
|
|
26
|
+
this.reliableUpdateDelayMs = options.reliableUpdate?.delayMs ?? DEFAULT_RELIABLE_UPDATE_DELAY_MS;
|
|
27
|
+
this.delay = options.delay ?? defaultDelay;
|
|
28
|
+
const session = new IotasSession({
|
|
29
|
+
log: options.log,
|
|
30
|
+
email: options.email,
|
|
31
|
+
authenticate: options.authenticate,
|
|
32
|
+
onTokensChanged: options.onTokensChanged,
|
|
33
|
+
initialTokens: options.initialTokens,
|
|
34
|
+
baseUrl: options.baseUrl,
|
|
35
|
+
authRetryDelaysMs: options.authRetryDelaysMs,
|
|
36
|
+
fetch: options.fetch,
|
|
37
|
+
delay: options.delay,
|
|
38
|
+
});
|
|
39
|
+
this.transport = new IotasTransport({
|
|
40
|
+
session,
|
|
41
|
+
baseUrl: options.baseUrl,
|
|
42
|
+
maxRequestAuthRetries: options.maxRequestAuthRetries,
|
|
43
|
+
fetch: options.fetch,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
static withCredentials(log, email, password, unitName, options) {
|
|
47
|
+
return new IotasClient({
|
|
48
|
+
log,
|
|
49
|
+
email,
|
|
50
|
+
unitName,
|
|
51
|
+
authenticate: async () => ({ username: email, password }),
|
|
52
|
+
...options,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async initialize() {
|
|
56
|
+
if (this.unit !== 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const account = await this.transport.request('/account/me');
|
|
60
|
+
this.log.info('Found account id', account.id);
|
|
61
|
+
const residencies = await this.transport.request(`/account/${account.id}/residency`);
|
|
62
|
+
if (residencies.length === 0) {
|
|
63
|
+
this.log.error('Unable to find any units. Abandoning...');
|
|
64
|
+
throw new Error('Unable to find any units');
|
|
65
|
+
}
|
|
66
|
+
this.log.info('Found unit(s):', residencies.map((r) => r.unitName).join(', '));
|
|
67
|
+
if (this.unitName) {
|
|
68
|
+
const customUnit = residencies.find((r) => r.unitName === this.unitName);
|
|
69
|
+
if (customUnit) {
|
|
70
|
+
this.unit = customUnit.unit;
|
|
71
|
+
this.log.info('Using custom unit:', this.unitName);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.log.warn('Could not find unit', this.unitName, ', using default');
|
|
75
|
+
}
|
|
76
|
+
this.unit = residencies[0].unit;
|
|
77
|
+
this.log.info('Using first unit found:', residencies[0].unitName, '. If you would like to use a custom unit, please set the "unitName" property in the config.');
|
|
78
|
+
}
|
|
79
|
+
async getRooms() {
|
|
80
|
+
await this.initialize();
|
|
81
|
+
if (this.unitRequest !== null) {
|
|
82
|
+
return this.unitRequest;
|
|
83
|
+
}
|
|
84
|
+
this.unitRequest = this.transport.request(`/unit/${this.unit}/rooms`);
|
|
85
|
+
this.unitRequest.finally(() => {
|
|
86
|
+
this.unitRequest = null;
|
|
87
|
+
});
|
|
88
|
+
const rooms = await this.unitRequest;
|
|
89
|
+
this.buildFeatureIndex(rooms);
|
|
90
|
+
return rooms;
|
|
91
|
+
}
|
|
92
|
+
async getFeature(featureId) {
|
|
93
|
+
if (!this.featureIndex) {
|
|
94
|
+
await this.getRooms();
|
|
95
|
+
}
|
|
96
|
+
return this.featureIndex?.get(featureId) ?? null;
|
|
97
|
+
}
|
|
98
|
+
buildFeatureIndex(rooms) {
|
|
99
|
+
this.featureIndex = new Map();
|
|
100
|
+
for (const room of rooms) {
|
|
101
|
+
for (const device of room.devices) {
|
|
102
|
+
for (const feature of device.features) {
|
|
103
|
+
this.featureIndex.set(feature.id.toString(), feature);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async updateFeature(featureId, value) {
|
|
109
|
+
await this.transport.request(`/feature/${encodeURIComponent(featureId)}/value`, {
|
|
110
|
+
method: 'PUT',
|
|
111
|
+
body: JSON.stringify({ value }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async updateFeatureReliable(featureId, value) {
|
|
115
|
+
for (let attempt = 0; attempt < this.reliableUpdateAttempts; attempt++) {
|
|
116
|
+
await this.updateFeature(featureId, value);
|
|
117
|
+
if (attempt < this.reliableUpdateAttempts - 1) {
|
|
118
|
+
await this.delay(this.reliableUpdateDelayMs);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=iotasClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iotasClient.js","sourceRoot":"","sources":["../../../src/api/iotasClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether a JWT token has expired based on its `exp` claim.
|
|
3
|
+
*
|
|
4
|
+
* WARNING: This performs NO signature verification. It must NOT be used
|
|
5
|
+
* for authentication or authorization decisions.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isTokenExpired(token: string): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether a JWT token has expired based on its `exp` claim.
|
|
3
|
+
*
|
|
4
|
+
* WARNING: This performs NO signature verification. It must NOT be used
|
|
5
|
+
* for authentication or authorization decisions.
|
|
6
|
+
*/
|
|
7
|
+
export function isTokenExpired(token) {
|
|
8
|
+
const payload = token.split('.')[1];
|
|
9
|
+
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());
|
|
10
|
+
// Date.now() returns Unix time in milliseconds, while JWT exp is defined in seconds, hence the division by 1000.
|
|
11
|
+
return decoded.exp < Date.now() / 1000;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=jwt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../../../src/api/jwt.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,EAAE,CAAoB,CAAC;IAC5F,iHAAiH;IACjH,OAAO,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { IotasLogger } from '../logger.js';
|
|
2
|
+
import type { IotasTokens } from '../types.js';
|
|
3
|
+
export interface IotasSessionOptions {
|
|
4
|
+
log: IotasLogger;
|
|
5
|
+
email: string;
|
|
6
|
+
authenticate: () => Promise<{
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}>;
|
|
10
|
+
onTokensChanged?: (tokens: IotasTokens) => void;
|
|
11
|
+
initialTokens?: IotasTokens;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
authRetryDelaysMs?: number[];
|
|
14
|
+
/** Injected fetch function for testing. Default: globalThis.fetch */
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
/** Injected delay function for testing. Default: setTimeout-based sleep */
|
|
17
|
+
delay?: (ms: number) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Manages authentication and token lifecycle for the IOTAS API.
|
|
21
|
+
*
|
|
22
|
+
* Handles initial auth, token refresh, JWT expiry checks,
|
|
23
|
+
* retry with backoff, and request deduplication.
|
|
24
|
+
*/
|
|
25
|
+
export declare class IotasSession {
|
|
26
|
+
private token;
|
|
27
|
+
private refreshToken;
|
|
28
|
+
private authenticateRequest;
|
|
29
|
+
private readonly log;
|
|
30
|
+
private readonly email;
|
|
31
|
+
private readonly authenticateFn;
|
|
32
|
+
private readonly onTokensChanged?;
|
|
33
|
+
private readonly baseUrl;
|
|
34
|
+
private readonly authRetryDelaysMs;
|
|
35
|
+
private readonly fetch;
|
|
36
|
+
private readonly delay;
|
|
37
|
+
constructor(options: IotasSessionOptions);
|
|
38
|
+
getToken(): Promise<string>;
|
|
39
|
+
invalidateToken(): void;
|
|
40
|
+
private authenticate;
|
|
41
|
+
private authenticateWithRetry;
|
|
42
|
+
private refreshAccessToken;
|
|
43
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { isTokenExpired } from './jwt.js';
|
|
2
|
+
const DEFAULT_BASE_URL = 'https://api.iotashome.com/api/v1';
|
|
3
|
+
const DEFAULT_AUTH_RETRY_DELAYS_MS = [60_000, 300_000, 600_000];
|
|
4
|
+
const defaultDelay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
/**
|
|
6
|
+
* Manages authentication and token lifecycle for the IOTAS API.
|
|
7
|
+
*
|
|
8
|
+
* Handles initial auth, token refresh, JWT expiry checks,
|
|
9
|
+
* retry with backoff, and request deduplication.
|
|
10
|
+
*/
|
|
11
|
+
export class IotasSession {
|
|
12
|
+
token = null;
|
|
13
|
+
refreshToken = '';
|
|
14
|
+
authenticateRequest = null;
|
|
15
|
+
log;
|
|
16
|
+
email;
|
|
17
|
+
authenticateFn;
|
|
18
|
+
onTokensChanged;
|
|
19
|
+
baseUrl;
|
|
20
|
+
authRetryDelaysMs;
|
|
21
|
+
fetch;
|
|
22
|
+
delay;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.log = options.log;
|
|
25
|
+
this.email = options.email;
|
|
26
|
+
this.authenticateFn = options.authenticate;
|
|
27
|
+
this.onTokensChanged = options.onTokensChanged;
|
|
28
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
29
|
+
this.authRetryDelaysMs = options.authRetryDelaysMs ?? DEFAULT_AUTH_RETRY_DELAYS_MS;
|
|
30
|
+
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
31
|
+
this.delay = options.delay ?? defaultDelay;
|
|
32
|
+
if (options.initialTokens) {
|
|
33
|
+
this.token = options.initialTokens.jwt;
|
|
34
|
+
this.refreshToken = options.initialTokens.refresh;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async getToken() {
|
|
38
|
+
if (this.token === null) {
|
|
39
|
+
return this.authenticate();
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
if (isTokenExpired(this.token)) {
|
|
43
|
+
return this.refreshAccessToken();
|
|
44
|
+
}
|
|
45
|
+
return this.token;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return this.authenticate();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
invalidateToken() {
|
|
52
|
+
this.token = null;
|
|
53
|
+
}
|
|
54
|
+
async authenticate() {
|
|
55
|
+
if (this.authenticateRequest !== null) {
|
|
56
|
+
return this.authenticateRequest;
|
|
57
|
+
}
|
|
58
|
+
this.authenticateRequest = this.authenticateWithRetry();
|
|
59
|
+
this.authenticateRequest.finally(() => {
|
|
60
|
+
this.authenticateRequest = null;
|
|
61
|
+
});
|
|
62
|
+
return this.authenticateRequest;
|
|
63
|
+
}
|
|
64
|
+
async authenticateWithRetry() {
|
|
65
|
+
let lastError;
|
|
66
|
+
const maxRetries = this.authRetryDelaysMs.length;
|
|
67
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
const { username, password } = await this.authenticateFn();
|
|
70
|
+
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
71
|
+
const response = await this.fetch(`${this.baseUrl}/auth/tokenwithrefresh`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Basic ${credentials}`,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({}),
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
const data = (await response.json());
|
|
83
|
+
this.refreshToken = data.refresh;
|
|
84
|
+
this.token = data.jwt;
|
|
85
|
+
this.onTokensChanged?.({ jwt: this.token, refresh: this.refreshToken });
|
|
86
|
+
return this.token;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
lastError = error;
|
|
90
|
+
if (attempt >= maxRetries) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const delayMs = this.authRetryDelaysMs[attempt];
|
|
94
|
+
this.log.error('Authentication error:', error instanceof Error ? error.message : String(error));
|
|
95
|
+
this.log.warn(`Authentication retry ${attempt + 1}/${maxRetries} scheduled in ${Math.round(delayMs / 1000)} seconds.`);
|
|
96
|
+
await this.delay(delayMs);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Authentication failed after ${maxRetries + 1} attempts: ${String(lastError)}`);
|
|
100
|
+
}
|
|
101
|
+
async refreshAccessToken() {
|
|
102
|
+
if (this.authenticateRequest !== null) {
|
|
103
|
+
return this.authenticateRequest;
|
|
104
|
+
}
|
|
105
|
+
this.authenticateRequest = (async () => {
|
|
106
|
+
try {
|
|
107
|
+
const response = await this.fetch(`${this.baseUrl}/auth/refresh`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
refresh: this.refreshToken,
|
|
114
|
+
email: this.email,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
119
|
+
}
|
|
120
|
+
const data = (await response.json());
|
|
121
|
+
this.token = data.jwt;
|
|
122
|
+
this.onTokensChanged?.({ jwt: this.token, refresh: this.refreshToken });
|
|
123
|
+
return this.token;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
this.log.error('Token refresh error:', error instanceof Error ? error.message : String(error));
|
|
127
|
+
this.authenticateRequest = null;
|
|
128
|
+
return this.authenticateWithRetry();
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
this.authenticateRequest.finally(() => {
|
|
132
|
+
this.authenticateRequest = null;
|
|
133
|
+
});
|
|
134
|
+
return this.authenticateRequest;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/api/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IotasSession } from './session.js';
|
|
2
|
+
export interface IotasTransportOptions {
|
|
3
|
+
session: IotasSession;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
maxRequestAuthRetries?: number;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Authenticated HTTP transport for the IOTAS API.
|
|
10
|
+
*
|
|
11
|
+
* Handles bearer token injection, 401 retry with token invalidation,
|
|
12
|
+
* and empty response body parsing.
|
|
13
|
+
*/
|
|
14
|
+
export declare class IotasTransport {
|
|
15
|
+
private readonly session;
|
|
16
|
+
private readonly baseUrl;
|
|
17
|
+
private readonly maxRequestAuthRetries;
|
|
18
|
+
private readonly fetch;
|
|
19
|
+
constructor(options: IotasTransportOptions);
|
|
20
|
+
request<T>(path: string, options?: RequestInit, authRetryCount?: number): Promise<T>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = 'https://api.iotashome.com/api/v1';
|
|
2
|
+
const DEFAULT_MAX_REQUEST_AUTH_RETRIES = 1;
|
|
3
|
+
/**
|
|
4
|
+
* Authenticated HTTP transport for the IOTAS API.
|
|
5
|
+
*
|
|
6
|
+
* Handles bearer token injection, 401 retry with token invalidation,
|
|
7
|
+
* and empty response body parsing.
|
|
8
|
+
*/
|
|
9
|
+
export class IotasTransport {
|
|
10
|
+
session;
|
|
11
|
+
baseUrl;
|
|
12
|
+
maxRequestAuthRetries;
|
|
13
|
+
fetch;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.session = options.session;
|
|
16
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
17
|
+
this.maxRequestAuthRetries = options.maxRequestAuthRetries ?? DEFAULT_MAX_REQUEST_AUTH_RETRIES;
|
|
18
|
+
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
19
|
+
}
|
|
20
|
+
async request(path, options = {}, authRetryCount = 0) {
|
|
21
|
+
const token = await this.session.getToken();
|
|
22
|
+
const headers = new Headers(options.headers);
|
|
23
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
24
|
+
if (!headers.has('Content-Type')) {
|
|
25
|
+
headers.set('Content-Type', 'application/json');
|
|
26
|
+
}
|
|
27
|
+
const response = await this.fetch(`${this.baseUrl}${path}`, {
|
|
28
|
+
...options,
|
|
29
|
+
headers,
|
|
30
|
+
});
|
|
31
|
+
if (response.status === 401) {
|
|
32
|
+
if (authRetryCount >= this.maxRequestAuthRetries) {
|
|
33
|
+
throw new Error('API request failed: unauthorized after token refresh retry');
|
|
34
|
+
}
|
|
35
|
+
this.session.invalidateToken();
|
|
36
|
+
return this.request(path, options, authRetryCount + 1);
|
|
37
|
+
}
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
// Handle empty responses (e.g., from PUT/DELETE requests)
|
|
42
|
+
const text = await response.text();
|
|
43
|
+
if (!text) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../../src/api/transport.ts"],"names":[],"mappings":"AAEA,MAAM,gBAAgB,GAAG,kCAAkC,CAAC;AAC5D,MAAM,gCAAgC,GAAG,CAAC,CAAC;AAS3C;;;;;GAKG;AACH,MAAM,OAAO,cAAc;IACR,OAAO,CAAe;IACtB,OAAO,CAAS;IAChB,qBAAqB,CAAS;IAC9B,KAAK,CAA0B;IAEhD,YAAY,OAA8B;QACxC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC;QACnD,IAAI,CAAC,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,IAAI,gCAAgC,CAAC;QAC/F,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAClE,CAAC;IAED,KAAK,CAAC,OAAO,CAAI,IAAY,EAAE,UAAuB,EAAE,EAAE,cAAc,GAAG,CAAC;QAC1E,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QAE5C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,KAAK,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QAClD,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;YAC1D,GAAG,OAAO;YACV,OAAO;SACR,CAAC,CAAC;QAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,IAAI,cAAc,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;gBACjD,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;YAChF,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,CAAC,CAAC,CAAC;QACzD,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QACnF,CAAC;QAED,0DAA0D;QAC1D,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,SAAc,CAAC;QACxB,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { IotasLogger } from '../logger.js';
|
|
2
|
+
import type { IotasClient } from '../api/iotasClient.js';
|
|
3
|
+
import type { FeatureCacheOptions, Rooms } from '../types.js';
|
|
4
|
+
export type FeatureChangeCallback = (changed: Map<string, number>) => void;
|
|
5
|
+
/**
|
|
6
|
+
* FeatureCache provides cached feature values with background polling.
|
|
7
|
+
*
|
|
8
|
+
* - Seeded from discovery snapshot (no cold-start defaults)
|
|
9
|
+
* - Single getRooms() call per poll cycle
|
|
10
|
+
* - Write barrier prevents stale poll data overwriting recent onSet
|
|
11
|
+
* - Subscription model for pushing updates via updateValue
|
|
12
|
+
*/
|
|
13
|
+
export declare class FeatureCache {
|
|
14
|
+
private readonly log;
|
|
15
|
+
private readonly client;
|
|
16
|
+
private readonly values;
|
|
17
|
+
private readonly writeTimestamps;
|
|
18
|
+
private readonly subscriptions;
|
|
19
|
+
private pollTimer;
|
|
20
|
+
private consecutiveFailures;
|
|
21
|
+
private stopped;
|
|
22
|
+
private readonly pollIntervalMs;
|
|
23
|
+
private readonly writeBarrierMs;
|
|
24
|
+
private readonly pollBackoffBaseMs;
|
|
25
|
+
private readonly pollBackoffMaxMs;
|
|
26
|
+
constructor(log: IotasLogger, client: IotasClient, options?: FeatureCacheOptions);
|
|
27
|
+
seed(rooms: Rooms): void;
|
|
28
|
+
get(featureId: string): number | undefined;
|
|
29
|
+
set(featureId: string, value: number): void;
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to feature changes. Callback is invoked when any of the
|
|
32
|
+
* specified features are updated by the poller.
|
|
33
|
+
* Returns a dispose function to unsubscribe.
|
|
34
|
+
*/
|
|
35
|
+
subscribe(featureIds: string[], callback: FeatureChangeCallback): () => void;
|
|
36
|
+
start(): void;
|
|
37
|
+
stop(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Reset for a fresh start (used when re-discovering devices).
|
|
40
|
+
*
|
|
41
|
+
* This clears all cached values, write timestamps, and subscriptions.
|
|
42
|
+
* Callers must re-register subscriptions after calling reset().
|
|
43
|
+
*
|
|
44
|
+
* Expected call order:
|
|
45
|
+
* 1) reset()
|
|
46
|
+
* 2) seed(rooms)
|
|
47
|
+
* 3) recreate accessory runtimes (which re-subscribe)
|
|
48
|
+
* 4) start()
|
|
49
|
+
*/
|
|
50
|
+
reset(): void;
|
|
51
|
+
private schedulePoll;
|
|
52
|
+
private poll;
|
|
53
|
+
private updateFromSnapshot;
|
|
54
|
+
private notifySubscribers;
|
|
55
|
+
}
|