@smarthome.exchange/integrations 1.0.0 → 2.0.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/changelog.md +49 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/classes.configstore.d.ts +2 -0
- package/dist_ts/core/classes.configstore.js +84 -27
- package/dist_ts/core/classes.nativehomeadmission.d.ts +19 -0
- package/dist_ts/core/classes.nativehomeadmission.js +84 -0
- package/dist_ts/core/classes.nativeintegrationowner.d.ts +79 -0
- package/dist_ts/core/classes.nativeintegrationowner.js +269 -0
- package/dist_ts/core/index.d.ts +2 -0
- package/dist_ts/core/index.js +3 -1
- package/dist_ts/core/types.d.ts +14 -0
- package/dist_ts/integrations/braviatv/braviatv.classes.client.d.ts +23 -72
- package/dist_ts/integrations/braviatv/braviatv.classes.client.js +265 -625
- package/dist_ts/integrations/braviatv/braviatv.classes.configflow.d.ts +7 -7
- package/dist_ts/integrations/braviatv/braviatv.classes.configflow.js +51 -68
- package/dist_ts/integrations/braviatv/braviatv.classes.integration.d.ts +8 -11
- package/dist_ts/integrations/braviatv/braviatv.classes.integration.js +186 -135
- package/dist_ts/integrations/braviatv/braviatv.discovery.js +12 -11
- package/dist_ts/integrations/braviatv/braviatv.mapper.d.ts +6 -14
- package/dist_ts/integrations/braviatv/braviatv.mapper.js +37 -131
- package/dist_ts/integrations/braviatv/braviatv.types.d.ts +55 -133
- package/dist_ts/integrations/braviatv/braviatv.validation.d.ts +7 -0
- package/dist_ts/integrations/braviatv/braviatv.validation.js +72 -0
- package/dist_ts/integrations/broadlink/broadlink.actions.d.ts +2 -0
- package/dist_ts/integrations/broadlink/broadlink.actions.js +36 -0
- package/dist_ts/integrations/broadlink/broadlink.classes.client.d.ts +21 -19
- package/dist_ts/integrations/broadlink/broadlink.classes.client.js +185 -124
- package/dist_ts/integrations/broadlink/broadlink.classes.configflow.d.ts +5 -4
- package/dist_ts/integrations/broadlink/broadlink.classes.configflow.js +64 -83
- package/dist_ts/integrations/broadlink/broadlink.classes.integration.d.ts +16 -19
- package/dist_ts/integrations/broadlink/broadlink.classes.integration.js +114 -59
- package/dist_ts/integrations/broadlink/broadlink.mapper.d.ts +3 -2
- package/dist_ts/integrations/broadlink/broadlink.mapper.js +2 -1
- package/dist_ts/integrations/broadlink/broadlink.selected.mapper.d.ts +10 -0
- package/dist_ts/integrations/broadlink/broadlink.selected.mapper.js +41 -0
- package/dist_ts/integrations/broadlink/broadlink.types.d.ts +43 -7
- package/dist_ts/integrations/broadlink/broadlink.validation.d.ts +7 -0
- package/dist_ts/integrations/broadlink/broadlink.validation.js +63 -0
- package/dist_ts/integrations/daikin/daikin.classes.client.d.ts +10 -52
- package/dist_ts/integrations/daikin/daikin.classes.client.js +55 -614
- package/dist_ts/integrations/daikin/daikin.classes.configflow.d.ts +8 -12
- package/dist_ts/integrations/daikin/daikin.classes.configflow.js +77 -138
- package/dist_ts/integrations/daikin/daikin.classes.integration.d.ts +7 -24
- package/dist_ts/integrations/daikin/daikin.classes.integration.js +175 -67
- package/dist_ts/integrations/daikin/daikin.discovery.d.ts +11 -24
- package/dist_ts/integrations/daikin/daikin.discovery.js +98 -305
- package/dist_ts/integrations/daikin/daikin.mapper.d.ts +6 -7
- package/dist_ts/integrations/daikin/daikin.mapper.js +95 -155
- package/dist_ts/integrations/daikin/daikin.types.d.ts +24 -44
- package/dist_ts/integrations/daikin/daikin.types.js +1 -1
- package/dist_ts/integrations/daikin/daikin.validation.d.ts +5 -0
- package/dist_ts/integrations/daikin/daikin.validation.js +53 -0
- package/dist_ts/integrations/denonavr/denonavr.classes.client.d.ts +31 -40
- package/dist_ts/integrations/denonavr/denonavr.classes.client.js +149 -404
- package/dist_ts/integrations/denonavr/denonavr.classes.configflow.d.ts +7 -2
- package/dist_ts/integrations/denonavr/denonavr.classes.configflow.js +44 -36
- package/dist_ts/integrations/denonavr/denonavr.classes.integration.d.ts +6 -11
- package/dist_ts/integrations/denonavr/denonavr.classes.integration.js +192 -113
- package/dist_ts/integrations/denonavr/denonavr.discovery.d.ts +10 -18
- package/dist_ts/integrations/denonavr/denonavr.discovery.js +71 -193
- package/dist_ts/integrations/denonavr/denonavr.mapper.d.ts +5 -13
- package/dist_ts/integrations/denonavr/denonavr.mapper.js +35 -159
- package/dist_ts/integrations/denonavr/denonavr.types.d.ts +55 -90
- package/dist_ts/integrations/denonavr/denonavr.validation.d.ts +10 -0
- package/dist_ts/integrations/denonavr/denonavr.validation.js +86 -0
- package/dist_ts/integrations/kodi/kodi.classes.client.d.ts +11 -45
- package/dist_ts/integrations/kodi/kodi.classes.client.js +84 -310
- package/dist_ts/integrations/kodi/kodi.classes.configflow.d.ts +9 -8
- package/dist_ts/integrations/kodi/kodi.classes.configflow.js +53 -48
- package/dist_ts/integrations/kodi/kodi.classes.integration.d.ts +11 -11
- package/dist_ts/integrations/kodi/kodi.classes.integration.js +114 -171
- package/dist_ts/integrations/kodi/kodi.discovery.js +53 -29
- package/dist_ts/integrations/kodi/kodi.mapper.d.ts +7 -2
- package/dist_ts/integrations/kodi/kodi.mapper.js +48 -10
- package/dist_ts/integrations/kodi/kodi.types.d.ts +32 -28
- package/dist_ts/integrations/kodi/kodi.validation.d.ts +9 -0
- package/dist_ts/integrations/kodi/kodi.validation.js +62 -0
- package/dist_ts/integrations/modbus/modbus.classes.client.d.ts +15 -61
- package/dist_ts/integrations/modbus/modbus.classes.client.js +117 -776
- package/dist_ts/integrations/modbus/modbus.classes.configflow.d.ts +9 -5
- package/dist_ts/integrations/modbus/modbus.classes.configflow.js +70 -38
- package/dist_ts/integrations/modbus/modbus.classes.integration.d.ts +6 -17
- package/dist_ts/integrations/modbus/modbus.classes.integration.js +133 -57
- package/dist_ts/integrations/modbus/modbus.discovery.d.ts +5 -8
- package/dist_ts/integrations/modbus/modbus.discovery.js +38 -93
- package/dist_ts/integrations/modbus/modbus.mapper.d.ts +8 -32
- package/dist_ts/integrations/modbus/modbus.mapper.js +71 -431
- package/dist_ts/integrations/modbus/modbus.types.d.ts +82 -332
- package/dist_ts/integrations/modbus/modbus.validation.d.ts +10 -0
- package/dist_ts/integrations/modbus/modbus.validation.js +128 -0
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.d.ts +12 -34
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.js +143 -231
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.js +105 -18
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.d.ts +8 -8
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.js +141 -155
- package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.d.ts +9 -0
- package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.js +35 -0
- package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.d.ts +6 -18
- package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.js +45 -220
- package/dist_ts/integrations/nanoleaf/nanoleaf.types.d.ts +33 -147
- package/dist_ts/integrations/nanoleaf/nanoleaf.validation.d.ts +6 -0
- package/dist_ts/integrations/nanoleaf/nanoleaf.validation.js +82 -0
- package/dist_ts/integrations/onkyo/onkyo.classes.client.d.ts +15 -20
- package/dist_ts/integrations/onkyo/onkyo.classes.client.js +161 -404
- package/dist_ts/integrations/onkyo/onkyo.classes.configflow.d.ts +9 -4
- package/dist_ts/integrations/onkyo/onkyo.classes.configflow.js +51 -6
- package/dist_ts/integrations/onkyo/onkyo.classes.integration.d.ts +22 -6
- package/dist_ts/integrations/onkyo/onkyo.classes.integration.js +144 -11
- package/dist_ts/integrations/onkyo/onkyo.discovery.d.ts +6 -1
- package/dist_ts/integrations/onkyo/onkyo.discovery.js +40 -4
- package/dist_ts/integrations/onkyo/onkyo.mapper.d.ts +7 -8
- package/dist_ts/integrations/onkyo/onkyo.mapper.js +45 -12
- package/dist_ts/integrations/onkyo/onkyo.types.d.ts +58 -8
- package/dist_ts/integrations/onkyo/onkyo.types.js +13 -102
- package/dist_ts/integrations/onkyo/onkyo.validation.d.ts +16 -0
- package/dist_ts/integrations/onkyo/onkyo.validation.js +77 -0
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.d.ts +25 -25
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.js +116 -360
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.d.ts +11 -3
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.js +76 -6
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.d.ts +26 -4
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.js +142 -9
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.d.ts +2 -1
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.js +31 -4
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.d.ts +9 -9
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.js +31 -149
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.d.ts +51 -11
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.js +22 -87
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.d.ts +6 -0
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.js +56 -0
- package/dist_ts/integrations/pioneer/pioneer.classes.client.d.ts +31 -16
- package/dist_ts/integrations/pioneer/pioneer.classes.client.js +167 -409
- package/dist_ts/integrations/pioneer/pioneer.classes.configflow.d.ts +10 -3
- package/dist_ts/integrations/pioneer/pioneer.classes.configflow.js +66 -6
- package/dist_ts/integrations/pioneer/pioneer.classes.integration.d.ts +13 -5
- package/dist_ts/integrations/pioneer/pioneer.classes.integration.js +239 -11
- package/dist_ts/integrations/pioneer/pioneer.discovery.d.ts +14 -1
- package/dist_ts/integrations/pioneer/pioneer.discovery.js +51 -4
- package/dist_ts/integrations/pioneer/pioneer.mapper.d.ts +14 -8
- package/dist_ts/integrations/pioneer/pioneer.mapper.js +54 -12
- package/dist_ts/integrations/pioneer/pioneer.types.d.ts +79 -12
- package/dist_ts/integrations/pioneer/pioneer.types.js +12 -76
- package/dist_ts/integrations/pioneer/pioneer.validation.d.ts +10 -0
- package/dist_ts/integrations/pioneer/pioneer.validation.js +85 -0
- package/dist_ts/integrations/soundtouch/soundtouch.classes.client.d.ts +13 -36
- package/dist_ts/integrations/soundtouch/soundtouch.classes.client.js +179 -502
- package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.js +50 -40
- package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.d.ts +14 -15
- package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.js +108 -128
- package/dist_ts/integrations/soundtouch/soundtouch.discovery.d.ts +2 -20
- package/dist_ts/integrations/soundtouch/soundtouch.discovery.js +23 -113
- package/dist_ts/integrations/soundtouch/soundtouch.mapper.d.ts +3 -12
- package/dist_ts/integrations/soundtouch/soundtouch.mapper.js +35 -189
- package/dist_ts/integrations/soundtouch/soundtouch.types.d.ts +57 -142
- package/dist_ts/integrations/soundtouch/soundtouch.types.js +18 -3
- package/dist_ts/integrations/soundtouch/soundtouch.validation.d.ts +22 -0
- package/dist_ts/integrations/soundtouch/soundtouch.validation.js +65 -0
- package/dist_ts/integrations/wiz/index.d.ts +1 -0
- package/dist_ts/integrations/wiz/index.js +2 -1
- package/dist_ts/integrations/wiz/wiz.classes.client.d.ts +14 -28
- package/dist_ts/integrations/wiz/wiz.classes.client.js +93 -229
- package/dist_ts/integrations/wiz/wiz.classes.configflow.d.ts +6 -4
- package/dist_ts/integrations/wiz/wiz.classes.configflow.js +38 -67
- package/dist_ts/integrations/wiz/wiz.classes.integration.d.ts +10 -21
- package/dist_ts/integrations/wiz/wiz.classes.integration.js +204 -44
- package/dist_ts/integrations/wiz/wiz.discovery.d.ts +10 -31
- package/dist_ts/integrations/wiz/wiz.discovery.js +48 -249
- package/dist_ts/integrations/wiz/wiz.mapper.d.ts +7 -51
- package/dist_ts/integrations/wiz/wiz.mapper.js +82 -668
- package/dist_ts/integrations/wiz/wiz.types.d.ts +34 -208
- package/dist_ts/integrations/wiz/wiz.validation.d.ts +8 -0
- package/dist_ts/integrations/wiz/wiz.validation.js +72 -0
- package/dist_ts/integrations/yamaha/index.d.ts +1 -0
- package/dist_ts/integrations/yamaha/index.js +2 -1
- package/dist_ts/integrations/yamaha/yamaha.classes.client.d.ts +30 -4
- package/dist_ts/integrations/yamaha/yamaha.classes.client.js +162 -6
- package/dist_ts/integrations/yamaha/yamaha.classes.configflow.d.ts +10 -3
- package/dist_ts/integrations/yamaha/yamaha.classes.configflow.js +106 -6
- package/dist_ts/integrations/yamaha/yamaha.classes.integration.d.ts +22 -4
- package/dist_ts/integrations/yamaha/yamaha.classes.integration.js +243 -8
- package/dist_ts/integrations/yamaha/yamaha.discovery.d.ts +13 -1
- package/dist_ts/integrations/yamaha/yamaha.discovery.js +57 -4
- package/dist_ts/integrations/yamaha/yamaha.mapper.d.ts +16 -8
- package/dist_ts/integrations/yamaha/yamaha.mapper.js +58 -12
- package/dist_ts/integrations/yamaha/yamaha.types.d.ts +73 -6
- package/dist_ts/integrations/yamaha/yamaha.types.js +8 -94
- package/dist_ts/integrations/yamaha/yamaha.validation.d.ts +12 -0
- package/dist_ts/integrations/yamaha/yamaha.validation.js +89 -0
- package/package.json +3 -3
- package/readme.md +514 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/classes.configstore.ts +84 -27
- package/ts/core/classes.nativehomeadmission.ts +96 -0
- package/ts/core/classes.nativeintegrationowner.ts +297 -0
- package/ts/core/index.ts +2 -0
- package/ts/core/types.ts +16 -0
- package/ts/integrations/braviatv/braviatv.classes.client.ts +245 -709
- package/ts/integrations/braviatv/braviatv.classes.configflow.ts +47 -76
- package/ts/integrations/braviatv/braviatv.classes.integration.ts +167 -147
- package/ts/integrations/braviatv/braviatv.discovery.ts +11 -10
- package/ts/integrations/braviatv/braviatv.mapper.ts +39 -142
- package/ts/integrations/braviatv/braviatv.types.ts +53 -173
- package/ts/integrations/braviatv/braviatv.validation.ts +68 -0
- package/ts/integrations/broadlink/broadlink.actions.ts +31 -0
- package/ts/integrations/broadlink/broadlink.classes.client.ts +170 -144
- package/ts/integrations/broadlink/broadlink.classes.configflow.ts +59 -89
- package/ts/integrations/broadlink/broadlink.classes.integration.ts +110 -64
- package/ts/integrations/broadlink/broadlink.mapper.ts +5 -4
- package/ts/integrations/broadlink/broadlink.selected.mapper.ts +46 -0
- package/ts/integrations/broadlink/broadlink.types.ts +48 -8
- package/ts/integrations/broadlink/broadlink.validation.ts +66 -0
- package/ts/integrations/daikin/daikin.classes.client.ts +52 -661
- package/ts/integrations/daikin/daikin.classes.configflow.ts +72 -152
- package/ts/integrations/daikin/daikin.classes.integration.ts +166 -71
- package/ts/integrations/daikin/daikin.discovery.ts +94 -346
- package/ts/integrations/daikin/daikin.mapper.ts +83 -166
- package/ts/integrations/daikin/daikin.types.ts +27 -48
- package/ts/integrations/daikin/daikin.validation.ts +51 -0
- package/ts/integrations/daikin/readme.md +83 -0
- package/ts/integrations/denonavr/denonavr.classes.client.ts +132 -421
- package/ts/integrations/denonavr/denonavr.classes.configflow.ts +44 -37
- package/ts/integrations/denonavr/denonavr.classes.integration.ts +171 -117
- package/ts/integrations/denonavr/denonavr.discovery.ts +67 -220
- package/ts/integrations/denonavr/denonavr.mapper.ts +36 -182
- package/ts/integrations/denonavr/denonavr.types.ts +48 -111
- package/ts/integrations/denonavr/denonavr.validation.ts +87 -0
- package/ts/integrations/kodi/kodi.classes.client.ts +72 -352
- package/ts/integrations/kodi/kodi.classes.configflow.ts +52 -55
- package/ts/integrations/kodi/kodi.classes.integration.ts +97 -178
- package/ts/integrations/kodi/kodi.discovery.ts +49 -28
- package/ts/integrations/kodi/kodi.mapper.ts +52 -10
- package/ts/integrations/kodi/kodi.types.ts +33 -58
- package/ts/integrations/kodi/kodi.validation.ts +60 -0
- package/ts/integrations/modbus/modbus.classes.client.ts +100 -842
- package/ts/integrations/modbus/modbus.classes.configflow.ts +70 -41
- package/ts/integrations/modbus/modbus.classes.integration.ts +115 -60
- package/ts/integrations/modbus/modbus.discovery.ts +38 -106
- package/ts/integrations/modbus/modbus.mapper.ts +64 -467
- package/ts/integrations/modbus/modbus.types.ts +64 -381
- package/ts/integrations/modbus/modbus.validation.ts +119 -0
- package/ts/integrations/nanoleaf/nanoleaf.classes.client.ts +115 -249
- package/ts/integrations/nanoleaf/nanoleaf.classes.configflow.ts +90 -19
- package/ts/integrations/nanoleaf/nanoleaf.classes.integration.ts +130 -181
- package/ts/integrations/nanoleaf/nanoleaf.credentials.ts +39 -0
- package/ts/integrations/nanoleaf/nanoleaf.mapper.ts +47 -260
- package/ts/integrations/nanoleaf/nanoleaf.types.ts +29 -147
- package/ts/integrations/nanoleaf/nanoleaf.validation.ts +77 -0
- package/ts/integrations/onkyo/onkyo.classes.client.ts +148 -457
- package/ts/integrations/onkyo/onkyo.classes.configflow.ts +52 -6
- package/ts/integrations/onkyo/onkyo.classes.integration.ts +121 -12
- package/ts/integrations/onkyo/onkyo.discovery.ts +41 -3
- package/ts/integrations/onkyo/onkyo.mapper.ts +46 -19
- package/ts/integrations/onkyo/onkyo.types.ts +55 -110
- package/ts/integrations/onkyo/onkyo.validation.ts +85 -0
- package/ts/integrations/onkyo/readme.md +126 -0
- package/ts/integrations/panasonic_viera/panasonic_viera.classes.client.ts +101 -398
- package/ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.ts +69 -5
- package/ts/integrations/panasonic_viera/panasonic_viera.classes.integration.ts +141 -9
- package/ts/integrations/panasonic_viera/panasonic_viera.discovery.ts +32 -3
- package/ts/integrations/panasonic_viera/panasonic_viera.mapper.ts +34 -161
- package/ts/integrations/panasonic_viera/panasonic_viera.types.ts +52 -96
- package/ts/integrations/panasonic_viera/panasonic_viera.validation.ts +56 -0
- package/ts/integrations/pioneer/pioneer.classes.client.ts +132 -428
- package/ts/integrations/pioneer/pioneer.classes.configflow.ts +67 -5
- package/ts/integrations/pioneer/pioneer.classes.integration.ts +225 -11
- package/ts/integrations/pioneer/pioneer.discovery.ts +45 -3
- package/ts/integrations/pioneer/pioneer.mapper.ts +57 -13
- package/ts/integrations/pioneer/pioneer.types.ts +87 -85
- package/ts/integrations/pioneer/pioneer.validation.ts +84 -0
- package/ts/integrations/soundtouch/soundtouch.classes.client.ts +152 -571
- package/ts/integrations/soundtouch/soundtouch.classes.configflow.ts +47 -42
- package/ts/integrations/soundtouch/soundtouch.classes.integration.ts +95 -150
- package/ts/integrations/soundtouch/soundtouch.discovery.ts +21 -133
- package/ts/integrations/soundtouch/soundtouch.mapper.ts +36 -207
- package/ts/integrations/soundtouch/soundtouch.types.ts +50 -195
- package/ts/integrations/soundtouch/soundtouch.validation.ts +60 -0
- package/ts/integrations/wiz/index.ts +1 -0
- package/ts/integrations/wiz/wiz.classes.client.ts +84 -254
- package/ts/integrations/wiz/wiz.classes.configflow.ts +39 -70
- package/ts/integrations/wiz/wiz.classes.integration.ts +189 -45
- package/ts/integrations/wiz/wiz.discovery.ts +48 -291
- package/ts/integrations/wiz/wiz.mapper.ts +81 -754
- package/ts/integrations/wiz/wiz.types.ts +24 -224
- package/ts/integrations/wiz/wiz.validation.ts +73 -0
- package/ts/integrations/yamaha/index.ts +1 -0
- package/ts/integrations/yamaha/yamaha.classes.client.ts +150 -6
- package/ts/integrations/yamaha/yamaha.classes.configflow.ts +100 -5
- package/ts/integrations/yamaha/yamaha.classes.integration.ts +236 -8
- package/ts/integrations/yamaha/yamaha.discovery.ts +52 -3
- package/ts/integrations/yamaha/yamaha.mapper.ts +65 -13
- package/ts/integrations/yamaha/yamaha.types.ts +77 -97
- package/ts/integrations/yamaha/yamaha.validation.ts +86 -0
|
@@ -28,6 +28,12 @@ export interface IConfigStoreInitializationOptions {
|
|
|
28
28
|
signal?: AbortSignal;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
interface IConfigStoreOperationState {
|
|
32
|
+
keyLookups: Set<Promise<void>>;
|
|
33
|
+
settled: boolean;
|
|
34
|
+
providerFailed: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
const validateName = (valueArg: string, nameArg: string, maxLengthArg: number): string => {
|
|
32
38
|
if (typeof valueArg !== 'string' || valueArg.length === 0 || valueArg.length > maxLengthArg || /[\p{Cc}\p{Cs}]/u.test(valueArg)) {
|
|
33
39
|
throw new TypeError(`${nameArg} must be a non-empty string without control characters, at most ${maxLengthArg} characters.`);
|
|
@@ -102,9 +108,13 @@ export class NoSqlConfigStore implements IConfigStore {
|
|
|
102
108
|
readonly #maxValueBytes: number;
|
|
103
109
|
readonly #lifetime = new AbortController();
|
|
104
110
|
readonly #preparations = new Set<Promise<void>>();
|
|
111
|
+
readonly #keyLookups = new Set<Promise<void>>();
|
|
112
|
+
readonly #retainedOperations = new Set<IConfigStoreOperationState>();
|
|
113
|
+
readonly #keyProviderFailures = new Set<Error>();
|
|
105
114
|
#operationTail: Promise<void> = Promise.resolve();
|
|
106
115
|
#pendingOperations = 0;
|
|
107
116
|
#closePromise?: Promise<void>;
|
|
117
|
+
#retryPromise?: Promise<void>;
|
|
108
118
|
|
|
109
119
|
constructor(optionsArg: INoSqlConfigStoreOptions) {
|
|
110
120
|
this.#homeId = validateName(optionsArg.homeId, 'homeId', 256);
|
|
@@ -156,19 +166,34 @@ export class NoSqlConfigStore implements IConfigStore {
|
|
|
156
166
|
await Promise.allSettled([
|
|
157
167
|
...this.#preparations,
|
|
158
168
|
this.#operationTail,
|
|
169
|
+
...this.#keyLookups,
|
|
159
170
|
]);
|
|
171
|
+
if (this.#keyProviderFailures.size) {
|
|
172
|
+
throw new AggregateError([...this.#keyProviderFailures], 'NoSqlConfigStore key-provider cleanup failed.');
|
|
173
|
+
}
|
|
160
174
|
});
|
|
161
175
|
this.#lifetime.abort(new Error('NoSqlConfigStore is closed.'));
|
|
162
176
|
}
|
|
163
177
|
return this.#closePromise;
|
|
164
178
|
}
|
|
165
179
|
|
|
180
|
+
/** Acknowledges settled provider cleanup failures without repeating callbacks or reopening the store. */
|
|
181
|
+
public retryCleanup(): Promise<void> {
|
|
182
|
+
if (!this.#closePromise) return Promise.reject(new Error('NoSqlConfigStore cleanup requires terminal close.'));
|
|
183
|
+
this.#retryPromise ??= Promise.resolve().then(async () => {
|
|
184
|
+
await this.#closePromise!.catch(() => undefined);
|
|
185
|
+
this.#keyProviderFailures.clear();
|
|
186
|
+
this.#retainedOperations.clear();
|
|
187
|
+
});
|
|
188
|
+
return this.#retryPromise;
|
|
189
|
+
}
|
|
190
|
+
|
|
166
191
|
public get<TValue>(keyArg: string, optionsArg: IConfigStoreOperationOptions = {}): Promise<TValue | undefined> {
|
|
167
192
|
const key = validateName(keyArg, 'config key', 1024);
|
|
168
193
|
const databaseOptions = this.#databaseOptions(optionsArg);
|
|
169
|
-
return this.#runOperation(async () => {
|
|
194
|
+
return this.#runOperation(async (operationArg) => {
|
|
170
195
|
const record = await this.#records.getInstance(this.#selector(key), databaseOptions);
|
|
171
|
-
return record ? this.#decrypt<TValue>(key, record.encryptedValue) : undefined;
|
|
196
|
+
return record ? this.#decrypt<TValue>(key, record.encryptedValue, operationArg) : undefined;
|
|
172
197
|
});
|
|
173
198
|
}
|
|
174
199
|
|
|
@@ -176,8 +201,9 @@ export class NoSqlConfigStore implements IConfigStore {
|
|
|
176
201
|
const key = validateName(keyArg, 'config key', 1024);
|
|
177
202
|
const serialized = this.#serialize(valueArg);
|
|
178
203
|
const databaseOptions = this.#databaseOptions(optionsArg);
|
|
179
|
-
return this.#runOperation(async () => {
|
|
180
|
-
const encryptedValue = await this.#encrypt(key, serialized);
|
|
204
|
+
return this.#runOperation(async (operationArg) => {
|
|
205
|
+
const encryptedValue = await this.#encrypt(key, serialized, operationArg);
|
|
206
|
+
this.#assertOpen();
|
|
181
207
|
await this.#records.atomicUpdate({ id: this.#selector(key).id }, {
|
|
182
208
|
$set: { homeId: this.#homeId, integrationDomain: this.#integrationDomain, key, encryptedValue },
|
|
183
209
|
}, { upsert: true, ...databaseOptions });
|
|
@@ -189,10 +215,11 @@ export class NoSqlConfigStore implements IConfigStore {
|
|
|
189
215
|
const key = validateName(keyArg, 'config key', 1024);
|
|
190
216
|
const serialized = this.#serialize(valueArg);
|
|
191
217
|
const databaseOptions = this.#databaseOptions(optionsArg);
|
|
192
|
-
return this.#runOperation(async () => {
|
|
218
|
+
return this.#runOperation(async (operationArg) => {
|
|
193
219
|
const record = Object.assign(new this.#records(), this.#selector(key), {
|
|
194
|
-
encryptedValue: await this.#encrypt(key, serialized),
|
|
220
|
+
encryptedValue: await this.#encrypt(key, serialized, operationArg),
|
|
195
221
|
});
|
|
222
|
+
this.#assertOpen();
|
|
196
223
|
await this.#records.insert(record, databaseOptions);
|
|
197
224
|
});
|
|
198
225
|
}
|
|
@@ -253,8 +280,9 @@ export class NoSqlConfigStore implements IConfigStore {
|
|
|
253
280
|
return serialized;
|
|
254
281
|
}
|
|
255
282
|
|
|
256
|
-
async #encrypt(keyArg: string, serializedArg: string): Promise<IEncryptedConfigValue> {
|
|
257
|
-
const encryptionKey = await this.#lookupKey((signalArg) => this.#keyProvider.currentKey(signalArg));
|
|
283
|
+
async #encrypt(keyArg: string, serializedArg: string, operationArg: IConfigStoreOperationState): Promise<IEncryptedConfigValue> {
|
|
284
|
+
const encryptionKey = await this.#lookupKey((signalArg) => this.#keyProvider.currentKey(signalArg), operationArg);
|
|
285
|
+
this.#assertOpen();
|
|
258
286
|
const keyId = validateName(encryptionKey.id, 'encryption key ID', 128);
|
|
259
287
|
const key = copyKey(encryptionKey.key);
|
|
260
288
|
try {
|
|
@@ -268,13 +296,15 @@ export class NoSqlConfigStore implements IConfigStore {
|
|
|
268
296
|
}
|
|
269
297
|
}
|
|
270
298
|
|
|
271
|
-
async #decrypt<TValue>(keyArg: string, valueArg: IEncryptedConfigValue): Promise<TValue> {
|
|
299
|
+
async #decrypt<TValue>(keyArg: string, valueArg: IEncryptedConfigValue, operationArg: IConfigStoreOperationState): Promise<TValue> {
|
|
272
300
|
if (!valueArg || valueArg.version !== 1) throw new Error('Stored config encryption envelope is invalid.');
|
|
273
301
|
const keyId = validateName(valueArg.keyId, 'stored encryption key ID', 128);
|
|
274
302
|
const nonce = decodeBase64(valueArg.nonce, 12, 12);
|
|
275
303
|
const tag = decodeBase64(valueArg.tag, 16, 16);
|
|
276
304
|
const ciphertext = decodeBase64(valueArg.ciphertext, this.#maxValueBytes);
|
|
277
|
-
const
|
|
305
|
+
const decryptionKey = await this.#lookupKey((signalArg) => this.#keyProvider.keyForId(keyId, signalArg), operationArg);
|
|
306
|
+
this.#assertOpen();
|
|
307
|
+
const key = copyKey(decryptionKey);
|
|
278
308
|
try {
|
|
279
309
|
const decipher = plugins.crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
|
280
310
|
decipher.setAAD(this.#aad(keyArg, keyId));
|
|
@@ -294,24 +324,46 @@ export class NoSqlConfigStore implements IConfigStore {
|
|
|
294
324
|
}
|
|
295
325
|
}
|
|
296
326
|
|
|
297
|
-
async #lookupKey<TResult>(lookupArg: (signalArg: AbortSignal) => Promise<TResult
|
|
327
|
+
async #lookupKey<TResult>(lookupArg: (signalArg: AbortSignal) => Promise<TResult>, operationArg: IConfigStoreOperationState): Promise<TResult> {
|
|
298
328
|
this.#assertOpen();
|
|
299
329
|
const controller = new AbortController();
|
|
300
|
-
|
|
301
|
-
|
|
330
|
+
const signal = AbortSignal.any([controller.signal, this.#lifetime.signal]);
|
|
331
|
+
let resolveProvider!: (valueArg: TResult | PromiseLike<TResult>) => void;
|
|
332
|
+
let rejectProvider!: (errorArg: unknown) => void;
|
|
333
|
+
const provider = new Promise<TResult>((resolveArg, rejectArg) => {
|
|
334
|
+
resolveProvider = resolveArg;
|
|
335
|
+
rejectProvider = rejectArg;
|
|
336
|
+
});
|
|
337
|
+
let abortLookup!: () => void;
|
|
338
|
+
const settlement = provider.then(() => undefined, (errorArg: unknown) => {
|
|
339
|
+
if (signal.aborted && errorArg !== signal.reason) {
|
|
340
|
+
operationArg.providerFailed = true;
|
|
341
|
+
this.#keyProviderFailures.add(new Error('Config encryption key provider rejected after cancellation.'));
|
|
342
|
+
}
|
|
343
|
+
}).finally(() => {
|
|
344
|
+
this.#keyLookups.delete(settlement);
|
|
345
|
+
operationArg.keyLookups.delete(settlement);
|
|
346
|
+
if (operationArg.settled && !operationArg.keyLookups.size && !operationArg.providerFailed) {
|
|
347
|
+
this.#retainedOperations.delete(operationArg);
|
|
348
|
+
}
|
|
349
|
+
signal.removeEventListener('abort', abortLookup);
|
|
350
|
+
});
|
|
351
|
+
// Own the actual callback promise before invoking foreign code, including synchronous close reentry.
|
|
352
|
+
this.#keyLookups.add(settlement);
|
|
353
|
+
operationArg.keyLookups.add(settlement);
|
|
354
|
+
const cancellation = new Promise<never>((resolveArg, rejectArg) => {
|
|
302
355
|
void resolveArg;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}, this.#timeoutMs);
|
|
308
|
-
timer.unref();
|
|
356
|
+
abortLookup = () => {
|
|
357
|
+
rejectArg(signal.reason);
|
|
358
|
+
};
|
|
359
|
+
signal.addEventListener('abort', abortLookup, { once: true });
|
|
309
360
|
});
|
|
361
|
+
const timer = setTimeout(() => controller.abort(new Error('Config encryption key lookup timed out.')), this.#timeoutMs);
|
|
362
|
+
timer.unref();
|
|
310
363
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
]);
|
|
364
|
+
try { resolveProvider(lookupArg(signal)); } catch (errorArg) { rejectProvider(errorArg); }
|
|
365
|
+
const result = await Promise.race([provider, cancellation]);
|
|
366
|
+
signal.throwIfAborted();
|
|
315
367
|
this.#assertOpen();
|
|
316
368
|
return result;
|
|
317
369
|
} finally {
|
|
@@ -319,18 +371,23 @@ export class NoSqlConfigStore implements IConfigStore {
|
|
|
319
371
|
}
|
|
320
372
|
}
|
|
321
373
|
|
|
322
|
-
#runOperation<TResult>(operationArg: () => Promise<TResult>): Promise<TResult> {
|
|
374
|
+
#runOperation<TResult>(operationArg: (stateArg: IConfigStoreOperationState) => Promise<TResult>): Promise<TResult> {
|
|
323
375
|
if (this.#lifetime.signal.aborted) {
|
|
324
376
|
return Promise.reject(this.#lifetime.signal.reason);
|
|
325
377
|
}
|
|
326
|
-
if (this.#pendingOperations >= this.#maxPendingOperations) {
|
|
378
|
+
if (this.#pendingOperations + this.#retainedOperations.size >= this.#maxPendingOperations) {
|
|
327
379
|
return Promise.reject(new Error(`NoSqlConfigStore operation capacity reached: ${this.#maxPendingOperations}.`));
|
|
328
380
|
}
|
|
329
381
|
this.#pendingOperations += 1;
|
|
382
|
+
const state: IConfigStoreOperationState = { keyLookups: new Set(), settled: false, providerFailed: false };
|
|
330
383
|
const operation = this.#operationTail.then(() => {
|
|
331
384
|
this.#assertOpen();
|
|
332
|
-
return operationArg();
|
|
333
|
-
}).finally(() => {
|
|
385
|
+
return operationArg(state);
|
|
386
|
+
}).finally(() => {
|
|
387
|
+
state.settled = true;
|
|
388
|
+
this.#pendingOperations -= 1;
|
|
389
|
+
if (state.keyLookups.size || state.providerFailed) this.#retainedOperations.add(state);
|
|
390
|
+
});
|
|
334
391
|
this.#operationTail = operation.then(() => undefined, () => undefined);
|
|
335
392
|
return operation;
|
|
336
393
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { NoSqlConfigStore } from './classes.configstore.js';
|
|
2
|
+
import type { IConfigFlowContext, IIntegrationSetupContext, ILogger } from './types.js';
|
|
3
|
+
|
|
4
|
+
export interface INativeHomeAdmissionOptions {
|
|
5
|
+
readonly homeId?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** A captured host selection, not an authentication credential. Persistence resources are borrowed. */
|
|
9
|
+
export interface INativeAdmissionContext {
|
|
10
|
+
readonly homeId: string;
|
|
11
|
+
readonly configStore?: NoSqlConfigStore;
|
|
12
|
+
readonly logger?: ILogger;
|
|
13
|
+
readonly abortSignal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const identifier = (valueArg: unknown, nameArg: string, maximumArg: number): string => {
|
|
17
|
+
if (typeof valueArg !== 'string' || !valueArg || valueArg.length > maximumArg
|
|
18
|
+
|| valueArg.trim() !== valueArg || /[\p{Cc}\p{Cs}]/u.test(valueArg)) {
|
|
19
|
+
throw new TypeError(`${nameArg} must be a non-empty identifier without surrounding whitespace or control characters, at most ${maximumArg} characters.`);
|
|
20
|
+
}
|
|
21
|
+
return valueArg;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const dataFields = (valueArg: unknown, allowedArg: readonly string[], nameArg: string): Record<string, unknown> => {
|
|
25
|
+
if (!valueArg || typeof valueArg !== 'object'
|
|
26
|
+
|| (Object.getPrototypeOf(valueArg) !== Object.prototype && Object.getPrototypeOf(valueArg) !== null)) {
|
|
27
|
+
throw new TypeError(`${nameArg} must be a plain data object.`);
|
|
28
|
+
}
|
|
29
|
+
const captured: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
30
|
+
for (const key of Reflect.ownKeys(valueArg)) {
|
|
31
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
32
|
+
if (typeof key !== 'string' || !allowedArg.includes(key) || !descriptor
|
|
33
|
+
|| !descriptor.enumerable || !('value' in descriptor)) {
|
|
34
|
+
throw new TypeError(`${nameArg} contains an unsupported field or accessor.`);
|
|
35
|
+
}
|
|
36
|
+
captured[key] = descriptor.value as unknown;
|
|
37
|
+
}
|
|
38
|
+
return captured;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const logger = (valueArg: unknown): ILogger | undefined => {
|
|
42
|
+
if (valueArg === undefined) return undefined;
|
|
43
|
+
if (!valueArg || typeof valueArg !== 'object') throw new TypeError('Native logger must expose a log method.');
|
|
44
|
+
let target: object | null = valueArg;
|
|
45
|
+
for (let depth = 0; target && depth < 16; depth += 1) {
|
|
46
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, 'log');
|
|
47
|
+
if (descriptor) {
|
|
48
|
+
if ('value' in descriptor && typeof descriptor.value === 'function') return valueArg as ILogger;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
target = Object.getPrototypeOf(target) as object | null;
|
|
52
|
+
}
|
|
53
|
+
throw new TypeError('Native logger must expose a data-method log function.');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Binds to independently selected host context only after all synchronous checks pass. */
|
|
57
|
+
export class NativeHomeAdmission {
|
|
58
|
+
readonly #domain: string;
|
|
59
|
+
#homeId?: string;
|
|
60
|
+
|
|
61
|
+
constructor(domainArg: string, optionsArg: INativeHomeAdmissionOptions = {}) {
|
|
62
|
+
this.#domain = identifier(domainArg, 'integrationDomain', 128);
|
|
63
|
+
const options = dataFields(optionsArg, ['homeId'], 'Native home options');
|
|
64
|
+
if (options.homeId !== undefined) this.#homeId = identifier(options.homeId, 'homeId', 256);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public get homeId(): string | undefined { return this.#homeId; }
|
|
68
|
+
|
|
69
|
+
public admit(contextArg: IIntegrationSetupContext | IConfigFlowContext, configHomeIdArg?: string): INativeAdmissionContext {
|
|
70
|
+
const context = dataFields(contextArg, ['homeId', 'configStore', 'logger', 'abortSignal'], 'Native context');
|
|
71
|
+
const homeId = identifier(context.homeId, 'Host-selected homeId', 256);
|
|
72
|
+
if (this.#homeId !== undefined && this.#homeId !== homeId) throw new Error('Native owning home does not match the selected host home.');
|
|
73
|
+
if (configHomeIdArg !== undefined && identifier(configHomeIdArg, 'Configuration homeId', 256) !== homeId) {
|
|
74
|
+
throw new Error('Native configuration home does not match the selected host home.');
|
|
75
|
+
}
|
|
76
|
+
const configStore = context.configStore;
|
|
77
|
+
if (configStore !== undefined) {
|
|
78
|
+
if (!(configStore instanceof NoSqlConfigStore)) throw new TypeError('Native configStore must be an actual NoSqlConfigStore.');
|
|
79
|
+
// Call the owning implementation: a structural or overridden getScope cannot grant admission.
|
|
80
|
+
const scope = NoSqlConfigStore.prototype.getScope.call(configStore);
|
|
81
|
+
if (scope.homeId !== homeId || scope.integrationDomain !== this.#domain) {
|
|
82
|
+
throw new Error('Native configStore home/domain does not match the admitted owner.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const capturedLogger = logger(context.logger);
|
|
86
|
+
const abortSignal = context.abortSignal;
|
|
87
|
+
if (abortSignal !== undefined) {
|
|
88
|
+
if (!(abortSignal instanceof AbortSignal)) throw new TypeError('Native abortSignal must be an AbortSignal.');
|
|
89
|
+
AbortSignal.prototype.throwIfAborted.call(abortSignal);
|
|
90
|
+
}
|
|
91
|
+
const captured = Object.freeze({ homeId, configStore: configStore as NoSqlConfigStore | undefined,
|
|
92
|
+
logger: capturedLogger, abortSignal: abortSignal as AbortSignal | undefined });
|
|
93
|
+
this.#homeId = homeId;
|
|
94
|
+
return captured;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { NativeHomeAdmission, type INativeAdmissionContext, type INativeHomeAdmissionOptions } from './classes.nativehomeadmission.js';
|
|
2
|
+
import type { IConfigFlowContext, IIntegrationSetupContext } from './types.js';
|
|
3
|
+
|
|
4
|
+
export interface INativeIntegrationOwnerOptions extends INativeHomeAdmissionOptions {
|
|
5
|
+
/** Shared limit for pending operations, runtimes, and retained cleanup resources. Defaults to 16. */
|
|
6
|
+
readonly capacity?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface INativeOperationLease {
|
|
10
|
+
readonly context: INativeAdmissionContext;
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
assertOpen(): void;
|
|
13
|
+
/** Register immediately after allocation, before invoking initialization or other foreign callbacks. */
|
|
14
|
+
own(cleanupArg: () => Promise<void>): void;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface INativeSetupLease extends INativeOperationLease {
|
|
19
|
+
/** Transfers this operation's cleanup ownership to the returned runtime. */
|
|
20
|
+
promoteToRuntime(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface INativeResource {
|
|
24
|
+
readonly cleanup: () => Promise<void>;
|
|
25
|
+
status: 'idle' | 'pending' | 'closed' | 'failed';
|
|
26
|
+
promise?: Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface INativeScopeState {
|
|
30
|
+
readonly lifetime: AbortController;
|
|
31
|
+
readonly leases: Set<NativeIntegrationLease>;
|
|
32
|
+
readonly operations: Set<Promise<unknown>>;
|
|
33
|
+
closed: boolean;
|
|
34
|
+
closePromise?: Promise<void>;
|
|
35
|
+
retryPromise?: Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const scopeState = (): INativeScopeState => ({ lifetime: new AbortController(), leases: new Set(), operations: new Set(), closed: false });
|
|
39
|
+
|
|
40
|
+
class NativeIntegrationLease implements INativeSetupLease {
|
|
41
|
+
readonly #state: NativeOwnerState;
|
|
42
|
+
readonly #scope: INativeScopeState;
|
|
43
|
+
readonly #runtimeAllowed: boolean;
|
|
44
|
+
readonly #lifetime = new AbortController();
|
|
45
|
+
readonly #resources = new Set<INativeResource>();
|
|
46
|
+
readonly context: INativeAdmissionContext;
|
|
47
|
+
readonly signal: AbortSignal;
|
|
48
|
+
#pending = true;
|
|
49
|
+
#promoted = false;
|
|
50
|
+
#closed = false;
|
|
51
|
+
#closePromise?: Promise<void>;
|
|
52
|
+
|
|
53
|
+
constructor(stateArg: NativeOwnerState, scopeArg: INativeScopeState, contextArg: INativeAdmissionContext, runtimeAllowedArg: boolean) {
|
|
54
|
+
this.#state = stateArg;
|
|
55
|
+
this.#scope = scopeArg;
|
|
56
|
+
this.#runtimeAllowed = runtimeAllowedArg;
|
|
57
|
+
this.context = contextArg;
|
|
58
|
+
this.signal = AbortSignal.any([stateArg.lifetime.signal, scopeArg.lifetime.signal, this.#lifetime.signal,
|
|
59
|
+
...(contextArg.abortSignal ? [contextArg.abortSignal] : [])]);
|
|
60
|
+
// Abort owns cleanup as well as cancellation; rejected cleanup remains on this retained lease.
|
|
61
|
+
this.signal.addEventListener('abort', () => { void this.close().catch(() => undefined); }, { once: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public get promoted(): boolean { return this.#promoted; }
|
|
65
|
+
public get weight(): number { return Math.max(1, [...this.#resources].filter((resource) => resource.status !== 'closed').length); }
|
|
66
|
+
public get failed(): boolean { return [...this.#resources].some((resource) => resource.status === 'failed'); }
|
|
67
|
+
|
|
68
|
+
public assertOpen(): void {
|
|
69
|
+
if (this.#closed || this.#state.closed || this.#scope.closed) throw new Error('Native admission lease is closed.');
|
|
70
|
+
this.signal.throwIfAborted();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public own(cleanupArg: () => Promise<void>): void {
|
|
74
|
+
this.assertOpen();
|
|
75
|
+
if (typeof cleanupArg !== 'function') throw new TypeError('Native cleanup must be a function.');
|
|
76
|
+
if ([...this.#resources].filter((resource) => resource.status !== 'closed').length > 0) this.#state.assertCapacity();
|
|
77
|
+
this.#resources.add({ cleanup: cleanupArg, status: 'idle' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public promoteToRuntime(): void {
|
|
81
|
+
this.assertOpen();
|
|
82
|
+
if (!this.#runtimeAllowed || !this.#pending) throw new Error('Only an admitted pending setup can become a runtime.');
|
|
83
|
+
if (!this.#resources.size) throw new Error('A native runtime must retain an owned cleanup handle.');
|
|
84
|
+
this.#promoted = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public finish(): void { this.#pending = false; this.#releaseIfClosed(); }
|
|
88
|
+
|
|
89
|
+
public close(): Promise<void> {
|
|
90
|
+
if (!this.#closePromise) {
|
|
91
|
+
this.#closed = true;
|
|
92
|
+
this.#closePromise = Promise.resolve().then(() => this.#drain(false));
|
|
93
|
+
this.#lifetime.abort(new Error('Native admission lease is closed.'));
|
|
94
|
+
}
|
|
95
|
+
return this.#closePromise;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public retryCleanup(): Promise<void> { return this.#drain(true); }
|
|
99
|
+
|
|
100
|
+
async #drain(retryArg: boolean): Promise<void> {
|
|
101
|
+
const results = await Promise.allSettled([...this.#resources].map((resource) => {
|
|
102
|
+
if (retryArg && resource.status === 'failed') resource.promise = undefined;
|
|
103
|
+
if (!resource.promise) {
|
|
104
|
+
resource.status = 'pending';
|
|
105
|
+
resource.promise = Promise.resolve().then(() => resource.cleanup()).then(() => {
|
|
106
|
+
resource.status = 'closed';
|
|
107
|
+
}, (errorArg: unknown) => {
|
|
108
|
+
resource.status = 'failed';
|
|
109
|
+
throw errorArg;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return resource.promise;
|
|
113
|
+
}));
|
|
114
|
+
this.#releaseIfClosed();
|
|
115
|
+
const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected').map((result) => result.reason as unknown);
|
|
116
|
+
if (failures.length) throw new AggregateError(failures, 'Native resource cleanup failed; handles remain owned for explicit retry.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#releaseIfClosed(): void {
|
|
120
|
+
if (!this.#pending && this.#closed && [...this.#resources].every((resource) => resource.status === 'closed')) {
|
|
121
|
+
this.#state.release(this, this.#scope);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class NativeOwnerState {
|
|
127
|
+
readonly admission: NativeHomeAdmission;
|
|
128
|
+
readonly lifetime = new AbortController();
|
|
129
|
+
readonly root = scopeState();
|
|
130
|
+
readonly #capacity: number;
|
|
131
|
+
readonly #leases = new Set<NativeIntegrationLease>();
|
|
132
|
+
readonly #scopes = new Set<INativeScopeState>([this.root]);
|
|
133
|
+
closed = false;
|
|
134
|
+
#closePromise?: Promise<void>;
|
|
135
|
+
#retryPromise?: Promise<void>;
|
|
136
|
+
|
|
137
|
+
constructor(domainArg: string, optionsArg: INativeIntegrationOwnerOptions) {
|
|
138
|
+
if (!optionsArg || (Object.getPrototypeOf(optionsArg) !== Object.prototype && Object.getPrototypeOf(optionsArg) !== null)) {
|
|
139
|
+
throw new TypeError('Native owner options must be a plain data object.');
|
|
140
|
+
}
|
|
141
|
+
for (const key of Reflect.ownKeys(optionsArg)) {
|
|
142
|
+
const descriptor = Object.getOwnPropertyDescriptor(optionsArg, key);
|
|
143
|
+
if ((key !== 'homeId' && key !== 'capacity') || !descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
144
|
+
throw new TypeError('Native owner options contain an unsupported field or accessor.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.#capacity = optionsArg.capacity ?? 16;
|
|
148
|
+
if (!Number.isSafeInteger(this.#capacity) || this.#capacity < 1) throw new RangeError('Native owner capacity must be a positive safe integer.');
|
|
149
|
+
this.admission = new NativeHomeAdmission(domainArg, { homeId: optionsArg.homeId });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public assertOpen(scopeArg: INativeScopeState): void {
|
|
153
|
+
if (this.closed || scopeArg.closed) throw new Error('Native integration owner/scope is closed.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public assertCapacity(): void {
|
|
157
|
+
if ([...this.#leases].reduce((count, lease) => count + lease.weight, 0) >= this.#capacity) {
|
|
158
|
+
throw new Error(`Native integration ownership capacity reached: ${this.#capacity}.`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public createScope(): INativeScopeState {
|
|
163
|
+
this.assertOpen(this.root);
|
|
164
|
+
if (this.#scopes.size > this.#capacity) throw new Error('Native config-flow scope capacity reached.');
|
|
165
|
+
const scope = scopeState();
|
|
166
|
+
this.#scopes.add(scope);
|
|
167
|
+
return scope;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public admit(scopeArg: INativeScopeState, contextArg: IIntegrationSetupContext | IConfigFlowContext, configHomeIdArg?: string): INativeAdmissionContext {
|
|
171
|
+
this.assertOpen(scopeArg);
|
|
172
|
+
return this.admission.admit(contextArg, configHomeIdArg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public run<TResult>(scopeArg: INativeScopeState, contextArg: IIntegrationSetupContext | IConfigFlowContext,
|
|
176
|
+
configHomeIdArg: string | undefined, workArg: (leaseArg: INativeSetupLease) => Promise<TResult>, runtimeArg: boolean): Promise<TResult> {
|
|
177
|
+
this.assertOpen(scopeArg);
|
|
178
|
+
if (typeof workArg !== 'function') throw new TypeError('Native operation must be a function.');
|
|
179
|
+
if (runtimeArg && typeof configHomeIdArg !== 'string') throw new TypeError('Native setup requires configuration homeId.');
|
|
180
|
+
this.assertCapacity();
|
|
181
|
+
const context = this.admission.admit(contextArg, configHomeIdArg);
|
|
182
|
+
const lease = new NativeIntegrationLease(this, scopeArg, context, runtimeArg);
|
|
183
|
+
this.#leases.add(lease);
|
|
184
|
+
scopeArg.leases.add(lease);
|
|
185
|
+
// Publish ownership and work before invoking any caller callback.
|
|
186
|
+
const operation = Promise.resolve().then(async () => {
|
|
187
|
+
try {
|
|
188
|
+
lease.assertOpen();
|
|
189
|
+
const result = await workArg(lease);
|
|
190
|
+
if (runtimeArg) {
|
|
191
|
+
// Returning a live runtime is a new admission; a completed flow effect is already accepted.
|
|
192
|
+
lease.assertOpen();
|
|
193
|
+
if (!lease.promoted) throw new Error('Native setup must transfer its cleanup lease to the returned runtime.');
|
|
194
|
+
}
|
|
195
|
+
if (!runtimeArg) await lease.close();
|
|
196
|
+
return result;
|
|
197
|
+
} catch (errorArg) {
|
|
198
|
+
try { await lease.close(); }
|
|
199
|
+
catch (cleanupErrorArg) { throw new AggregateError([errorArg, cleanupErrorArg], 'Native operation and cleanup failed.'); }
|
|
200
|
+
throw errorArg;
|
|
201
|
+
} finally { lease.finish(); }
|
|
202
|
+
});
|
|
203
|
+
scopeArg.operations.add(operation);
|
|
204
|
+
void operation.then(() => scopeArg.operations.delete(operation), () => scopeArg.operations.delete(operation));
|
|
205
|
+
return operation;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public release(leaseArg: NativeIntegrationLease, scopeArg: INativeScopeState): void {
|
|
209
|
+
this.#leases.delete(leaseArg);
|
|
210
|
+
scopeArg.leases.delete(leaseArg);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public closeScope(scopeArg: INativeScopeState): Promise<void> {
|
|
214
|
+
if (!scopeArg.closePromise) {
|
|
215
|
+
scopeArg.closed = true;
|
|
216
|
+
scopeArg.closePromise = Promise.resolve().then(async () => {
|
|
217
|
+
await this.#drain([scopeArg]);
|
|
218
|
+
this.#scopes.delete(scopeArg);
|
|
219
|
+
});
|
|
220
|
+
for (const lease of scopeArg.leases) void lease.close().catch(() => undefined);
|
|
221
|
+
scopeArg.lifetime.abort(new Error('Native config-flow scope is closed.'));
|
|
222
|
+
}
|
|
223
|
+
return scopeArg.closePromise;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public close(): Promise<void> {
|
|
227
|
+
if (!this.#closePromise) {
|
|
228
|
+
this.closed = true;
|
|
229
|
+
for (const scope of this.#scopes) scope.closed = true;
|
|
230
|
+
this.#closePromise = Promise.resolve().then(() => this.#drain([...this.#scopes]));
|
|
231
|
+
for (const lease of this.#leases) void lease.close().catch(() => undefined);
|
|
232
|
+
this.lifetime.abort(new Error('Native integration owner is closed.'));
|
|
233
|
+
for (const scope of this.#scopes) scope.lifetime.abort(this.lifetime.signal.reason);
|
|
234
|
+
}
|
|
235
|
+
return this.#closePromise;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public retryCleanup(scopeArg?: INativeScopeState): Promise<void> {
|
|
239
|
+
const existing = scopeArg ? scopeArg.retryPromise : this.#retryPromise;
|
|
240
|
+
if (existing) return existing;
|
|
241
|
+
const retry = Promise.resolve().then(async () => {
|
|
242
|
+
const leases = [...(scopeArg ? scopeArg.leases : this.#leases)].filter((lease) => lease.failed);
|
|
243
|
+
const results = await Promise.allSettled(leases.map((lease) => lease.retryCleanup()));
|
|
244
|
+
const errors = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected').map((result) => result.reason as unknown);
|
|
245
|
+
if (errors.length) throw new AggregateError(errors, 'Explicit native cleanup retry failed.');
|
|
246
|
+
for (const scope of this.#scopes) {
|
|
247
|
+
if (scope !== this.root && scope.closed && !scope.leases.size && !scope.operations.size) this.#scopes.delete(scope);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
if (scopeArg) scopeArg.retryPromise = retry;
|
|
251
|
+
else this.#retryPromise = retry;
|
|
252
|
+
const settled = (): void => { if (scopeArg) scopeArg.retryPromise = undefined; else this.#retryPromise = undefined; };
|
|
253
|
+
void retry.then(settled, settled);
|
|
254
|
+
return retry;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async #drain(scopesArg: INativeScopeState[]): Promise<void> {
|
|
258
|
+
const cleanup = scopesArg.flatMap((scope) => [...scope.leases].map((lease) => lease.close()));
|
|
259
|
+
const operations = scopesArg.flatMap((scope) => [...scope.operations]);
|
|
260
|
+
const [results] = await Promise.all([Promise.allSettled(cleanup), Promise.allSettled(operations)]);
|
|
261
|
+
// Operation failures are delivered to their callers; owner close owns joining and cleanup failures.
|
|
262
|
+
const finalResults = await Promise.allSettled(scopesArg.flatMap((scope) => [...scope.leases].map((lease) => lease.close())));
|
|
263
|
+
const errors = [...results, ...finalResults].filter((result): result is PromiseRejectedResult => result.status === 'rejected').map((result) => result.reason as unknown);
|
|
264
|
+
if (errors.length) throw new AggregateError([...new Set(errors)], 'Native owner cleanup failed; resources remain retained.');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A terminal child scope shares owner home admission and resource capacity. */
|
|
269
|
+
export class NativeIntegrationScope {
|
|
270
|
+
readonly #state: NativeOwnerState;
|
|
271
|
+
readonly #scope: INativeScopeState;
|
|
272
|
+
constructor(stateArg: NativeOwnerState, scopeArg: INativeScopeState) { this.#state = stateArg; this.#scope = scopeArg; }
|
|
273
|
+
public admit(contextArg: IConfigFlowContext, configHomeIdArg?: string): INativeAdmissionContext {
|
|
274
|
+
return this.#state.admit(this.#scope, contextArg, configHomeIdArg);
|
|
275
|
+
}
|
|
276
|
+
public run<TResult>(contextArg: IConfigFlowContext, configHomeIdArg: string | undefined,
|
|
277
|
+
workArg: (leaseArg: INativeOperationLease) => Promise<TResult>): Promise<TResult> {
|
|
278
|
+
return this.#state.run(this.#scope, contextArg, configHomeIdArg, workArg, false);
|
|
279
|
+
}
|
|
280
|
+
public close(): Promise<void> { return this.#state.closeScope(this.#scope); }
|
|
281
|
+
public retryCleanup(): Promise<void> { return this.#state.retryCleanup(this.#scope); }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Owns native admission, in-flight work, runtimes and failed cleanup without replaying transport work. */
|
|
285
|
+
export class NativeIntegrationOwner {
|
|
286
|
+
readonly #state: NativeOwnerState;
|
|
287
|
+
constructor(domainArg: string, optionsArg: INativeIntegrationOwnerOptions = {}) { this.#state = new NativeOwnerState(domainArg, optionsArg); }
|
|
288
|
+
public get admission(): NativeHomeAdmission { return this.#state.admission; }
|
|
289
|
+
public get signal(): AbortSignal { return this.#state.lifetime.signal; }
|
|
290
|
+
public runSetup<TResult>(contextArg: IIntegrationSetupContext, configHomeIdArg: string,
|
|
291
|
+
workArg: (leaseArg: INativeSetupLease) => Promise<TResult>): Promise<TResult> {
|
|
292
|
+
return this.#state.run(this.#state.root, contextArg, configHomeIdArg, workArg, true);
|
|
293
|
+
}
|
|
294
|
+
public createFlowScope(): NativeIntegrationScope { return new NativeIntegrationScope(this.#state, this.#state.createScope()); }
|
|
295
|
+
public close(): Promise<void> { return this.#state.close(); }
|
|
296
|
+
public retryCleanup(): Promise<void> { return this.#state.retryCleanup(); }
|
|
297
|
+
}
|
package/ts/core/index.ts
CHANGED
package/ts/core/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { NoSqlConfigStore } from './classes.configstore.js';
|
|
2
3
|
|
|
3
4
|
export type TDiscoverySource =
|
|
4
5
|
| 'mdns'
|
|
@@ -112,11 +113,18 @@ export interface IDiscoveryDescriptorOptions {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
export interface IIntegrationSetupContext {
|
|
116
|
+
/** Explicit owning home selected by the embedding host, never by device configuration. */
|
|
117
|
+
readonly homeId?: string;
|
|
115
118
|
logger?: ILogger;
|
|
116
119
|
configStore?: IConfigStore;
|
|
117
120
|
abortSignal?: AbortSignal;
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
export interface INativeIntegrationSetupContext extends IIntegrationSetupContext {
|
|
124
|
+
readonly homeId: string;
|
|
125
|
+
readonly configStore?: NoSqlConfigStore;
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
export type TEntityPlatform =
|
|
121
129
|
| 'light'
|
|
122
130
|
| 'switch'
|
|
@@ -211,8 +219,16 @@ export interface IConfigFlowField {
|
|
|
211
219
|
}
|
|
212
220
|
|
|
213
221
|
export interface IConfigFlowContext {
|
|
222
|
+
/** Explicit owning home selected by the embedding host. This is not an authentication token. */
|
|
223
|
+
readonly homeId?: string;
|
|
214
224
|
logger?: ILogger;
|
|
215
225
|
configStore?: IConfigStore;
|
|
226
|
+
abortSignal?: AbortSignal;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface INativeConfigFlowContext extends IConfigFlowContext {
|
|
230
|
+
readonly homeId: string;
|
|
231
|
+
readonly configStore?: NoSqlConfigStore;
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
export interface IConfigFlowStep<TConfig = unknown> {
|