@glasshome/sync-layer 0.1.10 → 0.1.12
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/connection/manager.d.ts +7 -0
- package/dist/core/types.d.ts +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +96 -2
- package/package.json +3 -3
|
@@ -36,6 +36,13 @@ export declare function getConnection(): Connection | null;
|
|
|
36
36
|
* Check if currently connected
|
|
37
37
|
*/
|
|
38
38
|
export declare function isConnected(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Get the underlying socket's connected flag.
|
|
41
|
+
*
|
|
42
|
+
* Exposed so apps can render a connection indicator in the UI. Distinct from
|
|
43
|
+
* `isConnected()` which reflects the manager's internal lifecycle state.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getConnectionState(): boolean;
|
|
39
46
|
/**
|
|
40
47
|
* Refresh all data
|
|
41
48
|
*/
|
package/dist/core/types.d.ts
CHANGED
|
@@ -175,8 +175,13 @@ export interface EntityView {
|
|
|
175
175
|
domain: EntityDomain;
|
|
176
176
|
/** Current state value */
|
|
177
177
|
state: string;
|
|
178
|
-
/**
|
|
179
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Entity attributes from Home Assistant, excluding keys that are
|
|
180
|
+
* surfaced as canonical resolved fields elsewhere on EntityView
|
|
181
|
+
* (`deviceClass`, `unitOfMeasurement`, `friendlyName`, `icon`).
|
|
182
|
+
* Use those fields instead of reaching into `attributes`.
|
|
183
|
+
*/
|
|
184
|
+
attributes: Omit<Record<string, any>, "device_class" | "unit_of_measurement" | "friendly_name" | "icon">;
|
|
180
185
|
lastChanged: Date;
|
|
181
186
|
lastUpdated: Date;
|
|
182
187
|
/** Event context */
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { addDebugIncomingMessageListener, addDebugOutgoingMessageListener, } from "./connection/adapter";
|
|
10
10
|
export type { ConnectionOptions } from "./connection/manager";
|
|
11
|
-
export { disconnect, getConnection, initConnection, isConnected } from "./connection/manager";
|
|
11
|
+
export { disconnect, getConnection, getConnectionState, initConnection, isConnected, } from "./connection/manager";
|
|
12
12
|
export { registerEntity } from "./connection/subscription-manager";
|
|
13
13
|
export type { SyncLayerConnection } from "./connection/types";
|
|
14
14
|
export type { OAuthOptions } from "./connection/auth";
|
package/dist/index.js
CHANGED
|
@@ -1154,6 +1154,85 @@ function applyStateDiff(s, entityId, diff) {
|
|
|
1154
1154
|
|
|
1155
1155
|
//#endregion
|
|
1156
1156
|
//#region src/connection/manager.ts
|
|
1157
|
+
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
1158
|
+
const HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
1159
|
+
const SUSPEND_DELAY_MS = 5 * 60 * 1e3;
|
|
1160
|
+
let heartbeatInterval = null;
|
|
1161
|
+
let suspendTimeout = null;
|
|
1162
|
+
let visiblePromiseResolve = null;
|
|
1163
|
+
let visibilityHandler = null;
|
|
1164
|
+
let pageshowHandler = null;
|
|
1165
|
+
function promiseTimeout(ms, promise) {
|
|
1166
|
+
return new Promise((resolve, reject) => {
|
|
1167
|
+
const timer = setTimeout(() => reject(new Error("Promise timeout")), ms);
|
|
1168
|
+
promise.then((value) => {
|
|
1169
|
+
clearTimeout(timer);
|
|
1170
|
+
resolve(value);
|
|
1171
|
+
}, (err) => {
|
|
1172
|
+
clearTimeout(timer);
|
|
1173
|
+
reject(err);
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
function clearSuspendTimeout() {
|
|
1178
|
+
if (suspendTimeout !== null) {
|
|
1179
|
+
clearTimeout(suspendTimeout);
|
|
1180
|
+
suspendTimeout = null;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function resolveVisiblePromise() {
|
|
1184
|
+
if (visiblePromiseResolve) {
|
|
1185
|
+
visiblePromiseResolve();
|
|
1186
|
+
visiblePromiseResolve = null;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function startHeartbeat(conn$1) {
|
|
1190
|
+
stopHeartbeat();
|
|
1191
|
+
heartbeatInterval = setInterval(() => {
|
|
1192
|
+
promiseTimeout(HEARTBEAT_TIMEOUT_MS, conn$1.ping()).catch(() => {
|
|
1193
|
+
conn$1.reconnect();
|
|
1194
|
+
});
|
|
1195
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
1196
|
+
}
|
|
1197
|
+
function stopHeartbeat() {
|
|
1198
|
+
if (heartbeatInterval !== null) {
|
|
1199
|
+
clearInterval(heartbeatInterval);
|
|
1200
|
+
heartbeatInterval = null;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function setupLifecycleListeners(conn$1) {
|
|
1204
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
1205
|
+
teardownLifecycleListeners();
|
|
1206
|
+
visibilityHandler = () => {
|
|
1207
|
+
if (document.visibilityState === "hidden") {
|
|
1208
|
+
const pending = new Promise((resolve) => {
|
|
1209
|
+
visiblePromiseResolve = resolve;
|
|
1210
|
+
});
|
|
1211
|
+
conn$1.suspendReconnectUntil(pending);
|
|
1212
|
+
clearSuspendTimeout();
|
|
1213
|
+
suspendTimeout = setTimeout(() => {
|
|
1214
|
+
conn$1.suspend();
|
|
1215
|
+
suspendTimeout = null;
|
|
1216
|
+
}, SUSPEND_DELAY_MS);
|
|
1217
|
+
} else {
|
|
1218
|
+
clearSuspendTimeout();
|
|
1219
|
+
resolveVisiblePromise();
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
pageshowHandler = (event) => {
|
|
1223
|
+
if (event.persisted) conn$1.reconnect();
|
|
1224
|
+
};
|
|
1225
|
+
document.addEventListener("visibilitychange", visibilityHandler);
|
|
1226
|
+
window.addEventListener("pageshow", pageshowHandler);
|
|
1227
|
+
}
|
|
1228
|
+
function teardownLifecycleListeners() {
|
|
1229
|
+
if (typeof document !== "undefined" && visibilityHandler) document.removeEventListener("visibilitychange", visibilityHandler);
|
|
1230
|
+
if (typeof window !== "undefined" && pageshowHandler) window.removeEventListener("pageshow", pageshowHandler);
|
|
1231
|
+
visibilityHandler = null;
|
|
1232
|
+
pageshowHandler = null;
|
|
1233
|
+
clearSuspendTimeout();
|
|
1234
|
+
resolveVisiblePromise();
|
|
1235
|
+
}
|
|
1157
1236
|
/**
|
|
1158
1237
|
* Initialize connection to Home Assistant
|
|
1159
1238
|
*/
|
|
@@ -1180,6 +1259,8 @@ async function initConnection(options) {
|
|
|
1180
1259
|
setupEventHandlers(conn$1, options);
|
|
1181
1260
|
await loadInitialData(conn$1);
|
|
1182
1261
|
await subscribeToUpdates(wrappedConn);
|
|
1262
|
+
setupLifecycleListeners(conn$1);
|
|
1263
|
+
startHeartbeat(conn$1);
|
|
1183
1264
|
options.onConnect?.();
|
|
1184
1265
|
return conn$1;
|
|
1185
1266
|
} catch (error) {
|
|
@@ -1197,6 +1278,8 @@ async function initConnection(options) {
|
|
|
1197
1278
|
*/
|
|
1198
1279
|
function disconnect() {
|
|
1199
1280
|
const conn$1 = state.conn;
|
|
1281
|
+
stopHeartbeat();
|
|
1282
|
+
teardownLifecycleListeners();
|
|
1200
1283
|
if (conn$1) conn$1.close();
|
|
1201
1284
|
setState(produce((s) => {
|
|
1202
1285
|
s.conn = null;
|
|
@@ -1215,6 +1298,16 @@ function getConnection() {
|
|
|
1215
1298
|
function isConnected() {
|
|
1216
1299
|
return state.connectionState === "connected";
|
|
1217
1300
|
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Get the underlying socket's connected flag.
|
|
1303
|
+
*
|
|
1304
|
+
* Exposed so apps can render a connection indicator in the UI. Distinct from
|
|
1305
|
+
* `isConnected()` which reflects the manager's internal lifecycle state.
|
|
1306
|
+
*/
|
|
1307
|
+
function getConnectionState() {
|
|
1308
|
+
const conn$1 = state.conn;
|
|
1309
|
+
return conn$1 ? conn$1.connected : false;
|
|
1310
|
+
}
|
|
1218
1311
|
function setupEventHandlers(conn$1, options) {
|
|
1219
1312
|
conn$1.addEventListener("disconnected", () => {
|
|
1220
1313
|
setState(produce((s) => {
|
|
@@ -1304,6 +1397,7 @@ function buildEntityView(entity$1, registry) {
|
|
|
1304
1397
|
const friendlyName = entity$1.attributes.friendly_name ?? registry?.name ?? entity$1.entity_id;
|
|
1305
1398
|
const name = registry?.name ?? friendlyName;
|
|
1306
1399
|
const iconResolution = resolveIcon(entity$1, registry);
|
|
1400
|
+
const { device_class: _dc, unit_of_measurement: _uom, friendly_name: _fn, icon: _icon,...restAttributes } = entity$1.attributes;
|
|
1307
1401
|
let areaId = registry?.area_id ?? null;
|
|
1308
1402
|
if (areaId === null && registry?.device_id) {
|
|
1309
1403
|
const device = state.devices[registry.device_id];
|
|
@@ -1313,7 +1407,7 @@ function buildEntityView(entity$1, registry) {
|
|
|
1313
1407
|
id: entity$1.entity_id,
|
|
1314
1408
|
domain,
|
|
1315
1409
|
state: entity$1.state,
|
|
1316
|
-
attributes:
|
|
1410
|
+
attributes: restAttributes,
|
|
1317
1411
|
lastChanged: new Date(entity$1.last_changed),
|
|
1318
1412
|
lastUpdated: new Date(entity$1.last_updated),
|
|
1319
1413
|
context: {
|
|
@@ -2416,4 +2510,4 @@ async function refreshForecasts(entityId, types) {
|
|
|
2416
2510
|
const VERSION = "0.1.0";
|
|
2417
2511
|
|
|
2418
2512
|
//#endregion
|
|
2419
|
-
export { DOMAINS_USE_LAST_UPDATED, LINE_ATTRIBUTES_TO_KEEP, NEED_ATTRIBUTE_DOMAINS, VERSION, addDebugIncomingMessageListener, addDebugOutgoingMessageListener, appendHistoryPoint, applyDemoServiceCall, area, areas, authenticateWithOAuth, authenticateWithToken, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, bulkAppendHistoryPoints, callService, createDemoFixtures, disconnect, entities, entitiesByAreaAndDomain, entitiesWhere, entity, entityIdHistoryNeedsAttributes, entityViewEquals, extractDomain, fetchEntityHistory, fetchForecast, fetchForecastData, fetchForecasts, fetchHistory, fetchStream, fetchStreamData, getAllEntityViews, getAreaView, getAreaViews, getConnection, getEntityView, getEntityViews, getForecast, getForecasts, getStream, getWebRtcClientConfig, historyStateToTimeline, initConnection, isAuthValid, isConnected, isDemoMode, isHistoryTracked, loadDemoData, refreshAuth, refreshForecast, refreshForecasts, refreshStream, registerEntity, resetStore, searchEntities, sendCommand, sendWebRtcCandidate, setState, startWebRtcSession, state, toggle, trackEntityHistory, turnOff, turnOn, unloadDemoData, untrackEntityHistory, updateEntity };
|
|
2513
|
+
export { DOMAINS_USE_LAST_UPDATED, LINE_ATTRIBUTES_TO_KEEP, NEED_ATTRIBUTE_DOMAINS, VERSION, addDebugIncomingMessageListener, addDebugOutgoingMessageListener, appendHistoryPoint, applyDemoServiceCall, area, areas, authenticateWithOAuth, authenticateWithToken, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, bulkAppendHistoryPoints, callService, createDemoFixtures, disconnect, entities, entitiesByAreaAndDomain, entitiesWhere, entity, entityIdHistoryNeedsAttributes, entityViewEquals, extractDomain, fetchEntityHistory, fetchForecast, fetchForecastData, fetchForecasts, fetchHistory, fetchStream, fetchStreamData, getAllEntityViews, getAreaView, getAreaViews, getConnection, getConnectionState, getEntityView, getEntityViews, getForecast, getForecasts, getStream, getWebRtcClientConfig, historyStateToTimeline, initConnection, isAuthValid, isConnected, isDemoMode, isHistoryTracked, loadDemoData, refreshAuth, refreshForecast, refreshForecasts, refreshStream, registerEntity, resetStore, searchEntities, sendCommand, sendWebRtcCandidate, setState, startWebRtcSession, state, toggle, trackEntityHistory, turnOff, turnOn, unloadDemoData, untrackEntityHistory, updateEntity };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/sync-layer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Type-safe, reactive state synchronization layer for Home Assistant",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"./package.json": "./package.json"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@glasshome/ha-types": "0.1.
|
|
32
|
+
"@glasshome/ha-types": "0.1.1",
|
|
33
33
|
"solid-js": "^1.9.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"home-assistant-js-websocket": "^9.6.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@glasshome/ha-types": "0.1.
|
|
56
|
+
"@glasshome/ha-types": "0.1.1",
|
|
57
57
|
"@types/bun": "latest",
|
|
58
58
|
"solid-js": "^1.9.0",
|
|
59
59
|
"tsdown": "^0.2.0",
|