@glasshome/sync-layer 0.1.9 → 0.1.11
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/subscription-manager.d.ts +3 -0
- package/dist/core/store.d.ts +25 -0
- package/dist/{demo-data-zwOczByr.js → demo-data-D1ofmMa4.js} +2 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +163 -67
- package/dist/solid/index.js +1 -2
- package/dist/testing/index.js +1 -1
- package/package.json +3 -3
- package/dist/subscription-manager-gR0qpDYZ.js +0 -96
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
import type { EntityId } from "../core/types";
|
|
11
11
|
import type { SyncLayerConnection } from "./types";
|
|
12
12
|
type UnsubscribeFn = () => Promise<void> | void;
|
|
13
|
+
declare global {
|
|
14
|
+
var __GH_SYNC_LAYER_SUB_MGR__: boolean | undefined;
|
|
15
|
+
}
|
|
13
16
|
/**
|
|
14
17
|
* Register an entity for live updates. Returns an unregister function.
|
|
15
18
|
* Call from useEntity/useEntities hooks via onCleanup.
|
package/dist/core/store.d.ts
CHANGED
|
@@ -46,6 +46,31 @@ export interface GlassHomeState {
|
|
|
46
46
|
/** Camera stream data (keyed by entity ID) */
|
|
47
47
|
streams: Record<EntityId, CameraStreamData>;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Module-level SolidJS createStore singleton
|
|
51
|
+
*
|
|
52
|
+
* `state` is a read-only reactive proxy.
|
|
53
|
+
* `setState` is used for mutations via produce() or direct path setting.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { state, setState } from '@glasshome/sync-layer/core/store';
|
|
58
|
+
*
|
|
59
|
+
* // Read state (auto-tracked in SolidJS reactive contexts)
|
|
60
|
+
* const entities = state.entities;
|
|
61
|
+
*
|
|
62
|
+
* // Mutate via produce
|
|
63
|
+
* setState(produce((s) => {
|
|
64
|
+
* s.entities['light.living_room'] = newEntity;
|
|
65
|
+
* }));
|
|
66
|
+
*
|
|
67
|
+
* // Bulk replace
|
|
68
|
+
* setState("entities", reconcile(newEntities));
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare global {
|
|
72
|
+
var __GH_SYNC_LAYER_STORE__: boolean | undefined;
|
|
73
|
+
}
|
|
49
74
|
export declare const state: Store<GlassHomeState>;
|
|
50
75
|
export declare const setState: SetStoreFunction<GlassHomeState>;
|
|
51
76
|
/**
|
|
@@ -18,28 +18,8 @@ const initialState = {
|
|
|
18
18
|
forecasts: {},
|
|
19
19
|
streams: {}
|
|
20
20
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
* `state` is a read-only reactive proxy.
|
|
25
|
-
* `setState` is used for mutations via produce() or direct path setting.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```typescript
|
|
29
|
-
* import { state, setState } from '@glasshome/sync-layer/core/store';
|
|
30
|
-
*
|
|
31
|
-
* // Read state (auto-tracked in SolidJS reactive contexts)
|
|
32
|
-
* const entities = state.entities;
|
|
33
|
-
*
|
|
34
|
-
* // Mutate via produce
|
|
35
|
-
* setState(produce((s) => {
|
|
36
|
-
* s.entities['light.living_room'] = newEntity;
|
|
37
|
-
* }));
|
|
38
|
-
*
|
|
39
|
-
* // Bulk replace
|
|
40
|
-
* setState("entities", reconcile(newEntities));
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
21
|
+
if (globalThis.__GH_SYNC_LAYER_STORE__) console.error("[@glasshome/sync-layer] DUPLICATE store module instantiated. This means @glasshome/sync-layer was bundled into >1 chunk. Reads and writes will hit different singletons. Check vendor build externals and relative-vs-bare imports across entry points.");
|
|
22
|
+
globalThis.__GH_SYNC_LAYER_STORE__ = true;
|
|
43
23
|
const storeTuple = createStore(initialState);
|
|
44
24
|
const state = storeTuple[0];
|
|
45
25
|
const setState = storeTuple[1];
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export { addDebugIncomingMessageListener, addDebugOutgoingMessageListener, } from "./connection/adapter";
|
|
10
10
|
export type { ConnectionOptions } from "./connection/manager";
|
|
11
11
|
export { disconnect, getConnection, initConnection, isConnected } from "./connection/manager";
|
|
12
|
+
export { registerEntity } from "./connection/subscription-manager";
|
|
12
13
|
export type { SyncLayerConnection } from "./connection/types";
|
|
13
14
|
export type { OAuthOptions } from "./connection/auth";
|
|
14
15
|
export { authenticateWithOAuth, authenticateWithToken, isAuthValid, refreshAuth, } from "./connection/auth";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { createDemoFixtures, resetStore, setState, state } from "./demo-data-
|
|
2
|
-
import { forceResubscribe, setManagerConnection, setResubscribeHandler } from "./subscription-manager-gR0qpDYZ.js";
|
|
1
|
+
import { createDemoFixtures, resetStore, setState, state } from "./demo-data-D1ofmMa4.js";
|
|
3
2
|
import { callService as callService$1, createConnection, createLongLivedTokenAuth, getAuth } from "home-assistant-js-websocket";
|
|
4
3
|
import { produce, reconcile } from "solid-js/store";
|
|
5
4
|
|
|
@@ -63,16 +62,16 @@ function notifyIncomingListeners(message) {
|
|
|
63
62
|
* await syncLayer.connect({ connection: wrappedConn });
|
|
64
63
|
* ```
|
|
65
64
|
*/
|
|
66
|
-
function wrapHAConnection(conn) {
|
|
65
|
+
function wrapHAConnection(conn$1) {
|
|
67
66
|
return {
|
|
68
67
|
connect: async () => {
|
|
69
68
|
return Promise.resolve();
|
|
70
69
|
},
|
|
71
70
|
disconnect: () => {
|
|
72
|
-
conn.close();
|
|
71
|
+
conn$1.close();
|
|
73
72
|
},
|
|
74
73
|
get connected$() {
|
|
75
|
-
return conn.connected;
|
|
74
|
+
return conn$1.connected;
|
|
76
75
|
},
|
|
77
76
|
authenticate: async () => {
|
|
78
77
|
return Promise.resolve();
|
|
@@ -85,18 +84,18 @@ function wrapHAConnection(conn) {
|
|
|
85
84
|
notifyIncomingListeners(event);
|
|
86
85
|
callback(event);
|
|
87
86
|
};
|
|
88
|
-
return conn.subscribeEvents(wrappedCallback, eventType);
|
|
87
|
+
return conn$1.subscribeEvents(wrappedCallback, eventType);
|
|
89
88
|
},
|
|
90
89
|
subscribeMessage: async (callback, message) => {
|
|
91
90
|
const wrappedCallback = (msg) => {
|
|
92
91
|
notifyIncomingListeners(msg);
|
|
93
92
|
callback(msg);
|
|
94
93
|
};
|
|
95
|
-
return conn.subscribeMessage(wrappedCallback, message);
|
|
94
|
+
return conn$1.subscribeMessage(wrappedCallback, message);
|
|
96
95
|
},
|
|
97
96
|
sendMessagePromise: async (message) => {
|
|
98
97
|
notifyOutgoingListeners(message);
|
|
99
|
-
const result = await conn.sendMessagePromise(message);
|
|
98
|
+
const result = await conn$1.sendMessagePromise(message);
|
|
100
99
|
notifyIncomingListeners(result);
|
|
101
100
|
return result;
|
|
102
101
|
},
|
|
@@ -395,6 +394,103 @@ async function refreshAuth(auth) {
|
|
|
395
394
|
return auth;
|
|
396
395
|
}
|
|
397
396
|
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/connection/subscription-manager.ts
|
|
399
|
+
if (globalThis.__GH_SYNC_LAYER_SUB_MGR__) console.error("[@glasshome/sync-layer] DUPLICATE subscription-manager module instantiated. registerEntity() and the connection setter will hit different singletons → subscribe_entities will never be sent. Cause: relative import of subscription-manager from a separate entry point.");
|
|
400
|
+
globalThis.__GH_SYNC_LAYER_SUB_MGR__ = true;
|
|
401
|
+
/** Ref counts — how many consumers need each entity */
|
|
402
|
+
const refCounts = /* @__PURE__ */ new Map();
|
|
403
|
+
/** Current HA subscription unsub function */
|
|
404
|
+
let currentEntityUnsub = null;
|
|
405
|
+
/** Snapshot of entity IDs in the current HA subscription */
|
|
406
|
+
let subscribedIds = /* @__PURE__ */ new Set();
|
|
407
|
+
/** Connection reference for resubscribing */
|
|
408
|
+
let conn = null;
|
|
409
|
+
/** Pending flush timer (microtask for register, timeout for unregister) */
|
|
410
|
+
let flushScheduled = false;
|
|
411
|
+
/** Delayed unregister timeout */
|
|
412
|
+
let unregisterTimeout = null;
|
|
413
|
+
/** Callback invoked on resubscribe — set by subscriptions.ts */
|
|
414
|
+
let onResubscribe = null;
|
|
415
|
+
/** Delay before unregistered entities are actually removed from subscription */
|
|
416
|
+
const UNREGISTER_DELAY_MS = 5e3;
|
|
417
|
+
/** Guard against concurrent flush() calls */
|
|
418
|
+
let flushInFlight = false;
|
|
419
|
+
/**
|
|
420
|
+
* Register an entity for live updates. Returns an unregister function.
|
|
421
|
+
* Call from useEntity/useEntities hooks via onCleanup.
|
|
422
|
+
*/
|
|
423
|
+
function registerEntity(entityId) {
|
|
424
|
+
refCounts.set(entityId, (refCounts.get(entityId) ?? 0) + 1);
|
|
425
|
+
scheduleFlush();
|
|
426
|
+
return () => unregisterEntity(entityId);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Set the connection reference used for resubscribing.
|
|
430
|
+
*/
|
|
431
|
+
function setManagerConnection(connection) {
|
|
432
|
+
conn = connection;
|
|
433
|
+
if (refCounts.size > 0) scheduleFlush();
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Set the resubscribe callback. Called by subscriptions.ts to wire
|
|
437
|
+
* the manager into the actual WS subscription lifecycle.
|
|
438
|
+
*/
|
|
439
|
+
function setResubscribeHandler(handler) {
|
|
440
|
+
onResubscribe = handler;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Force a resubscribe with the current active set.
|
|
444
|
+
* Used on reconnect.
|
|
445
|
+
*/
|
|
446
|
+
async function forceResubscribe() {
|
|
447
|
+
await flush();
|
|
448
|
+
}
|
|
449
|
+
function unregisterEntity(entityId) {
|
|
450
|
+
const count = (refCounts.get(entityId) ?? 1) - 1;
|
|
451
|
+
if (count <= 0) refCounts.delete(entityId);
|
|
452
|
+
else refCounts.set(entityId, count);
|
|
453
|
+
scheduleDelayedFlush();
|
|
454
|
+
}
|
|
455
|
+
function scheduleFlush() {
|
|
456
|
+
if (flushScheduled) return;
|
|
457
|
+
flushScheduled = true;
|
|
458
|
+
queueMicrotask(() => {
|
|
459
|
+
flushScheduled = false;
|
|
460
|
+
flush();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function scheduleDelayedFlush() {
|
|
464
|
+
if (unregisterTimeout) return;
|
|
465
|
+
unregisterTimeout = setTimeout(() => {
|
|
466
|
+
unregisterTimeout = null;
|
|
467
|
+
flush();
|
|
468
|
+
}, UNREGISTER_DELAY_MS);
|
|
469
|
+
}
|
|
470
|
+
async function flush() {
|
|
471
|
+
if (!conn || !onResubscribe) return;
|
|
472
|
+
if (flushInFlight) return;
|
|
473
|
+
flushInFlight = true;
|
|
474
|
+
try {
|
|
475
|
+
const newIds = new Set(refCounts.keys());
|
|
476
|
+
if (setsEqual(newIds, subscribedIds)) return;
|
|
477
|
+
const oldUnsub = currentEntityUnsub;
|
|
478
|
+
const entityIds = [...newIds];
|
|
479
|
+
currentEntityUnsub = await onResubscribe(conn, entityIds);
|
|
480
|
+
subscribedIds = newIds;
|
|
481
|
+
if (oldUnsub) try {
|
|
482
|
+
await oldUnsub();
|
|
483
|
+
} catch {}
|
|
484
|
+
} finally {
|
|
485
|
+
flushInFlight = false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function setsEqual(a, b) {
|
|
489
|
+
if (a.size !== b.size) return false;
|
|
490
|
+
for (const id of a) if (!b.has(id)) return false;
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
|
|
398
494
|
//#endregion
|
|
399
495
|
//#region src/core/types.ts
|
|
400
496
|
/**
|
|
@@ -835,31 +931,31 @@ async function subscribeEntities(connection, entityIds) {
|
|
|
835
931
|
/**
|
|
836
932
|
* Subscribe to all relevant events from Home Assistant
|
|
837
933
|
*/
|
|
838
|
-
async function subscribeToUpdates(conn) {
|
|
934
|
+
async function subscribeToUpdates(conn$1) {
|
|
839
935
|
await cleanupSubscriptions();
|
|
840
|
-
setManagerConnection(conn);
|
|
936
|
+
setManagerConnection(conn$1);
|
|
841
937
|
setResubscribeHandler(subscribeEntities);
|
|
842
|
-
const registryUpdatedSub = await conn.subscribeEvents((event) => {
|
|
938
|
+
const registryUpdatedSub = await conn$1.subscribeEvents((event) => {
|
|
843
939
|
handleRegistryUpdate(event);
|
|
844
940
|
}, "entity_registry_updated");
|
|
845
941
|
registrySubscriptions.push(() => registryUpdatedSub());
|
|
846
|
-
const deviceRegistrySub = await conn.subscribeEvents((event) => {
|
|
942
|
+
const deviceRegistrySub = await conn$1.subscribeEvents((event) => {
|
|
847
943
|
handleDeviceRegistryUpdate(event);
|
|
848
944
|
}, "device_registry_updated");
|
|
849
945
|
registrySubscriptions.push(() => deviceRegistrySub());
|
|
850
|
-
const areaRegistrySub = await conn.subscribeEvents((event) => {
|
|
946
|
+
const areaRegistrySub = await conn$1.subscribeEvents((event) => {
|
|
851
947
|
handleAreaRegistryUpdate(event);
|
|
852
948
|
}, "area_registry_updated");
|
|
853
949
|
registrySubscriptions.push(() => areaRegistrySub());
|
|
854
|
-
const floorRegistrySub = await conn.subscribeEvents((event) => {
|
|
950
|
+
const floorRegistrySub = await conn$1.subscribeEvents((event) => {
|
|
855
951
|
handleFloorRegistryUpdate(event);
|
|
856
952
|
}, "floor_registry_updated");
|
|
857
953
|
registrySubscriptions.push(() => floorRegistrySub());
|
|
858
|
-
const labelRegistrySub = await conn.subscribeEvents((event) => {
|
|
954
|
+
const labelRegistrySub = await conn$1.subscribeEvents((event) => {
|
|
859
955
|
handleLabelRegistryUpdate(event);
|
|
860
956
|
}, "label_registry_updated");
|
|
861
957
|
registrySubscriptions.push(() => labelRegistrySub());
|
|
862
|
-
const statisticsSub = await conn.subscribeEvents((event) => {
|
|
958
|
+
const statisticsSub = await conn$1.subscribeEvents((event) => {
|
|
863
959
|
handleStatisticsUpdate(event);
|
|
864
960
|
}, "recorder_5min_statistics_generated");
|
|
865
961
|
registrySubscriptions.push(() => statisticsSub());
|
|
@@ -875,10 +971,10 @@ async function cleanupSubscriptions() {
|
|
|
875
971
|
async function handleRegistryUpdate(event) {
|
|
876
972
|
const { action, entity_id } = event.data;
|
|
877
973
|
if (action === "create" || action === "update") {
|
|
878
|
-
const conn = state.conn;
|
|
879
|
-
if (!conn) return;
|
|
974
|
+
const conn$1 = state.conn;
|
|
975
|
+
if (!conn$1) return;
|
|
880
976
|
try {
|
|
881
|
-
const entry = await conn.sendMessagePromise({
|
|
977
|
+
const entry = await conn$1.sendMessagePromise({
|
|
882
978
|
type: "config/entity_registry/get",
|
|
883
979
|
entity_id
|
|
884
980
|
});
|
|
@@ -891,10 +987,10 @@ async function handleRegistryUpdate(event) {
|
|
|
891
987
|
async function handleDeviceRegistryUpdate(event) {
|
|
892
988
|
const { action, device_id } = event.data;
|
|
893
989
|
if (action === "create" || action === "update") {
|
|
894
|
-
const conn = state.conn;
|
|
895
|
-
if (!conn) return;
|
|
990
|
+
const conn$1 = state.conn;
|
|
991
|
+
if (!conn$1) return;
|
|
896
992
|
try {
|
|
897
|
-
const devices = await conn.sendMessagePromise({ type: "config/device_registry/list" });
|
|
993
|
+
const devices = await conn$1.sendMessagePromise({ type: "config/device_registry/list" });
|
|
898
994
|
const device = devices.find((d) => d.id === device_id);
|
|
899
995
|
if (device) updateDevice(device);
|
|
900
996
|
} catch (error) {
|
|
@@ -905,10 +1001,10 @@ async function handleDeviceRegistryUpdate(event) {
|
|
|
905
1001
|
async function handleAreaRegistryUpdate(event) {
|
|
906
1002
|
const { action, area_id } = event.data;
|
|
907
1003
|
if (action === "create" || action === "update") {
|
|
908
|
-
const conn = state.conn;
|
|
909
|
-
if (!conn) return;
|
|
1004
|
+
const conn$1 = state.conn;
|
|
1005
|
+
if (!conn$1) return;
|
|
910
1006
|
try {
|
|
911
|
-
const areas$1 = await conn.sendMessagePromise({ type: "config/area_registry/list" });
|
|
1007
|
+
const areas$1 = await conn$1.sendMessagePromise({ type: "config/area_registry/list" });
|
|
912
1008
|
const area$1 = areas$1.find((a) => a.id === area_id);
|
|
913
1009
|
if (area$1) updateArea(area$1);
|
|
914
1010
|
} catch (error) {
|
|
@@ -919,10 +1015,10 @@ async function handleAreaRegistryUpdate(event) {
|
|
|
919
1015
|
async function handleFloorRegistryUpdate(event) {
|
|
920
1016
|
const { action, floor_id } = event.data;
|
|
921
1017
|
if (action === "create" || action === "update") {
|
|
922
|
-
const conn = state.conn;
|
|
923
|
-
if (!conn) return;
|
|
1018
|
+
const conn$1 = state.conn;
|
|
1019
|
+
if (!conn$1) return;
|
|
924
1020
|
try {
|
|
925
|
-
const floors = await conn.sendMessagePromise({ type: "config/floor_registry/list" });
|
|
1021
|
+
const floors = await conn$1.sendMessagePromise({ type: "config/floor_registry/list" });
|
|
926
1022
|
const floor = floors.find((f) => f.floor_id === floor_id);
|
|
927
1023
|
if (floor) updateFloor(floor);
|
|
928
1024
|
} catch (error) {
|
|
@@ -933,10 +1029,10 @@ async function handleFloorRegistryUpdate(event) {
|
|
|
933
1029
|
async function handleLabelRegistryUpdate(event) {
|
|
934
1030
|
const { action, label_id } = event.data;
|
|
935
1031
|
if (action === "create" || action === "update") {
|
|
936
|
-
const conn = state.conn;
|
|
937
|
-
if (!conn) return;
|
|
1032
|
+
const conn$1 = state.conn;
|
|
1033
|
+
if (!conn$1) return;
|
|
938
1034
|
try {
|
|
939
|
-
const labels = await conn.sendMessagePromise({ type: "config/label_registry/list" });
|
|
1035
|
+
const labels = await conn$1.sendMessagePromise({ type: "config/label_registry/list" });
|
|
940
1036
|
const label = labels.find((l) => l.label_id === label_id);
|
|
941
1037
|
if (label) updateLabel(label);
|
|
942
1038
|
} catch (error) {
|
|
@@ -947,10 +1043,10 @@ async function handleLabelRegistryUpdate(event) {
|
|
|
947
1043
|
async function handleStatisticsUpdate(event) {
|
|
948
1044
|
const { statistic_ids } = event.data;
|
|
949
1045
|
if (!statistic_ids || statistic_ids.length === 0) return;
|
|
950
|
-
const conn = state.conn;
|
|
951
|
-
if (!conn) return;
|
|
1046
|
+
const conn$1 = state.conn;
|
|
1047
|
+
if (!conn$1) return;
|
|
952
1048
|
try {
|
|
953
|
-
const metadata = await conn.sendMessagePromise({
|
|
1049
|
+
const metadata = await conn$1.sendMessagePromise({
|
|
954
1050
|
type: "recorder/get_statistics_metadata",
|
|
955
1051
|
statistic_ids
|
|
956
1052
|
});
|
|
@@ -1071,21 +1167,21 @@ async function initConnection(options) {
|
|
|
1071
1167
|
if (typeof options.auth === "string") auth = authenticateWithToken(options.url, options.auth);
|
|
1072
1168
|
else if (typeof options.auth === "object" && "clientId" in options.auth) auth = await authenticateWithOAuth(options.auth);
|
|
1073
1169
|
else auth = options.auth;
|
|
1074
|
-
const conn = await createConnection({
|
|
1170
|
+
const conn$1 = await createConnection({
|
|
1075
1171
|
auth,
|
|
1076
1172
|
...options.createSocket ? { createSocket: options.createSocket } : {}
|
|
1077
1173
|
});
|
|
1078
|
-
const wrappedConn = wrapHAConnection(conn);
|
|
1174
|
+
const wrappedConn = wrapHAConnection(conn$1);
|
|
1079
1175
|
setState(produce((s) => {
|
|
1080
|
-
s.conn = conn;
|
|
1176
|
+
s.conn = conn$1;
|
|
1081
1177
|
s.hassUrl = options.url.replace(/\/$/, "");
|
|
1082
1178
|
s.connectionState = "connected";
|
|
1083
1179
|
}));
|
|
1084
|
-
setupEventHandlers(conn, options);
|
|
1085
|
-
await loadInitialData(conn);
|
|
1180
|
+
setupEventHandlers(conn$1, options);
|
|
1181
|
+
await loadInitialData(conn$1);
|
|
1086
1182
|
await subscribeToUpdates(wrappedConn);
|
|
1087
1183
|
options.onConnect?.();
|
|
1088
|
-
return conn;
|
|
1184
|
+
return conn$1;
|
|
1089
1185
|
} catch (error) {
|
|
1090
1186
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1091
1187
|
setState(produce((s) => {
|
|
@@ -1100,8 +1196,8 @@ async function initConnection(options) {
|
|
|
1100
1196
|
* Disconnect from Home Assistant
|
|
1101
1197
|
*/
|
|
1102
1198
|
function disconnect() {
|
|
1103
|
-
const conn = state.conn;
|
|
1104
|
-
if (conn) conn.close();
|
|
1199
|
+
const conn$1 = state.conn;
|
|
1200
|
+
if (conn$1) conn$1.close();
|
|
1105
1201
|
setState(produce((s) => {
|
|
1106
1202
|
s.conn = null;
|
|
1107
1203
|
s.connectionState = "disconnected";
|
|
@@ -1119,40 +1215,40 @@ function getConnection() {
|
|
|
1119
1215
|
function isConnected() {
|
|
1120
1216
|
return state.connectionState === "connected";
|
|
1121
1217
|
}
|
|
1122
|
-
function setupEventHandlers(conn, options) {
|
|
1123
|
-
conn.addEventListener("disconnected", () => {
|
|
1218
|
+
function setupEventHandlers(conn$1, options) {
|
|
1219
|
+
conn$1.addEventListener("disconnected", () => {
|
|
1124
1220
|
setState(produce((s) => {
|
|
1125
1221
|
s.connectionState = "disconnected";
|
|
1126
1222
|
}));
|
|
1127
1223
|
options.onDisconnect?.();
|
|
1128
1224
|
});
|
|
1129
|
-
conn.addEventListener("reconnect-error", () => {
|
|
1225
|
+
conn$1.addEventListener("reconnect-error", () => {
|
|
1130
1226
|
setState(produce((s) => {
|
|
1131
1227
|
s.connectionState = "reconnecting";
|
|
1132
1228
|
}));
|
|
1133
1229
|
});
|
|
1134
|
-
conn.addEventListener("ready", async () => {
|
|
1230
|
+
conn$1.addEventListener("ready", async () => {
|
|
1135
1231
|
const wasReconnecting = state.connectionState === "reconnecting";
|
|
1136
1232
|
setState(produce((s) => {
|
|
1137
1233
|
s.connectionState = "connected";
|
|
1138
1234
|
s.connectionError = null;
|
|
1139
1235
|
}));
|
|
1140
1236
|
if (wasReconnecting) {
|
|
1141
|
-
await loadInitialData(conn);
|
|
1237
|
+
await loadInitialData(conn$1);
|
|
1142
1238
|
await forceResubscribe();
|
|
1143
1239
|
options.onReconnect?.();
|
|
1144
1240
|
}
|
|
1145
1241
|
});
|
|
1146
1242
|
}
|
|
1147
|
-
async function loadInitialData(conn) {
|
|
1243
|
+
async function loadInitialData(conn$1) {
|
|
1148
1244
|
try {
|
|
1149
1245
|
const [states, entityRegistry, deviceRegistry, areaRegistry, floorRegistry, labelRegistry] = await Promise.all([
|
|
1150
|
-
conn.sendMessagePromise({ type: "get_states" }),
|
|
1151
|
-
conn.sendMessagePromise({ type: "config/entity_registry/list" }),
|
|
1152
|
-
conn.sendMessagePromise({ type: "config/device_registry/list" }),
|
|
1153
|
-
conn.sendMessagePromise({ type: "config/area_registry/list" }),
|
|
1154
|
-
conn.sendMessagePromise({ type: "config/floor_registry/list" }),
|
|
1155
|
-
conn.sendMessagePromise({ type: "config/label_registry/list" })
|
|
1246
|
+
conn$1.sendMessagePromise({ type: "get_states" }),
|
|
1247
|
+
conn$1.sendMessagePromise({ type: "config/entity_registry/list" }),
|
|
1248
|
+
conn$1.sendMessagePromise({ type: "config/device_registry/list" }),
|
|
1249
|
+
conn$1.sendMessagePromise({ type: "config/area_registry/list" }),
|
|
1250
|
+
conn$1.sendMessagePromise({ type: "config/floor_registry/list" }),
|
|
1251
|
+
conn$1.sendMessagePromise({ type: "config/label_registry/list" })
|
|
1156
1252
|
]);
|
|
1157
1253
|
bulkUpdateEntities(states);
|
|
1158
1254
|
bulkUpdateEntityRegistry(entityRegistry);
|
|
@@ -1980,9 +2076,9 @@ async function refreshStream(entityId, format = "hls") {
|
|
|
1980
2076
|
* Get WebRTC client configuration (STUN/TURN servers) from HA
|
|
1981
2077
|
*/
|
|
1982
2078
|
async function getWebRtcClientConfig(entityId) {
|
|
1983
|
-
const conn = state.conn;
|
|
1984
|
-
if (!conn) throw new Error("No connection available");
|
|
1985
|
-
return conn.sendMessagePromise({
|
|
2079
|
+
const conn$1 = state.conn;
|
|
2080
|
+
if (!conn$1) throw new Error("No connection available");
|
|
2081
|
+
return conn$1.sendMessagePromise({
|
|
1986
2082
|
type: "camera/webrtc/get_client_config",
|
|
1987
2083
|
entity_id: entityId
|
|
1988
2084
|
});
|
|
@@ -1994,8 +2090,8 @@ async function getWebRtcClientConfig(entityId) {
|
|
|
1994
2090
|
* Local ICE candidates should be sent via sendWebRtcCandidate().
|
|
1995
2091
|
*/
|
|
1996
2092
|
function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
1997
|
-
const conn = state.conn;
|
|
1998
|
-
if (!conn) return Promise.reject(new Error("Not connected"));
|
|
2093
|
+
const conn$1 = state.conn;
|
|
2094
|
+
if (!conn$1) return Promise.reject(new Error("Not connected"));
|
|
1999
2095
|
return new Promise((resolve, reject) => {
|
|
2000
2096
|
const session = {
|
|
2001
2097
|
sessionId: null,
|
|
@@ -2008,7 +2104,7 @@ function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
|
2008
2104
|
reject(new Error("WebRTC offer timed out"));
|
|
2009
2105
|
}
|
|
2010
2106
|
}, 15e3);
|
|
2011
|
-
conn.subscribeMessage((event) => {
|
|
2107
|
+
conn$1.subscribeMessage((event) => {
|
|
2012
2108
|
switch (event.type) {
|
|
2013
2109
|
case "session":
|
|
2014
2110
|
session.sessionId = event.session_id ?? null;
|
|
@@ -2048,9 +2144,9 @@ function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
|
2048
2144
|
* Send a local ICE candidate to HA
|
|
2049
2145
|
*/
|
|
2050
2146
|
async function sendWebRtcCandidate(entityId, sessionId, candidate) {
|
|
2051
|
-
const conn = state.conn;
|
|
2052
|
-
if (!conn) throw new Error("Not connected");
|
|
2053
|
-
await conn.sendMessagePromise({
|
|
2147
|
+
const conn$1 = state.conn;
|
|
2148
|
+
if (!conn$1) throw new Error("Not connected");
|
|
2149
|
+
await conn$1.sendMessagePromise({
|
|
2054
2150
|
type: "camera/webrtc/candidate",
|
|
2055
2151
|
entity_id: entityId,
|
|
2056
2152
|
session_id: sessionId,
|
|
@@ -2105,8 +2201,8 @@ function buildDemoForecast(entityId, type) {
|
|
|
2105
2201
|
*/
|
|
2106
2202
|
async function fetchForecast(entityId, type) {
|
|
2107
2203
|
if (isDemoMode()) return buildDemoForecast(entityId, type);
|
|
2108
|
-
const conn = state.conn;
|
|
2109
|
-
if (!conn) throw new Error("Not connected");
|
|
2204
|
+
const conn$1 = state.conn;
|
|
2205
|
+
if (!conn$1) throw new Error("Not connected");
|
|
2110
2206
|
return new Promise((resolve, reject) => {
|
|
2111
2207
|
let resolved = false;
|
|
2112
2208
|
let unsubscribe = null;
|
|
@@ -2116,7 +2212,7 @@ async function fetchForecast(entityId, type) {
|
|
|
2116
2212
|
unsubscribe().then(() => reject(new Error("Forecast subscription timeout"))).catch(() => reject(new Error("Forecast subscription timeout")));
|
|
2117
2213
|
}
|
|
2118
2214
|
}, 1e4);
|
|
2119
|
-
conn.subscribeMessage((event) => {
|
|
2215
|
+
conn$1.subscribeMessage((event) => {
|
|
2120
2216
|
if (resolved) return;
|
|
2121
2217
|
if (event && typeof event === "object" && "forecast" in event) {
|
|
2122
2218
|
resolved = true;
|
|
@@ -2320,4 +2416,4 @@ async function refreshForecasts(entityId, types) {
|
|
|
2320
2416
|
const VERSION = "0.1.0";
|
|
2321
2417
|
|
|
2322
2418
|
//#endregion
|
|
2323
|
-
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, resetStore, searchEntities, sendCommand, sendWebRtcCandidate, setState, startWebRtcSession, state, toggle, trackEntityHistory, turnOff, turnOn, unloadDemoData, untrackEntityHistory, updateEntity };
|
|
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 };
|
package/dist/solid/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { registerEntity } from "../subscription-manager-gR0qpDYZ.js";
|
|
2
1
|
import { createEffect, createMemo, onCleanup } from "solid-js";
|
|
3
|
-
import { buildAreaView, callService, entityViewEquals, extractDomain, getEntityView, state, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
|
|
2
|
+
import { buildAreaView, callService, entityViewEquals, extractDomain, getEntityView, registerEntity, state, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
|
|
4
3
|
|
|
5
4
|
//#region src/solid/hooks.ts
|
|
6
5
|
/**
|
package/dist/testing/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/sync-layer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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",
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
//#region src/connection/subscription-manager.ts
|
|
2
|
-
/** Ref counts — how many consumers need each entity */
|
|
3
|
-
const refCounts = /* @__PURE__ */ new Map();
|
|
4
|
-
/** Current HA subscription unsub function */
|
|
5
|
-
let currentEntityUnsub = null;
|
|
6
|
-
/** Snapshot of entity IDs in the current HA subscription */
|
|
7
|
-
let subscribedIds = /* @__PURE__ */ new Set();
|
|
8
|
-
/** Connection reference for resubscribing */
|
|
9
|
-
let conn = null;
|
|
10
|
-
/** Pending flush timer (microtask for register, timeout for unregister) */
|
|
11
|
-
let flushScheduled = false;
|
|
12
|
-
/** Delayed unregister timeout */
|
|
13
|
-
let unregisterTimeout = null;
|
|
14
|
-
/** Callback invoked on resubscribe — set by subscriptions.ts */
|
|
15
|
-
let onResubscribe = null;
|
|
16
|
-
/** Delay before unregistered entities are actually removed from subscription */
|
|
17
|
-
const UNREGISTER_DELAY_MS = 5e3;
|
|
18
|
-
/** Guard against concurrent flush() calls */
|
|
19
|
-
let flushInFlight = false;
|
|
20
|
-
/**
|
|
21
|
-
* Register an entity for live updates. Returns an unregister function.
|
|
22
|
-
* Call from useEntity/useEntities hooks via onCleanup.
|
|
23
|
-
*/
|
|
24
|
-
function registerEntity(entityId) {
|
|
25
|
-
refCounts.set(entityId, (refCounts.get(entityId) ?? 0) + 1);
|
|
26
|
-
scheduleFlush();
|
|
27
|
-
return () => unregisterEntity(entityId);
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Set the connection reference used for resubscribing.
|
|
31
|
-
*/
|
|
32
|
-
function setManagerConnection(connection) {
|
|
33
|
-
conn = connection;
|
|
34
|
-
if (refCounts.size > 0) scheduleFlush();
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Set the resubscribe callback. Called by subscriptions.ts to wire
|
|
38
|
-
* the manager into the actual WS subscription lifecycle.
|
|
39
|
-
*/
|
|
40
|
-
function setResubscribeHandler(handler) {
|
|
41
|
-
onResubscribe = handler;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Force a resubscribe with the current active set.
|
|
45
|
-
* Used on reconnect.
|
|
46
|
-
*/
|
|
47
|
-
async function forceResubscribe() {
|
|
48
|
-
await flush();
|
|
49
|
-
}
|
|
50
|
-
function unregisterEntity(entityId) {
|
|
51
|
-
const count = (refCounts.get(entityId) ?? 1) - 1;
|
|
52
|
-
if (count <= 0) refCounts.delete(entityId);
|
|
53
|
-
else refCounts.set(entityId, count);
|
|
54
|
-
scheduleDelayedFlush();
|
|
55
|
-
}
|
|
56
|
-
function scheduleFlush() {
|
|
57
|
-
if (flushScheduled) return;
|
|
58
|
-
flushScheduled = true;
|
|
59
|
-
queueMicrotask(() => {
|
|
60
|
-
flushScheduled = false;
|
|
61
|
-
flush();
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
function scheduleDelayedFlush() {
|
|
65
|
-
if (unregisterTimeout) return;
|
|
66
|
-
unregisterTimeout = setTimeout(() => {
|
|
67
|
-
unregisterTimeout = null;
|
|
68
|
-
flush();
|
|
69
|
-
}, UNREGISTER_DELAY_MS);
|
|
70
|
-
}
|
|
71
|
-
async function flush() {
|
|
72
|
-
if (!conn || !onResubscribe) return;
|
|
73
|
-
if (flushInFlight) return;
|
|
74
|
-
flushInFlight = true;
|
|
75
|
-
try {
|
|
76
|
-
const newIds = new Set(refCounts.keys());
|
|
77
|
-
if (setsEqual(newIds, subscribedIds)) return;
|
|
78
|
-
const oldUnsub = currentEntityUnsub;
|
|
79
|
-
const entityIds = [...newIds];
|
|
80
|
-
currentEntityUnsub = await onResubscribe(conn, entityIds);
|
|
81
|
-
subscribedIds = newIds;
|
|
82
|
-
if (oldUnsub) try {
|
|
83
|
-
await oldUnsub();
|
|
84
|
-
} catch {}
|
|
85
|
-
} finally {
|
|
86
|
-
flushInFlight = false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function setsEqual(a, b) {
|
|
90
|
-
if (a.size !== b.size) return false;
|
|
91
|
-
for (const id of a) if (!b.has(id)) return false;
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
//#endregion
|
|
96
|
-
export { forceResubscribe, registerEntity, setManagerConnection, setResubscribeHandler };
|