mirror of
				https://github.com/wanderer-industries/wanderer
				synced 2025-10-31 14:37:04 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			v1.65.20
			...
			refactorin
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d38c828600 | ||
|   | 24196efbac | ||
|   | 83c76e3a67 | 
| @@ -1,6 +1,5 @@ | ||||
| import { createRoot } from 'react-dom/client'; | ||||
| import Mapper from './MapRoot'; | ||||
| import { decompressToJson } from './utils'; | ||||
|  | ||||
| export default { | ||||
|   _rootEl: null, | ||||
| @@ -28,17 +27,12 @@ export default { | ||||
|  | ||||
|   handleEventWrapper(event: string, handler: (payload: any) => void) { | ||||
|     this.handleEvent(event, (body: any) => { | ||||
|       if (event === 'map_event') { | ||||
|         const { type, body: data } = body; | ||||
|         handler({ type, body: decompressToJson(data) }); | ||||
|       } else { | ||||
|         handler(body); | ||||
|       } | ||||
|       handler(body); | ||||
|     }); | ||||
|   }, | ||||
|  | ||||
|   reconnected() { | ||||
|     this.pushEvent('reconnected'); | ||||
|     this.pushEvent('ui_loaded'); | ||||
|   }, | ||||
|  | ||||
|   async pushEventAsync(event: string, payload: any) { | ||||
|   | ||||
| @@ -1,14 +0,0 @@ | ||||
| import pako from 'pako'; | ||||
|  | ||||
| export const decompressToJson = (base64string: string) => { | ||||
|   const base64_decoded = atob(base64string); | ||||
|   const charData = base64_decoded.split('').map(function (x) { | ||||
|     return x.charCodeAt(0); | ||||
|   }); | ||||
|   const zlibData = new Uint8Array(charData); | ||||
|   const inflatedData = pako.inflate(zlibData, { | ||||
|     to: 'string', | ||||
|   }); | ||||
|  | ||||
|   return JSON.parse(inflatedData); | ||||
| }; | ||||
| @@ -1,3 +1,2 @@ | ||||
| export * from './contextStore'; | ||||
| export * from './decompressToJson'; | ||||
| export * from './getQueryVariable'; | ||||
|   | ||||
| @@ -21,7 +21,6 @@ | ||||
|     "live_select": "file:../deps/live_select", | ||||
|     "lodash.debounce": "^4.0.8", | ||||
|     "lodash.isequal": "^4.5.0", | ||||
|     "pako": "^2.1.0", | ||||
|     "phoenix": "file:../deps/phoenix", | ||||
|     "phoenix_html": "file:../deps/phoenix_html", | ||||
|     "phoenix_live_view": "file:../deps/phoenix_live_view", | ||||
| @@ -44,7 +43,6 @@ | ||||
|     "@tailwindcss/typography": "^0.5.13", | ||||
|     "@types/lodash.debounce": "^4.0.9", | ||||
|     "@types/lodash.isequal": "^4.5.8", | ||||
|     "@types/pako": "^2.0.3", | ||||
|     "@types/react": "18.2.0", | ||||
|     "@types/react-dom": "18.2.1", | ||||
|     "@types/react-grid-layout": "^1.3.4", | ||||
|   | ||||
							
								
								
									
										344
									
								
								assets/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										344
									
								
								assets/yarn.lock
									
									
									
									
									
								
							| @@ -33,7 +33,7 @@ | ||||
|   resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz" | ||||
|   integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== | ||||
|  | ||||
| "@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.14.8", "@babel/core@^7.24.5": | ||||
| "@babel/core@^7.14.8", "@babel/core@^7.24.5": | ||||
|   version "7.24.5" | ||||
|   resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz" | ||||
|   integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA== | ||||
| @@ -240,11 +240,121 @@ | ||||
|     "@babel/helper-validator-identifier" "^7.24.5" | ||||
|     to-fast-properties "^2.0.0" | ||||
|  | ||||
| "@esbuild/aix-ppc64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" | ||||
|   integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== | ||||
|  | ||||
| "@esbuild/android-arm64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" | ||||
|   integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== | ||||
|  | ||||
| "@esbuild/android-arm@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" | ||||
|   integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== | ||||
|  | ||||
| "@esbuild/android-x64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" | ||||
|   integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== | ||||
|  | ||||
| "@esbuild/darwin-arm64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz" | ||||
|   integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== | ||||
|  | ||||
| "@esbuild/darwin-x64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" | ||||
|   integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== | ||||
|  | ||||
| "@esbuild/freebsd-arm64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" | ||||
|   integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== | ||||
|  | ||||
| "@esbuild/freebsd-x64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" | ||||
|   integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== | ||||
|  | ||||
| "@esbuild/linux-arm64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" | ||||
|   integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== | ||||
|  | ||||
| "@esbuild/linux-arm@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" | ||||
|   integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== | ||||
|  | ||||
| "@esbuild/linux-ia32@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" | ||||
|   integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== | ||||
|  | ||||
| "@esbuild/linux-loong64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" | ||||
|   integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== | ||||
|  | ||||
| "@esbuild/linux-mips64el@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" | ||||
|   integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== | ||||
|  | ||||
| "@esbuild/linux-ppc64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" | ||||
|   integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== | ||||
|  | ||||
| "@esbuild/linux-riscv64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" | ||||
|   integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== | ||||
|  | ||||
| "@esbuild/linux-s390x@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" | ||||
|   integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== | ||||
|  | ||||
| "@esbuild/linux-x64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" | ||||
|   integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== | ||||
|  | ||||
| "@esbuild/netbsd-x64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" | ||||
|   integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== | ||||
|  | ||||
| "@esbuild/openbsd-x64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" | ||||
|   integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== | ||||
|  | ||||
| "@esbuild/sunos-x64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" | ||||
|   integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== | ||||
|  | ||||
| "@esbuild/win32-arm64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" | ||||
|   integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== | ||||
|  | ||||
| "@esbuild/win32-ia32@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" | ||||
|   integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== | ||||
|  | ||||
| "@esbuild/win32-x64@0.20.2": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" | ||||
|   integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== | ||||
|  | ||||
| "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": | ||||
|   version "4.4.0" | ||||
|   resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" | ||||
| @@ -341,7 +451,7 @@ | ||||
|     "@nodelib/fs.stat" "2.0.5" | ||||
|     run-parallel "^1.1.9" | ||||
|  | ||||
| "@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": | ||||
| "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": | ||||
|   version "2.0.5" | ||||
|   resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" | ||||
|   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== | ||||
| @@ -359,7 +469,7 @@ | ||||
|   resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz" | ||||
|   integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== | ||||
|  | ||||
| "@react-rxjs/core@^0.10.7", "@react-rxjs/core@>=0.1.0": | ||||
| "@react-rxjs/core@^0.10.7": | ||||
|   version "0.10.7" | ||||
|   resolved "https://registry.npmjs.org/@react-rxjs/core/-/core-0.10.7.tgz" | ||||
|   integrity sha512-dornp8pUs9OcdqFKKRh9+I2FVe21gWufNun6RYU1ddts7kUy9i4Thvl0iqcPFbGY61cJQMAJF7dxixWMSD/A/A== | ||||
| @@ -455,11 +565,91 @@ | ||||
|     estree-walker "^2.0.2" | ||||
|     picomatch "^2.3.1" | ||||
|  | ||||
| "@rollup/rollup-android-arm-eabi@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz#1a32112822660ee104c5dd3a7c595e26100d4c2d" | ||||
|   integrity sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ== | ||||
|  | ||||
| "@rollup/rollup-android-arm64@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz#5aeef206d65ff4db423f3a93f71af91b28662c5b" | ||||
|   integrity sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw== | ||||
|  | ||||
| "@rollup/rollup-darwin-arm64@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz" | ||||
|   integrity sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw== | ||||
|  | ||||
| "@rollup/rollup-darwin-x64@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz#f64fc51ed12b19f883131ccbcea59fc68cbd6c0b" | ||||
|   integrity sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ== | ||||
|  | ||||
| "@rollup/rollup-linux-arm-gnueabihf@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz#1a7641111be67c10111f7122d1e375d1226cbf14" | ||||
|   integrity sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A== | ||||
|  | ||||
| "@rollup/rollup-linux-arm-musleabihf@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz#c93fd632923e0fee25aacd2ae414288d0b7455bb" | ||||
|   integrity sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg== | ||||
|  | ||||
| "@rollup/rollup-linux-arm64-gnu@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz#fa531425dd21d058a630947527b4612d9d0b4a4a" | ||||
|   integrity sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A== | ||||
|  | ||||
| "@rollup/rollup-linux-arm64-musl@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz#8acc16f095ceea5854caf7b07e73f7d1802ac5af" | ||||
|   integrity sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA== | ||||
|  | ||||
| "@rollup/rollup-linux-powerpc64le-gnu@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz#94e69a8499b5cf368911b83a44bb230782aeb571" | ||||
|   integrity sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ== | ||||
|  | ||||
| "@rollup/rollup-linux-riscv64-gnu@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz#7ef1c781c7e59e85a6ce261cc95d7f1e0b56db0f" | ||||
|   integrity sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg== | ||||
|  | ||||
| "@rollup/rollup-linux-s390x-gnu@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz#f15775841c3232fca9b78cd25a7a0512c694b354" | ||||
|   integrity sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g== | ||||
|  | ||||
| "@rollup/rollup-linux-x64-gnu@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz#b521d271798d037ad70c9f85dd97d25f8a52e811" | ||||
|   integrity sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ== | ||||
|  | ||||
| "@rollup/rollup-linux-x64-gnu@4.9.5": | ||||
|   version "4.9.5" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49" | ||||
|   integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA== | ||||
|  | ||||
| "@rollup/rollup-linux-x64-musl@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz#9254019cc4baac35800991315d133cc9fd1bf385" | ||||
|   integrity sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q== | ||||
|  | ||||
| "@rollup/rollup-win32-arm64-msvc@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz#27f65a89f6f52ee9426ec11e3571038e4671790f" | ||||
|   integrity sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA== | ||||
|  | ||||
| "@rollup/rollup-win32-ia32-msvc@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz#a2fbf8246ed0bb014f078ca34ae6b377a90cb411" | ||||
|   integrity sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ== | ||||
|  | ||||
| "@rollup/rollup-win32-x64-msvc@4.17.2": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz#5a2d08b81e8064b34242d5cc9973ef8dd1e60503" | ||||
|   integrity sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w== | ||||
|  | ||||
| "@rx-state/core@0.1.4": | ||||
|   version "0.1.4" | ||||
|   resolved "https://registry.npmjs.org/@rx-state/core/-/core-0.1.4.tgz" | ||||
| @@ -740,7 +930,7 @@ | ||||
|     "@types/d3-transition" "*" | ||||
|     "@types/d3-zoom" "*" | ||||
|  | ||||
| "@types/estree@*", "@types/estree@^1.0.0", "@types/estree@1.0.5": | ||||
| "@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0": | ||||
|   version "1.0.5" | ||||
|   resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" | ||||
|   integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== | ||||
| @@ -774,11 +964,6 @@ | ||||
|   resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz" | ||||
|   integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== | ||||
|  | ||||
| "@types/pako@^2.0.3": | ||||
|   version "2.0.3" | ||||
|   resolved "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz" | ||||
|   integrity sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q== | ||||
|  | ||||
| "@types/prop-types@*": | ||||
|   version "15.7.11" | ||||
|   resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz" | ||||
| @@ -805,7 +990,7 @@ | ||||
|   dependencies: | ||||
|     "@types/react" "*" | ||||
|  | ||||
| "@types/react@*", "@types/react@^17.0.0 || ^18.0.0", "@types/react@>=16.8", "@types/react@18.2.0": | ||||
| "@types/react@*", "@types/react@18.2.0": | ||||
|   version "18.2.0" | ||||
|   resolved "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz" | ||||
|   integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA== | ||||
| @@ -846,7 +1031,7 @@ | ||||
|     semver "^7.5.4" | ||||
|     ts-api-utils "^1.0.1" | ||||
|  | ||||
| "@typescript-eslint/parser@^6.0.0 || ^6.0.0-alpha", "@typescript-eslint/parser@^6.21.0": | ||||
| "@typescript-eslint/parser@^6.21.0": | ||||
|   version "6.21.0" | ||||
|   resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz" | ||||
|   integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== | ||||
| @@ -947,7 +1132,7 @@ acorn-jsx@^5.3.2: | ||||
|   resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" | ||||
|   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== | ||||
|  | ||||
| "acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.4.0, acorn@^8.9.0: | ||||
| acorn@^8.4.0, acorn@^8.9.0: | ||||
|   version "8.11.3" | ||||
|   resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" | ||||
|   integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== | ||||
| @@ -1147,7 +1332,7 @@ braces@^3.0.2, braces@~3.0.2: | ||||
|   dependencies: | ||||
|     fill-range "^7.0.1" | ||||
|  | ||||
| browserslist@^4.22.2, browserslist@^4.23.0, "browserslist@>= 4.21.0": | ||||
| browserslist@^4.22.2, browserslist@^4.23.0: | ||||
|   version "4.23.0" | ||||
|   resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz" | ||||
|   integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== | ||||
| @@ -1205,7 +1390,7 @@ child_process@^1.0.2: | ||||
|   resolved "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz" | ||||
|   integrity sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g== | ||||
|  | ||||
| chokidar@^3.3.0, chokidar@^3.5.3, "chokidar@>=3.0.0 <4.0.0": | ||||
| "chokidar@>=3.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.5.3: | ||||
|   version "3.6.0" | ||||
|   resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" | ||||
|   integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== | ||||
| @@ -1258,16 +1443,16 @@ color-convert@^2.0.1: | ||||
|   dependencies: | ||||
|     color-name "~1.1.4" | ||||
|  | ||||
| color-name@~1.1.4: | ||||
|   version "1.1.4" | ||||
|   resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" | ||||
|   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | ||||
|  | ||||
| color-name@1.1.3: | ||||
|   version "1.1.3" | ||||
|   resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" | ||||
|   integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== | ||||
|  | ||||
| color-name@~1.1.4: | ||||
|   version "1.1.4" | ||||
|   resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" | ||||
|   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | ||||
|  | ||||
| commander@^4.0.0: | ||||
|   version "4.1.1" | ||||
|   resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" | ||||
| @@ -1325,7 +1510,7 @@ culori@^3: | ||||
|   resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" | ||||
|   integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== | ||||
|  | ||||
| d3-drag@^3.0.0, "d3-drag@2 - 3": | ||||
| "d3-drag@2 - 3", d3-drag@^3.0.0: | ||||
|   version "3.0.0" | ||||
|   resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" | ||||
|   integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== | ||||
| @@ -1345,7 +1530,7 @@ d3-drag@^3.0.0, "d3-drag@2 - 3": | ||||
|   dependencies: | ||||
|     d3-color "1 - 3" | ||||
|  | ||||
| d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3: | ||||
| "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: | ||||
|   version "3.0.0" | ||||
|   resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" | ||||
|   integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== | ||||
| @@ -1663,7 +1848,7 @@ escape-string-regexp@^4.0.0: | ||||
|   resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" | ||||
|   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== | ||||
|  | ||||
| eslint-config-prettier@*, eslint-config-prettier@^9.1.0: | ||||
| eslint-config-prettier@^9.1.0: | ||||
|   version "9.1.0" | ||||
|   resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz" | ||||
|   integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== | ||||
| @@ -1723,7 +1908,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 | ||||
|   resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" | ||||
|   integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== | ||||
|  | ||||
| "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0", eslint@^8.57.0, eslint@>=7, eslint@>=7.0.0, eslint@>=8.0.0: | ||||
| eslint@^8.57.0: | ||||
|   version "8.57.0" | ||||
|   resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz" | ||||
|   integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== | ||||
| @@ -1795,12 +1980,7 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: | ||||
|   resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" | ||||
|   integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== | ||||
|  | ||||
| estree-walker@^2.0.1: | ||||
|   version "2.0.2" | ||||
|   resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" | ||||
|   integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== | ||||
|  | ||||
| estree-walker@^2.0.2: | ||||
| estree-walker@^2.0.1, estree-walker@^2.0.2: | ||||
|   version "2.0.2" | ||||
|   resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" | ||||
|   integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== | ||||
| @@ -2010,18 +2190,6 @@ glob-parent@^6.0.2: | ||||
|   dependencies: | ||||
|     is-glob "^4.0.3" | ||||
|  | ||||
| glob@^7.1.3: | ||||
|   version "7.2.3" | ||||
|   resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" | ||||
|   integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== | ||||
|   dependencies: | ||||
|     fs.realpath "^1.0.0" | ||||
|     inflight "^1.0.4" | ||||
|     inherits "2" | ||||
|     minimatch "^3.1.1" | ||||
|     once "^1.3.0" | ||||
|     path-is-absolute "^1.0.0" | ||||
|  | ||||
| glob@7.1.6: | ||||
|   version "7.1.6" | ||||
|   resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" | ||||
| @@ -2034,6 +2202,18 @@ glob@7.1.6: | ||||
|     once "^1.3.0" | ||||
|     path-is-absolute "^1.0.0" | ||||
|  | ||||
| glob@^7.1.3: | ||||
|   version "7.2.3" | ||||
|   resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" | ||||
|   integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== | ||||
|   dependencies: | ||||
|     fs.realpath "^1.0.0" | ||||
|     inflight "^1.0.4" | ||||
|     inherits "2" | ||||
|     minimatch "^3.1.1" | ||||
|     once "^1.3.0" | ||||
|     path-is-absolute "^1.0.0" | ||||
|  | ||||
| globals@^11.1.0: | ||||
|   version "11.12.0" | ||||
|   resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" | ||||
| @@ -2141,14 +2321,7 @@ hasown@^2.0.0: | ||||
|   dependencies: | ||||
|     function-bind "^1.1.2" | ||||
|  | ||||
| hasown@^2.0.1: | ||||
|   version "2.0.2" | ||||
|   resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" | ||||
|   integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== | ||||
|   dependencies: | ||||
|     function-bind "^1.1.2" | ||||
|  | ||||
| hasown@^2.0.2: | ||||
| hasown@^2.0.1, hasown@^2.0.2: | ||||
|   version "2.0.2" | ||||
|   resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" | ||||
|   integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== | ||||
| @@ -2420,7 +2593,7 @@ iterator.prototype@^1.1.2: | ||||
|     reflect.getprototypeof "^1.0.4" | ||||
|     set-function-name "^2.0.1" | ||||
|  | ||||
| jiti@^1.19.1, jiti@>=1.21.0: | ||||
| jiti@^1.19.1: | ||||
|   version "1.21.0" | ||||
|   resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz" | ||||
|   integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== | ||||
| @@ -2518,7 +2691,6 @@ lines-and-columns@^1.1.6: | ||||
|  | ||||
| "live_select@file:../deps/live_select": | ||||
|   version "1.4.2" | ||||
|   resolved "file:../deps/live_select" | ||||
|  | ||||
| locate-path@^6.0.0: | ||||
|   version "6.0.0" | ||||
| @@ -2612,13 +2784,6 @@ mini-svg-data-uri@^1.2.3: | ||||
|   resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz" | ||||
|   integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== | ||||
|  | ||||
| minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: | ||||
|   version "3.1.2" | ||||
|   resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" | ||||
|   integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== | ||||
|   dependencies: | ||||
|     brace-expansion "^1.1.7" | ||||
|  | ||||
| minimatch@9.0.3: | ||||
|   version "9.0.3" | ||||
|   resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" | ||||
| @@ -2626,6 +2791,13 @@ minimatch@9.0.3: | ||||
|   dependencies: | ||||
|     brace-expansion "^2.0.1" | ||||
|  | ||||
| minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: | ||||
|   version "3.1.2" | ||||
|   resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" | ||||
|   integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== | ||||
|   dependencies: | ||||
|     brace-expansion "^1.1.7" | ||||
|  | ||||
| ms@2.1.2: | ||||
|   version "2.1.2" | ||||
|   resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" | ||||
| @@ -2770,11 +2942,6 @@ p-locate@^5.0.0: | ||||
|   dependencies: | ||||
|     p-limit "^3.0.2" | ||||
|  | ||||
| pako@^2.1.0: | ||||
|   version "2.1.0" | ||||
|   resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz" | ||||
|   integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== | ||||
|  | ||||
| parent-module@^1.0.0: | ||||
|   version "1.0.1" | ||||
|   resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" | ||||
| @@ -2812,17 +2979,14 @@ path-type@^5.0.0: | ||||
|   resolved "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz" | ||||
|   integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== | ||||
|  | ||||
| "phoenix@file:../deps/phoenix": | ||||
|   version "1.7.14" | ||||
|  | ||||
| "phoenix_html@file:../deps/phoenix_html": | ||||
|   version "4.1.0" | ||||
|   resolved "file:../deps/phoenix_html" | ||||
|  | ||||
| "phoenix_live_view@file:../deps/phoenix_live_view": | ||||
|   version "0.20.17" | ||||
|   resolved "file:../deps/phoenix_live_view" | ||||
|  | ||||
| "phoenix@file:../deps/phoenix": | ||||
|   version "1.7.14" | ||||
|   resolved "file:../deps/phoenix" | ||||
|  | ||||
| picocolors@^1, picocolors@^1.0.0: | ||||
|   version "1.0.0" | ||||
| @@ -2923,14 +3087,6 @@ postcss-reporter@^7.0.0: | ||||
|     picocolors "^1.0.0" | ||||
|     thenby "^1.3.4" | ||||
|  | ||||
| postcss-selector-parser@^6.0.11: | ||||
|   version "6.0.13" | ||||
|   resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz" | ||||
|   integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== | ||||
|   dependencies: | ||||
|     cssesc "^3.0.0" | ||||
|     util-deprecate "^1.0.2" | ||||
|  | ||||
| postcss-selector-parser@6.0.10: | ||||
|   version "6.0.10" | ||||
|   resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz" | ||||
| @@ -2939,12 +3095,20 @@ postcss-selector-parser@6.0.10: | ||||
|     cssesc "^3.0.0" | ||||
|     util-deprecate "^1.0.2" | ||||
|  | ||||
| postcss-selector-parser@^6.0.11: | ||||
|   version "6.0.13" | ||||
|   resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz" | ||||
|   integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== | ||||
|   dependencies: | ||||
|     cssesc "^3.0.0" | ||||
|     util-deprecate "^1.0.2" | ||||
|  | ||||
| postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: | ||||
|   version "4.2.0" | ||||
|   resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" | ||||
|   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== | ||||
|  | ||||
| postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.38, postcss@>=8.0.9: | ||||
| postcss@^8.4.23, postcss@^8.4.38: | ||||
|   version "8.4.38" | ||||
|   resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz" | ||||
|   integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== | ||||
| @@ -2965,7 +3129,7 @@ prettier-linter-helpers@^1.0.0: | ||||
|   dependencies: | ||||
|     fast-diff "^1.1.2" | ||||
|  | ||||
| prettier@^3.2.5, prettier@>=3.0.0: | ||||
| prettier@^3.2.5: | ||||
|   version "3.2.5" | ||||
|   resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz" | ||||
|   integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== | ||||
| @@ -2993,7 +3157,7 @@ primereact@^10.6.5: | ||||
|     "@types/react-transition-group" "^4.4.1" | ||||
|     react-transition-group "^4.4.1" | ||||
|  | ||||
| prop-types@^15.6.2, prop-types@^15.8.1, prop-types@15.x: | ||||
| prop-types@15.x, prop-types@^15.6.2, prop-types@^15.8.1: | ||||
|   version "15.8.1" | ||||
|   resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" | ||||
|   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== | ||||
| @@ -3012,7 +3176,7 @@ queue-microtask@^1.2.2: | ||||
|   resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" | ||||
|   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== | ||||
|  | ||||
| "react-dom@^17.0.0 || ^18.0.0", "react-dom@>= 16.3.0", react-dom@>=16.6.0, react-dom@>=17, react-dom@>=18, "react-dom@16 || 17 || 18", react-dom@18.2.0: | ||||
| react-dom@18.2.0: | ||||
|   version "18.2.0" | ||||
|   resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" | ||||
|   integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== | ||||
| @@ -3099,7 +3263,7 @@ react-usestateref@^1.0.9: | ||||
|   resolved "https://registry.npmjs.org/react-usestateref/-/react-usestateref-1.0.9.tgz" | ||||
|   integrity sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw== | ||||
|  | ||||
| "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.0 || ^18.0.0", react@^18.2.0, "react@>= 16.3", "react@>= 16.3.0", react@>=16.13.1, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=17, react@>=18, react@>16.0.0, "react@16 || 17 || 18", react@18.2.0: | ||||
| react@18.2.0: | ||||
|   version "18.2.0" | ||||
|   resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" | ||||
|   integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== | ||||
| @@ -3215,7 +3379,7 @@ rollup-plugin-external-globals@^0.10.0: | ||||
|     is-reference "^3.0.2" | ||||
|     magic-string "^0.30.5" | ||||
|  | ||||
| rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, "rollup@^2.25.0 || ^3.3.0 || ^4.1.4", rollup@^4.13.0: | ||||
| rollup@^4.13.0: | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz" | ||||
|   integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ== | ||||
| @@ -3247,7 +3411,7 @@ run-parallel@^1.1.9: | ||||
|   dependencies: | ||||
|     queue-microtask "^1.2.2" | ||||
|  | ||||
| rxjs@^7.8.1, rxjs@>=6, rxjs@>=7: | ||||
| rxjs@^7.8.1: | ||||
|   version "7.8.1" | ||||
|   resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" | ||||
|   integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== | ||||
| @@ -3280,7 +3444,7 @@ sass-loader@^14.2.1: | ||||
|   dependencies: | ||||
|     neo-async "^2.6.2" | ||||
|  | ||||
| sass@*, sass@^1.3.0, sass@^1.77.2: | ||||
| sass@^1.77.2: | ||||
|   version "1.77.2" | ||||
|   resolved "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz" | ||||
|   integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA== | ||||
| @@ -3362,7 +3526,7 @@ slash@^5.0.0, slash@^5.1.0: | ||||
|   resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz" | ||||
|   integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== | ||||
|  | ||||
| source-map-js@^1.2.0, "source-map-js@>=0.6.2 <2.0.0": | ||||
| "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" | ||||
|   integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== | ||||
| @@ -3479,7 +3643,7 @@ synckit@^0.8.6: | ||||
|     "@pkgr/core" "^0.1.0" | ||||
|     tslib "^2.6.2" | ||||
|  | ||||
| tailwindcss@^3.3.6, "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", "tailwindcss@>=3.0.0 || >= 3.0.0-alpha.1", "tailwindcss@>=3.0.0 || insiders": | ||||
| tailwindcss@^3.3.6: | ||||
|   version "3.3.6" | ||||
|   resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz" | ||||
|   integrity sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw== | ||||
| @@ -3619,7 +3783,7 @@ typed-array-length@^1.0.6: | ||||
|     is-typed-array "^1.1.13" | ||||
|     possible-typed-array-names "^1.0.0" | ||||
|  | ||||
| typescript@^5.2.2, typescript@>=4.2.0: | ||||
| typescript@^5.2.2: | ||||
|   version "5.4.5" | ||||
|   resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz" | ||||
|   integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== | ||||
| @@ -3664,7 +3828,7 @@ use-local-storage-state@^19.3.1: | ||||
|   resolved "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-19.3.1.tgz" | ||||
|   integrity sha512-y3Z1dODXvZXZB4qtLDNN8iuXbsYD6TAxz61K58GWB9/yKwrNG9ynI0GzCTHi/Je1rMiyOwMimz0oyFsZn+Kj7Q== | ||||
|  | ||||
| use-sync-external-store@^1.0.0, use-sync-external-store@1.2.0: | ||||
| use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0: | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" | ||||
|   integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== | ||||
| @@ -3692,7 +3856,7 @@ vite-plugin-externals@^0.6.2: | ||||
|     fs-extra "^10.0.0" | ||||
|     magic-string "^0.25.7" | ||||
|  | ||||
| "vite@^4.2.0 || ^5.0.0", vite@^5.0.5, vite@>=2.0.0: | ||||
| vite@^5.0.5: | ||||
|   version "5.2.11" | ||||
|   resolved "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz" | ||||
|   integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ== | ||||
|   | ||||
| @@ -55,7 +55,6 @@ config :wanderer_app, WandererAppWeb.Endpoint, | ||||
| config :wanderer_app, WandererAppWeb.Endpoint, | ||||
|   live_reload: [ | ||||
|     interval: 1000, | ||||
|     web_console_logger: true, | ||||
|     patterns: [ | ||||
|       ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", | ||||
|       ~r"priv/gettext/.*(po)$", | ||||
|   | ||||
| @@ -30,80 +30,89 @@ defmodule WandererApp.Api.Calculations.CalcMapPermissions do | ||||
|  | ||||
|     result = | ||||
|       record.acls | ||||
|       |> Enum.filter(fn acl -> | ||||
|         acl.owner_id in character_ids or | ||||
|           acl.members |> Enum.any?(fn member -> member.eve_character_id in character_eve_ids end) or | ||||
|       |> Enum.reduce([0, 0], fn acl, acc -> | ||||
|         is_owner? = acl.owner_id in character_ids | ||||
|  | ||||
|         is_character_member? = | ||||
|           acl.members |> Enum.any?(fn member -> member.eve_character_id in character_eve_ids end) | ||||
|  | ||||
|         is_corporation_member? = | ||||
|           acl.members | ||||
|           |> Enum.any?(fn member -> member.eve_corporation_id in character_corporation_ids end) or | ||||
|           |> Enum.any?(fn member -> member.eve_corporation_id in character_corporation_ids end) | ||||
|  | ||||
|         is_alliance_member? = | ||||
|           acl.members | ||||
|           |> Enum.any?(fn member -> member.eve_alliance_id in character_alliance_ids end) | ||||
|       end) | ||||
|       |> Enum.reduce([0, 0], fn acl, acc -> | ||||
|         case acc do | ||||
|           [_, -1] -> | ||||
|             [-1, -1] | ||||
|  | ||||
|           [-1, char_acc] -> | ||||
|             char_acl_mask = | ||||
|               acl.members | ||||
|               |> Enum.filter(fn member -> | ||||
|                 member.eve_character_id in character_eve_ids | ||||
|               end) | ||||
|               |> Enum.reduce(0, fn member, acc -> | ||||
|                 case acc do | ||||
|         if is_owner? || is_character_member? || is_corporation_member? || is_alliance_member? do | ||||
|           case acc do | ||||
|             [_, -1] -> | ||||
|               [-1, -1] | ||||
|  | ||||
|             [-1, char_acc] -> | ||||
|               char_acl_mask = | ||||
|                 acl.members | ||||
|                 |> Enum.filter(fn member -> | ||||
|                   member.eve_character_id in character_eve_ids | ||||
|                 end) | ||||
|                 |> Enum.reduce(0, fn member, acc -> | ||||
|                   case acc do | ||||
|                     -1 -> -1 | ||||
|                     _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   end | ||||
|                 end) | ||||
|  | ||||
|               char_acc = | ||||
|                 case char_acl_mask do | ||||
|                   -1 -> -1 | ||||
|                   _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   _ -> char_acc ||| char_acl_mask | ||||
|                 end | ||||
|               end) | ||||
|  | ||||
|             char_acc = | ||||
|               case char_acl_mask do | ||||
|                 -1 -> -1 | ||||
|                 _ -> char_acc ||| char_acl_mask | ||||
|               end | ||||
|               [-1, char_acc] | ||||
|  | ||||
|             [-1, char_acc] | ||||
|             [any_acc, char_acc] -> | ||||
|               any_acl_mask = | ||||
|                 acl.members | ||||
|                 |> Enum.filter(fn member -> | ||||
|                   member.eve_character_id in character_eve_ids || | ||||
|                     member.eve_corporation_id in character_corporation_ids || | ||||
|                     member.eve_alliance_id in character_alliance_ids | ||||
|                 end) | ||||
|                 |> Enum.reduce(0, fn member, acc -> | ||||
|                   case acc do | ||||
|                     -1 -> -1 | ||||
|                     _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   end | ||||
|                 end) | ||||
|  | ||||
|           [any_acc, char_acc] -> | ||||
|             any_acl_mask = | ||||
|               acl.members | ||||
|               |> Enum.filter(fn member -> | ||||
|                 member.eve_character_id in character_eve_ids or | ||||
|                   member.eve_corporation_id in character_corporation_ids or | ||||
|                   member.eve_alliance_id in character_alliance_ids | ||||
|               end) | ||||
|               |> Enum.reduce(0, fn member, acc -> | ||||
|                 case acc do | ||||
|               char_acl_mask = | ||||
|                 acl.members | ||||
|                 |> Enum.filter(fn member -> | ||||
|                   member.eve_character_id in character_eve_ids | ||||
|                 end) | ||||
|                 |> Enum.reduce(0, fn member, acc -> | ||||
|                   case acc do | ||||
|                     -1 -> -1 | ||||
|                     _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   end | ||||
|                 end) | ||||
|  | ||||
|               any_acc = | ||||
|                 case any_acl_mask do | ||||
|                   -1 -> -1 | ||||
|                   _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   _ -> any_acc ||| any_acl_mask | ||||
|                 end | ||||
|               end) | ||||
|  | ||||
|             char_acl_mask = | ||||
|               acl.members | ||||
|               |> Enum.filter(fn member -> | ||||
|                 member.eve_character_id in character_eve_ids | ||||
|               end) | ||||
|               |> Enum.reduce(0, fn member, acc -> | ||||
|                 case acc do | ||||
|               char_acc = | ||||
|                 case char_acl_mask do | ||||
|                   -1 -> -1 | ||||
|                   _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   _ -> char_acc ||| char_acl_mask | ||||
|                 end | ||||
|               end) | ||||
|  | ||||
|             any_acc = | ||||
|               case any_acl_mask do | ||||
|                 -1 -> -1 | ||||
|                 _ -> any_acc ||| any_acl_mask | ||||
|               end | ||||
|  | ||||
|             char_acc = | ||||
|               case char_acl_mask do | ||||
|                 -1 -> -1 | ||||
|                 _ -> char_acc ||| char_acl_mask | ||||
|               end | ||||
|  | ||||
|             [any_acc, char_acc] | ||||
|               [any_acc, char_acc] | ||||
|           end | ||||
|         else | ||||
|           acc | ||||
|         end | ||||
|       end) | ||||
|  | ||||
|   | ||||
| @@ -48,7 +48,13 @@ defmodule WandererApp.Api.MapConnection do | ||||
|       argument(:map_id, :string, allow_nil?: false) | ||||
|       argument(:solar_system_source, :integer, allow_nil?: false) | ||||
|       argument(:solar_system_target, :integer, allow_nil?: false) | ||||
|       filter(expr(map_id == ^arg(:map_id) and solar_system_source == ^arg(:solar_system_source) and solar_system_target == ^arg(:solar_system_target))) | ||||
|  | ||||
|       filter( | ||||
|         expr( | ||||
|           map_id == ^arg(:map_id) and solar_system_source == ^arg(:solar_system_source) and | ||||
|             solar_system_target == ^arg(:solar_system_target) | ||||
|         ) | ||||
|       ) | ||||
|     end | ||||
|  | ||||
|     read :get_link_pairs_advanced do | ||||
|   | ||||
| @@ -38,8 +38,6 @@ defmodule WandererApp.Application do | ||||
|         WandererApp.Character.TrackerManager, | ||||
|         WandererApp.Map.Manager, | ||||
|         WandererApp.Map.ZkbDataFetcher, | ||||
|         WandererApp.Character.ActivityTracker, | ||||
|         WandererApp.User.ActivityTracker, | ||||
|         WandererAppWeb.Presence, | ||||
|         WandererAppWeb.Endpoint | ||||
|       ] ++ maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?()) | ||||
|   | ||||
| @@ -71,11 +71,24 @@ defmodule WandererApp.Character do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def get_character_state!(character_id) do | ||||
|     case get_character_state(character_id) do | ||||
|       {:ok, character_state} -> | ||||
|         character_state | ||||
|  | ||||
|       _ -> | ||||
|         Logger.error("Failed to get character_state #{character_id}") | ||||
|         throw("Failed to get character_state #{character_id}") | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update_character_state(character_id, character_state_update) do | ||||
|     Cachex.get_and_update(:character_state_cache, character_id, fn character_state -> | ||||
|       case character_state do | ||||
|         nil -> | ||||
|           new_state = WandererApp.Character.Tracker.init(character_id: character_id) | ||||
|           :telemetry.execute([:wanderer_app, :character, :tracker, :started], %{count: 1}) | ||||
|  | ||||
|           {:commit, Map.merge(new_state, character_state_update)} | ||||
|  | ||||
|         _ -> | ||||
|   | ||||
| @@ -1,60 +0,0 @@ | ||||
| defmodule WandererApp.Character.ActivityTracker do | ||||
|   @moduledoc false | ||||
|   use GenServer | ||||
|  | ||||
|   require Logger | ||||
|  | ||||
|   @name __MODULE__ | ||||
|  | ||||
|   def start_link(args) do | ||||
|     GenServer.start(__MODULE__, args, name: @name) | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def init(_args) do | ||||
|     Logger.info("#{__MODULE__} started") | ||||
|  | ||||
|     {:ok, %{}, {:continue, :start}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_continue(:start, state) do | ||||
|     :telemetry.attach_many( | ||||
|       "map_character_activity_handler", | ||||
|       [ | ||||
|         [:wanderer_app, :map, :character, :jump] | ||||
|       ], | ||||
|       &WandererApp.Character.ActivityTracker.handle_event/4, | ||||
|       nil | ||||
|     ) | ||||
|  | ||||
|     {:noreply, state} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def terminate(_reason, _state) do | ||||
|     :ok | ||||
|   end | ||||
|  | ||||
|   def handle_event( | ||||
|         [:wanderer_app, :map, :character, :jump], | ||||
|         _event_data, | ||||
|         %{ | ||||
|           character: character, | ||||
|           map_id: map_id, | ||||
|           solar_system_source_id: solar_system_source_id, | ||||
|           solar_system_target_id: solar_system_target_id | ||||
|         } = _metadata, | ||||
|         _config | ||||
|       ) do | ||||
|     {:ok, _} = | ||||
|       WandererApp.Api.MapChainPassages.new(%{ | ||||
|         map_id: map_id, | ||||
|         character_id: character.id, | ||||
|         ship_type_id: character.ship, | ||||
|         ship_name: character.ship_name, | ||||
|         solar_system_source_id: solar_system_source_id, | ||||
|         solar_system_target_id: solar_system_target_id | ||||
|       }) | ||||
|   end | ||||
| end | ||||
| @@ -35,6 +35,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|   @online_error_timeout :timer.minutes(2) | ||||
|   @forbidden_ttl :timer.minutes(1) | ||||
|   @pubsub_client Application.compile_env(:wanderer_app, :pubsub_client) | ||||
|  | ||||
|   def new(), do: __struct__() | ||||
|   def new(args), do: __struct__(args) | ||||
| @@ -53,69 +54,55 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|     {:ok, | ||||
|      character_state | ||||
|      |> _maybe_update_active_maps(track_settings) | ||||
|      |> _maybe_stop_tracking(track_settings) | ||||
|      |> _maybe_start_online_tracking(track_settings) | ||||
|      |> _maybe_start_location_tracking(track_settings) | ||||
|      |> _maybe_start_ship_tracking(track_settings)} | ||||
|      |> maybe_update_active_maps(track_settings) | ||||
|      |> maybe_stop_tracking(track_settings) | ||||
|      |> maybe_start_online_tracking(track_settings) | ||||
|      |> maybe_start_location_tracking(track_settings) | ||||
|      |> maybe_start_ship_tracking(track_settings)} | ||||
|   end | ||||
|  | ||||
|   def update_info(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_info(character_state) | ||||
|   end | ||||
|     WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden") | ||||
|     |> case do | ||||
|       true -> | ||||
|         {:error, :skipped} | ||||
|  | ||||
|   def update_ship(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_ship(character_state) | ||||
|   end | ||||
|       false -> | ||||
|         {:ok, %{eve_id: eve_id}} = WandererApp.Character.get_character(character_id) | ||||
|  | ||||
|   def update_location(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_location(character_state) | ||||
|   end | ||||
|         case WandererApp.Esi.get_character_info(eve_id) do | ||||
|           {:ok, info} -> | ||||
|             {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|             update = maybe_update_corporation(character_state, info) | ||||
|             WandererApp.Character.update_character_state(character_id, update) | ||||
|  | ||||
|   def update_online(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_online(character_state) | ||||
|   end | ||||
|             :ok | ||||
|  | ||||
|   def check_online_errors(character_id) do | ||||
|     case(WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")) do | ||||
|       nil -> | ||||
|         :skip | ||||
|           {:error, :forbidden} -> | ||||
|             Logger.warning("#{__MODULE__} failed to get_character_info: forbidden") | ||||
|  | ||||
|       error_time -> | ||||
|         duration = DateTime.diff(DateTime.utc_now(), error_time, :second) | ||||
|             WandererApp.Cache.put( | ||||
|               "character:#{character_id}:info_forbidden", | ||||
|               true, | ||||
|               ttl: @forbidden_ttl | ||||
|             ) | ||||
|  | ||||
|         if duration >= @online_error_timeout do | ||||
|           {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|           WandererApp.Cache.delete("character:#{character_id}:online_forbidden") | ||||
|           WandererApp.Cache.delete("character:#{character_id}:online_error_time") | ||||
|           WandererApp.Character.update_character(character_id, %{online: false}) | ||||
|           WandererApp.Cache.delete("character:#{character_id}:location_started") | ||||
|           WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id") | ||||
|             {:error, :forbidden} | ||||
|  | ||||
|           WandererApp.Character.update_character_state(character_id, %{ | ||||
|             character_state | ||||
|             | is_online: false, | ||||
|               track_ship: false, | ||||
|               track_location: false | ||||
|           }) | ||||
|  | ||||
|           :ok | ||||
|         else | ||||
|           :skip | ||||
|           {:error, error} -> | ||||
|             Logger.error("#{__MODULE__} failed to get_character_info: #{inspect(error)}") | ||||
|             {:error, error} | ||||
|         end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update_wallet(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_wallet(character_state) | ||||
|   def update_ship(character_id) when is_binary(character_id) do | ||||
|     character_id | ||||
|     |> WandererApp.Character.get_character_state!() | ||||
|     |> update_ship() | ||||
|   end | ||||
|  | ||||
|   defp _update_ship(%{character_id: character_id, track_ship: true} = character_state) do | ||||
|   def update_ship(%{character_id: character_id, track_ship: true} = character_state) do | ||||
|     case WandererApp.Character.get_character(character_id) do | ||||
|       {:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) -> | ||||
|         WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden") | ||||
| @@ -123,14 +110,14 @@ defmodule WandererApp.Character.Tracker do | ||||
|           true -> | ||||
|             {:error, :skipped} | ||||
|  | ||||
|           false -> | ||||
|           _ -> | ||||
|             case WandererApp.Esi.get_character_ship(eve_id, | ||||
|                    access_token: access_token, | ||||
|                    character_id: character_id, | ||||
|                    refresh_token?: true | ||||
|                  ) do | ||||
|               {:ok, ship} -> | ||||
|                 character_state |> _maybe_update_ship(ship) | ||||
|                 character_state |> maybe_update_ship(ship) | ||||
|  | ||||
|                 :ok | ||||
|  | ||||
| @@ -156,9 +143,68 @@ defmodule WandererApp.Character.Tracker do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_ship(_), do: {:error, :skipped} | ||||
|   def update_ship(_), do: {:error, :skipped} | ||||
|  | ||||
|   defp _update_online(%{track_online: true, character_id: character_id} = character_state) do | ||||
|   def update_location(character_id) when is_binary(character_id) do | ||||
|     character_id | ||||
|     |> WandererApp.Character.get_character_state!() | ||||
|     |> update_location() | ||||
|   end | ||||
|  | ||||
|   def update_location(%{track_location: true, character_id: character_id} = character_state) do | ||||
|     case WandererApp.Character.get_character(character_id) do | ||||
|       {:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) -> | ||||
|         WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden") | ||||
|         |> case do | ||||
|           true -> | ||||
|             {:error, :skipped} | ||||
|  | ||||
|           _ -> | ||||
|             case WandererApp.Esi.get_character_location(eve_id, | ||||
|                    access_token: access_token, | ||||
|                    character_id: character_id, | ||||
|                    refresh_token?: true | ||||
|                  ) do | ||||
|               {:ok, location} -> | ||||
|                 character_state | ||||
|                 |> maybe_update_location(location) | ||||
|  | ||||
|                 :ok | ||||
|  | ||||
|               {:error, :forbidden} -> | ||||
|                 Logger.warning("#{__MODULE__} failed to update_location: forbidden") | ||||
|  | ||||
|                 WandererApp.Cache.put( | ||||
|                   "character:#{character_id}:location_forbidden", | ||||
|                   true, | ||||
|                   ttl: @forbidden_ttl | ||||
|                 ) | ||||
|  | ||||
|                 {:error, :forbidden} | ||||
|  | ||||
|               {:error, error} -> | ||||
|                 Logger.error("#{__MODULE__} failed to update_location: #{inspect(error)}") | ||||
|                 {:error, error} | ||||
|             end | ||||
|  | ||||
|           _ -> | ||||
|             {:error, :skipped} | ||||
|         end | ||||
|  | ||||
|       _ -> | ||||
|         {:error, :skipped} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update_location(_), do: {:error, :skipped} | ||||
|  | ||||
|   def update_online(character_id) when is_binary(character_id) do | ||||
|     character_id | ||||
|     |> WandererApp.Character.get_character_state!() | ||||
|     |> update_online() | ||||
|   end | ||||
|  | ||||
|   def update_online(%{track_online: true, character_id: character_id} = character_state) do | ||||
|     case WandererApp.Character.get_character(character_id) do | ||||
|       {:ok, %{eve_id: eve_id, access_token: access_token}} | ||||
|       when not is_nil(access_token) -> | ||||
| @@ -167,14 +213,14 @@ defmodule WandererApp.Character.Tracker do | ||||
|           true -> | ||||
|             {:error, :skipped} | ||||
|  | ||||
|           false -> | ||||
|           _ -> | ||||
|             case WandererApp.Esi.get_character_online(eve_id, | ||||
|                    access_token: access_token, | ||||
|                    character_id: character_id, | ||||
|                    refresh_token?: true | ||||
|                  ) do | ||||
|               {:ok, online} -> | ||||
|                 online = _get_online(online) | ||||
|                 online = get_online(online) | ||||
|  | ||||
|                 WandererApp.Cache.delete("character:#{character_id}:online_forbidden") | ||||
|                 WandererApp.Cache.delete("character:#{character_id}:online_error_time") | ||||
| @@ -240,57 +286,43 @@ defmodule WandererApp.Character.Tracker do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_online(_), do: {:error, :skipped} | ||||
|   def update_online(_), do: {:error, :skipped} | ||||
|  | ||||
|   defp _update_location(%{track_location: true, character_id: character_id} = character_state) do | ||||
|     case WandererApp.Character.get_character(character_id) do | ||||
|       {:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) -> | ||||
|         WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden") | ||||
|         |> case do | ||||
|           true -> | ||||
|             {:error, :skipped} | ||||
|   def check_online_errors(character_id) do | ||||
|     WandererApp.Cache.lookup!("character:#{character_id}:online_error_time") | ||||
|     |> case do | ||||
|       nil -> | ||||
|         :skip | ||||
|  | ||||
|           false -> | ||||
|             case WandererApp.Esi.get_character_location(eve_id, | ||||
|                    access_token: access_token, | ||||
|                    character_id: character_id, | ||||
|                    refresh_token?: true | ||||
|                  ) do | ||||
|               {:ok, location} -> | ||||
|                 character_state | ||||
|                 |> _maybe_update_location(location) | ||||
|       error_time -> | ||||
|         duration = DateTime.diff(DateTime.utc_now(), error_time, :second) | ||||
|  | ||||
|                 :ok | ||||
|         if duration >= @online_error_timeout do | ||||
|           {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|           WandererApp.Cache.delete("character:#{character_id}:online_forbidden") | ||||
|           WandererApp.Cache.delete("character:#{character_id}:online_error_time") | ||||
|           WandererApp.Character.update_character(character_id, %{online: false}) | ||||
|           WandererApp.Cache.delete("character:#{character_id}:location_started") | ||||
|           WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id") | ||||
|  | ||||
|               {:error, :forbidden} -> | ||||
|                 Logger.warning("#{__MODULE__} failed to update_location: forbidden") | ||||
|           WandererApp.Character.update_character_state(character_id, %{ | ||||
|             character_state | ||||
|             | is_online: false, | ||||
|               track_ship: false, | ||||
|               track_location: false | ||||
|           }) | ||||
|  | ||||
|                 WandererApp.Cache.put( | ||||
|                   "character:#{character_id}:location_forbidden", | ||||
|                   true, | ||||
|                   ttl: @forbidden_ttl | ||||
|                 ) | ||||
|  | ||||
|                 {:error, :forbidden} | ||||
|  | ||||
|               {:error, error} -> | ||||
|                 Logger.error("#{__MODULE__} failed to update_location: #{inspect(error)}") | ||||
|                 {:error, error} | ||||
|             end | ||||
|  | ||||
|           _ -> | ||||
|             {:error, :skipped} | ||||
|           :ok | ||||
|         else | ||||
|           :skip | ||||
|         end | ||||
|  | ||||
|       _ -> | ||||
|         {:error, :skipped} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_location(_), do: {:error, :skipped} | ||||
|  | ||||
|   defp _update_wallet(%{character_id: character_id} = state) do | ||||
|     case WandererApp.Character.get_character(character_id) do | ||||
|   def update_wallet(character_id) do | ||||
|     character_id | ||||
|     |> WandererApp.Character.get_character() | ||||
|     |> case do | ||||
|       {:ok, %{eve_id: eve_id, access_token: access_token} = character} | ||||
|       when not is_nil(access_token) -> | ||||
|         character | ||||
| @@ -302,7 +334,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|               true -> | ||||
|                 {:error, :skipped} | ||||
|  | ||||
|               false -> | ||||
|               _ -> | ||||
|                 case WandererApp.Esi.get_character_wallet(eve_id, | ||||
|                        params: %{datasource: "tranquility"}, | ||||
|                        access_token: access_token, | ||||
| @@ -310,7 +342,8 @@ defmodule WandererApp.Character.Tracker do | ||||
|                        refresh_token?: true | ||||
|                      ) do | ||||
|                   {:ok, result} -> | ||||
|                     state |> _maybe_update_wallet(result) | ||||
|                     {:ok, state} = WandererApp.Character.get_character_state(character_id) | ||||
|                     maybe_update_wallet(state, result) | ||||
|  | ||||
|                     :ok | ||||
|  | ||||
| @@ -340,42 +373,10 @@ defmodule WandererApp.Character.Tracker do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_info(%{character_id: character_id} = character_state) do | ||||
|     {:ok, %{eve_id: eve_id}} = WandererApp.Character.get_character(character_id) | ||||
|  | ||||
|     WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden") | ||||
|   defp update_alliance(%{character_id: character_id} = state, alliance_id) do | ||||
|     alliance_id | ||||
|     |> WandererApp.Esi.get_alliance_info() | ||||
|     |> case do | ||||
|       true -> | ||||
|         {:error, :skipped} | ||||
|  | ||||
|       false -> | ||||
|         case WandererApp.Esi.get_character_info(eve_id) do | ||||
|           {:ok, info} -> | ||||
|             update = character_state |> _maybe_update_corporation(info) | ||||
|             WandererApp.Character.update_character_state(character_id, update) | ||||
|  | ||||
|             :ok | ||||
|  | ||||
|           {:error, :forbidden} -> | ||||
|             Logger.warning("#{__MODULE__} failed to get_character_info: forbidden") | ||||
|  | ||||
|             WandererApp.Cache.put( | ||||
|               "character:#{character_id}:info_forbidden", | ||||
|               true, | ||||
|               ttl: @forbidden_ttl | ||||
|             ) | ||||
|  | ||||
|             {:error, :forbidden} | ||||
|  | ||||
|           {:error, error} -> | ||||
|             Logger.error("#{__MODULE__} failed to get_character_info: #{inspect(error)}") | ||||
|             {:error, error} | ||||
|         end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_alliance(%{character_id: character_id} = state, alliance_id) do | ||||
|     case WandererApp.Esi.get_alliance_info(alliance_id) do | ||||
|       {:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} -> | ||||
|         {:ok, character} = WandererApp.Character.get_character(character_id) | ||||
|  | ||||
| @@ -390,7 +391,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|         WandererApp.Character.update_character(character_id, character_update) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
|         @pubsub_client.broadcast( | ||||
|           WandererApp.PubSub, | ||||
|           "character:#{character_id}:alliance", | ||||
|           {:character_alliance, {character_id, character_update}} | ||||
| @@ -404,8 +405,10 @@ defmodule WandererApp.Character.Tracker do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_corporation(%{character_id: character_id} = state, corporation_id) do | ||||
|     case WandererApp.Esi.get_corporation_info(corporation_id) do | ||||
|   defp update_corporation(%{character_id: character_id} = state, corporation_id) do | ||||
|     corporation_id | ||||
|     |> WandererApp.Esi.get_corporation_info() | ||||
|     |> case do | ||||
|       {:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} -> | ||||
|         alliance_id = Map.get(corporation_info, "alliance_id") | ||||
|  | ||||
| @@ -424,7 +427,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|         WandererApp.Character.update_character(character_id, character_update) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
|         @pubsub_client.broadcast( | ||||
|           WandererApp.PubSub, | ||||
|           "character:#{character_id}:corporation", | ||||
|           {:character_corporation, | ||||
| @@ -438,7 +441,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|         state | ||||
|         |> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id}) | ||||
|         |> _maybe_update_alliance() | ||||
|         |> maybe_update_alliance() | ||||
|  | ||||
|       _error -> | ||||
|         Logger.warning("Failed to get corporation info for #{corporation_id}") | ||||
| @@ -446,7 +449,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_ship( | ||||
|   defp maybe_update_ship( | ||||
|          %{ | ||||
|            character_id: character_id | ||||
|          } = | ||||
| @@ -459,38 +462,33 @@ defmodule WandererApp.Character.Tracker do | ||||
|     {:ok, %{ship: old_ship_type_id, ship_name: old_ship_name} = character} = | ||||
|       WandererApp.Character.get_character(character_id) | ||||
|  | ||||
|     case old_ship_type_id != ship_type_id or old_ship_name != ship_name do | ||||
|       true -> | ||||
|         character_update = %{ | ||||
|           ship: ship_type_id, | ||||
|           ship_name: ship_name | ||||
|         } | ||||
|     ship_updated = old_ship_type_id != ship_type_id || old_ship_name != ship_name | ||||
|  | ||||
|         {:ok, _character} = | ||||
|           WandererApp.Api.Character.update_ship(character, character_update) | ||||
|     if ship_updated do | ||||
|       character_update = %{ | ||||
|         ship: ship_type_id, | ||||
|         ship_name: ship_name | ||||
|       } | ||||
|  | ||||
|         WandererApp.Character.update_character(character_id, character_update) | ||||
|       {:ok, _character} = | ||||
|         WandererApp.Api.Character.update_ship(character, character_update) | ||||
|  | ||||
|         state | ||||
|  | ||||
|       _ -> | ||||
|         state | ||||
|       WandererApp.Character.update_character(character_id, character_update) | ||||
|     end | ||||
|  | ||||
|     state | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_location( | ||||
|   defp maybe_update_location( | ||||
|          %{ | ||||
|            character_id: character_id | ||||
|          } = | ||||
|            state, | ||||
|          location | ||||
|        ) do | ||||
|     location = _get_location(location) | ||||
|     location = get_location(location) | ||||
|  | ||||
|     if not WandererApp.Cache.lookup!( | ||||
|          "character:#{character_id}:location_started", | ||||
|          false | ||||
|        ) do | ||||
|     if not is_location_started?(character_id) do | ||||
|       WandererApp.Cache.lookup!("character:#{character_id}:start_solar_system_id", nil) | ||||
|       |> case do | ||||
|         nil -> | ||||
| @@ -512,58 +510,51 @@ defmodule WandererApp.Character.Tracker do | ||||
|     {:ok, %{solar_system_id: solar_system_id, structure_id: structure_id} = character} = | ||||
|       WandererApp.Character.get_character(character_id) | ||||
|  | ||||
|     WandererApp.Cache.lookup!( | ||||
|       "character:#{character_id}:location_started", | ||||
|       false | ||||
|     ) | ||||
|     (not is_location_started?(character_id) || | ||||
|        is_location_updated?(location, solar_system_id, structure_id)) | ||||
|     |> case do | ||||
|       true -> | ||||
|         case solar_system_id != location.solar_system_id or | ||||
|                structure_id != location.structure_id do | ||||
|           true -> | ||||
|             {:ok, _character} = WandererApp.Api.Character.update_location(character, location) | ||||
|  | ||||
|             WandererApp.Character.update_character(character_id, location) | ||||
|  | ||||
|             :ok | ||||
|  | ||||
|           _ -> | ||||
|             :ok | ||||
|         end | ||||
|  | ||||
|       false -> | ||||
|         {:ok, _character} = WandererApp.Api.Character.update_location(character, location) | ||||
|  | ||||
|         WandererApp.Character.update_character(character_id, location) | ||||
|  | ||||
|         :ok | ||||
|  | ||||
|       _ -> | ||||
|         :ok | ||||
|     end | ||||
|  | ||||
|     state | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_corporation( | ||||
|   defp is_location_started?(character_id), | ||||
|     do: | ||||
|       WandererApp.Cache.lookup!( | ||||
|         "character:#{character_id}:location_started", | ||||
|         false | ||||
|       ) | ||||
|  | ||||
|   defp is_location_updated?(location, solar_system_id, structure_id), | ||||
|     do: | ||||
|       solar_system_id != location.solar_system_id || | ||||
|         structure_id != location.structure_id | ||||
|  | ||||
|   defp maybe_update_corporation( | ||||
|          state, | ||||
|          %{ | ||||
|            "corporation_id" => corporation_id | ||||
|          } = _info | ||||
|        ) do | ||||
|     case corporation_id do | ||||
|       nil -> | ||||
|         state | ||||
|        ) | ||||
|        when not is_nil(corporation_id), | ||||
|        do: update_corporation(state, corporation_id) | ||||
|  | ||||
|       _ -> | ||||
|         _update_corporation(state, corporation_id) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_corporation( | ||||
|   defp maybe_update_corporation( | ||||
|          state, | ||||
|          _info | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_update_alliance( | ||||
|   defp maybe_update_alliance( | ||||
|          %{character_id: character_id, alliance_id: alliance_id} = | ||||
|            state | ||||
|        ) do | ||||
| @@ -582,7 +573,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|         WandererApp.Character.update_character(character_id, character_update) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
|         @pubsub_client.broadcast( | ||||
|           WandererApp.PubSub, | ||||
|           "character:#{character_id}:alliance", | ||||
|           {:character_alliance, {character_id, character_update}} | ||||
| @@ -591,11 +582,11 @@ defmodule WandererApp.Character.Tracker do | ||||
|         state | ||||
|  | ||||
|       _ -> | ||||
|         _update_alliance(state, alliance_id) | ||||
|         update_alliance(state, alliance_id) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_wallet( | ||||
|   defp maybe_update_wallet( | ||||
|          %{character_id: character_id} = | ||||
|            state, | ||||
|          wallet_balance | ||||
| @@ -611,7 +602,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|       eve_wallet_balance: wallet_balance | ||||
|     }) | ||||
|  | ||||
|     Phoenix.PubSub.broadcast( | ||||
|     @pubsub_client.broadcast( | ||||
|       WandererApp.PubSub, | ||||
|       "character:#{character_id}", | ||||
|       {:character_wallet_balance} | ||||
| @@ -620,7 +611,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|     state | ||||
|   end | ||||
|  | ||||
|   defp _maybe_start_online_tracking( | ||||
|   defp maybe_start_online_tracking( | ||||
|          state, | ||||
|          %{track_online: true} = _track_settings | ||||
|        ), | ||||
| @@ -631,38 +622,37 @@ defmodule WandererApp.Character.Tracker do | ||||
|            track_ship: true | ||||
|        } | ||||
|  | ||||
|   defp _maybe_start_online_tracking( | ||||
|   defp maybe_start_online_tracking( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_start_location_tracking( | ||||
|   defp maybe_start_location_tracking( | ||||
|          state, | ||||
|          %{track_location: true} = _track_settings | ||||
|        ) do | ||||
|     %{state | track_location: true} | ||||
|   end | ||||
|        ), | ||||
|        do: %{state | track_location: true} | ||||
|  | ||||
|   defp _maybe_start_location_tracking( | ||||
|   defp maybe_start_location_tracking( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_start_ship_tracking( | ||||
|   defp maybe_start_ship_tracking( | ||||
|          state, | ||||
|          %{track_ship: true} = _track_settings | ||||
|        ), | ||||
|        do: %{state | track_ship: true} | ||||
|  | ||||
|   defp _maybe_start_ship_tracking( | ||||
|   defp maybe_start_ship_tracking( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_update_active_maps( | ||||
|   defp maybe_update_active_maps( | ||||
|          %{character_id: character_id, active_maps: active_maps} = | ||||
|            state, | ||||
|          %{map_id: map_id, track: true} = _track_settings | ||||
| @@ -677,11 +667,12 @@ defmodule WandererApp.Character.Tracker do | ||||
|     %{state | active_maps: [map_id | active_maps] |> Enum.uniq()} | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_active_maps( | ||||
|   defp maybe_update_active_maps( | ||||
|          %{character_id: character_id, active_maps: active_maps} = state, | ||||
|          %{map_id: map_id, track: false} = _track_settings | ||||
|        ) do | ||||
|     case WandererApp.Cache.take("character:#{character_id}:map:#{map_id}:tracking_start_time") do | ||||
|     WandererApp.Cache.take("character:#{character_id}:map:#{map_id}:tracking_start_time") | ||||
|     |> case do | ||||
|       start_time when not is_nil(start_time) -> | ||||
|         duration = DateTime.diff(DateTime.utc_now(), start_time, :second) | ||||
|         :telemetry.execute([:wanderer_app, :character, :tracker], %{duration: duration}) | ||||
| @@ -695,13 +686,13 @@ defmodule WandererApp.Character.Tracker do | ||||
|     %{state | active_maps: Enum.filter(active_maps, &(&1 != map_id))} | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_active_maps( | ||||
|   defp maybe_update_active_maps( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_stop_tracking( | ||||
|   defp maybe_stop_tracking( | ||||
|          %{active_maps: [], character_id: character_id, opts: opts} = state, | ||||
|          _track_settings | ||||
|        ) do | ||||
| @@ -722,25 +713,21 @@ defmodule WandererApp.Character.Tracker do | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   defp _maybe_stop_tracking( | ||||
|   defp maybe_stop_tracking( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _get_location(%{"solar_system_id" => solar_system_id, "structure_id" => structure_id}) do | ||||
|     %{solar_system_id: solar_system_id, structure_id: structure_id} | ||||
|   end | ||||
|   defp get_location(%{"solar_system_id" => solar_system_id, "structure_id" => structure_id}), | ||||
|     do: %{solar_system_id: solar_system_id, structure_id: structure_id} | ||||
|  | ||||
|   defp _get_location(%{"solar_system_id" => solar_system_id}) do | ||||
|     %{solar_system_id: solar_system_id, structure_id: nil} | ||||
|   end | ||||
|   defp get_location(%{"solar_system_id" => solar_system_id}), | ||||
|     do: %{solar_system_id: solar_system_id, structure_id: nil} | ||||
|  | ||||
|   defp _get_location(_), do: %{solar_system_id: nil, structure_id: nil} | ||||
|   defp get_location(_), do: %{solar_system_id: nil, structure_id: nil} | ||||
|  | ||||
|   defp _get_online(%{"online" => online}) do | ||||
|     %{online: online} | ||||
|   end | ||||
|   defp get_online(%{"online" => online}), do: %{online: online} | ||||
|  | ||||
|   defp _get_online(_), do: %{} | ||||
|   defp get_online(_), do: %{} | ||||
| end | ||||
|   | ||||
| @@ -46,9 +46,7 @@ defmodule WandererApp.Character.TrackerManager do | ||||
|   def handle_call(:error, _, state), do: {:stop, :error, :ok, state} | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call(:stop, _, state) do | ||||
|     {:stop, :normal, :ok, state} | ||||
|   end | ||||
|   def handle_call(:stop, _, state), do: {:stop, :normal, :ok, state} | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call( | ||||
|   | ||||
| @@ -70,12 +70,10 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|       false -> | ||||
|         Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end) | ||||
|  | ||||
|         Task.start_link(fn -> | ||||
|           WandererApp.Character.update_character_state(character_id, %{opts: opts}) | ||||
|           :telemetry.execute([:wanderer_app, :character, :tracker, :started], %{count: 1}) | ||||
|  | ||||
|           :ok | ||||
|         end) | ||||
|         WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [ | ||||
|           character_id, | ||||
|           %{opts: opts} | ||||
|         ]) | ||||
|  | ||||
|         tracked_characters = [character_id | state.characters] |> Enum.uniq() | ||||
|         WandererApp.Cache.insert("tracked_characters", tracked_characters) | ||||
| @@ -180,9 +178,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_online(character_id) | ||||
|       end) | ||||
|       WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [ | ||||
|         character_id | ||||
|       ]) | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -207,10 +205,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|     Process.send_after(self(), :check_online_errors, @check_online_errors_interval) | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.check_online_errors(character_id) | ||||
|       end) | ||||
|     |> Task.async_stream( | ||||
|       fn character_id -> | ||||
|         WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :check_online_errors, [ | ||||
|           character_id | ||||
|         ]) | ||||
|       end, | ||||
|       timeout: :timer.seconds(15), | ||||
|       max_concurrency: System.schedulers_online(), | ||||
|       on_timeout: :kill_task | ||||
|     ) | ||||
|     |> Enum.each(fn | ||||
|       {:ok, _result} -> :ok | ||||
|       {:error, reason} -> @logger.error("Error in check_online_errors: #{inspect(reason)}") | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -228,9 +235,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_location(character_id) | ||||
|       end) | ||||
|       WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [ | ||||
|         character_id | ||||
|       ]) | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -257,9 +264,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_ship(character_id) | ||||
|       end) | ||||
|       WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [ | ||||
|         character_id | ||||
|       ]) | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -285,10 +292,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|     Process.send_after(self(), :update_info, @update_info_interval) | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_info(character_id) | ||||
|       end) | ||||
|     |> Task.async_stream( | ||||
|       fn character_id -> | ||||
|         WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [ | ||||
|           character_id | ||||
|         ]) | ||||
|       end, | ||||
|       timeout: :timer.seconds(15), | ||||
|       max_concurrency: System.schedulers_online(), | ||||
|       on_timeout: :kill_task | ||||
|     ) | ||||
|     |> Enum.each(fn | ||||
|       {:ok, _result} -> :ok | ||||
|       {:error, reason} -> @logger.error("Error in update_info: #{inspect(reason)}") | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -314,10 +330,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|     Process.send_after(self(), :update_wallet, @update_wallet_interval) | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_wallet(character_id) | ||||
|       end) | ||||
|     |> Task.async_stream( | ||||
|       fn character_id -> | ||||
|         WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [ | ||||
|           character_id | ||||
|         ]) | ||||
|       end, | ||||
|       timeout: :timer.seconds(15), | ||||
|       max_concurrency: System.schedulers_online(), | ||||
|       on_timeout: :kill_task | ||||
|     ) | ||||
|     |> Enum.each(fn | ||||
|       {:ok, _result} -> :ok | ||||
|       {:error, reason} -> @logger.error("Error in update_wallet: #{inspect(reason)}") | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -358,7 +383,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|             end | ||||
|         end | ||||
|       end, | ||||
|       max_concurrency: 20, | ||||
|       max_concurrency: System.schedulers_online(), | ||||
|       on_timeout: :kill_task, | ||||
|       timeout: :timer.seconds(15) | ||||
|     ) | ||||
| @@ -394,7 +419,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|  | ||||
|         WandererApp.Character.update_character_state(character_id, character_state) | ||||
|       end, | ||||
|       max_concurrency: 20, | ||||
|       max_concurrency: System.schedulers_online(), | ||||
|       on_timeout: :kill_task, | ||||
|       timeout: :timer.seconds(30) | ||||
|     ) | ||||
| @@ -404,7 +429,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|   end | ||||
|  | ||||
|   def handle_info({:stop_track, character_id}, state) do | ||||
|     Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end) | ||||
|     @logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end) | ||||
|     stop_tracking(state, character_id) | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -1,42 +0,0 @@ | ||||
| defmodule DDRT do | ||||
|   use DDRT.DynamicRtree | ||||
|   alias DDRT.DynamicRtree | ||||
|  | ||||
|   @moduledoc """ | ||||
|   This is the top-level `DDRT` module. Use this to create a distributed r-tree. If you're only interested in using this package for the r-tree implementation, you should instead use `DDRT.DynamicRtree` | ||||
|  | ||||
|   Please refer to `DDRT.DynamicRtree` module documentation for complete function specs and examples for general usage of the core API methods. | ||||
|   """ | ||||
|  | ||||
|   # DDRT party begins. | ||||
|   @spec start_link(DynamicRtree.tree_config()) :: {:ok, pid} | ||||
|   @doc "See `DDRT.DynamicRtree.start_link/1` for documentation and configuration parameters" | ||||
|   def start_link(opts) do | ||||
|     name = Keyword.get(opts, :name, DynamicRtree) | ||||
|  | ||||
|     children = [ | ||||
|       {DeltaCrdt, | ||||
|        [ | ||||
|          crdt: DeltaCrdt.AWLWWMap, | ||||
|          name: Module.concat([name, Crdt]), | ||||
|          on_diffs: &on_diffs(&1, DynamicRtree, name) | ||||
|        ]}, | ||||
|       {DynamicRtree, | ||||
|        [ | ||||
|          conf: Keyword.put_new(opts, :mode, :distributed), | ||||
|          crdt: Module.concat([name, Crdt]), | ||||
|          name: name | ||||
|        ]} | ||||
|     ] | ||||
|  | ||||
|     Supervisor.start_link(children, | ||||
|       strategy: :one_for_one, | ||||
|       name: Module.concat([name, Supervisor]) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   @doc false | ||||
|   def on_diffs(diffs, mod, name) do | ||||
|     mod.merge_diffs(diffs, name) | ||||
|   end | ||||
| end | ||||
| @@ -1,725 +0,0 @@ | ||||
| defmodule DDRT.DynamicRtree do | ||||
|   use GenServer, restart: :transient | ||||
|   use DDRT.DynamicRtreeImpl | ||||
|  | ||||
|   @type tree_init :: [ | ||||
|           name: GenServer.name(), | ||||
|           crdt: module(), | ||||
|           conf: tree_config() | ||||
|         ] | ||||
|  | ||||
|   @type tree_config :: [ | ||||
|           name: GenServer.name(), | ||||
|           width: integer(), | ||||
|           type: module(), | ||||
|           verbose: boolean(), | ||||
|           seed: integer(), | ||||
|           mode: ddrt_mode() | ||||
|         ] | ||||
|  | ||||
|   @type ddrt_mode :: :standalone | :distributed | ||||
|   @type coord_range :: {number(), number()} | ||||
|   @type bounding_box :: list(coord_range()) | ||||
|   @type id :: number() | String.t() | ||||
|   @type leaf :: {id(), bounding_box()} | ||||
|   @type member :: GenServer.name() | {GenServer.name(), node()} | ||||
|  | ||||
|   @callback delete(ids :: id() | [id()], name :: GenServer.name()) :: | ||||
|               {:ok, map()} | {:badtree, map()} | ||||
|   @callback insert(leaves :: leaf() | [leaf()], name :: GenServer.name()) :: | ||||
|               {:ok, map()} | {:badtree, map()} | ||||
|   @callback metadata(name :: GenServer.name()) :: map() | ||||
|   @callback pquery(box :: bounding_box(), depth :: integer(), name :: GenServer.name()) :: | ||||
|               {:ok, [id()]} | {:badtree, map()} | ||||
|   @callback query(box :: bounding_box(), name :: GenServer.name()) :: | ||||
|               {:ok, [id()]} | {:badtree, map()} | ||||
|   @callback update( | ||||
|               ids :: id(), | ||||
|               box :: bounding_box() | {bounding_box(), bounding_box()}, | ||||
|               name :: GenServer.name() | ||||
|             ) :: {:ok, map()} | {:badtree, map()} | ||||
|   @callback bulk_update(leaves :: [leaf()], name :: GenServer.name()) :: | ||||
|               {:ok, map()} | {:badtree, map()} | ||||
|   @callback new(opts :: Keyword.t(), name :: GenServer.name()) :: {:ok, map()} | ||||
|   @callback tree(name :: GenServer.name()) :: map() | ||||
|   @callback set_members(name :: GenServer.name(), [member()]) :: :ok | ||||
|  | ||||
|   @doc false | ||||
|   defmacro doc_referral({name, arity}) do | ||||
|     "See `DDRT.DynamicRtree.#{name}/#{arity}` for documentation and usage examples." | ||||
|   end | ||||
|  | ||||
|   defmacro __using__(_) do | ||||
|     quote do | ||||
|       alias DDRT.DynamicRtree | ||||
|       @behaviour DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:delete, 2})) | ||||
|       defdelegate delete(ids, name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:insert, 2})) | ||||
|       defdelegate insert(leaves, name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:metadata, 1})) | ||||
|       defdelegate metadata(name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:pquery, 3})) | ||||
|       defdelegate pquery(box, depth, name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:query, 2})) | ||||
|       defdelegate query(box, name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:update, 3})) | ||||
|       defdelegate update(ids, box, name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:bulk_update, 2})) | ||||
|       defdelegate bulk_update(leaves, name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:new, 2})) | ||||
|       defdelegate new(opts, name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:tree, 1})) | ||||
|       defdelegate tree(name), to: DynamicRtree | ||||
|  | ||||
|       @doc unquote(doc_referral({:set_members, 2})) | ||||
|       defdelegate set_members(name, members), to: DynamicRtree | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defstruct metadata: nil, | ||||
|             tree: nil, | ||||
|             listeners: [], | ||||
|             crdt: nil, | ||||
|             name: nil | ||||
|  | ||||
|   @moduledoc """ | ||||
|   Use this module if you're interested in creating an R-Tree optimized to run on a single machine. If you'd instead like to run a distributed R-Tree on a cluster of Elixir nodes, use the `DDRT` module. | ||||
|   """ | ||||
|  | ||||
|   @doc """ | ||||
|   These are all of the possible configuration parameters for `opts` and their default values: | ||||
|  | ||||
|   - **name**: The name of the DDRT process. Defaults to `DDRT` | ||||
|   - **width**: The max number of children a node may have. Defaults to `6` | ||||
|   - **verbose**: allows `Logger` to report console logs. (Also decreases performance). Defaults to `false`. | ||||
|   - **seed**: Sets the seed value for the pseudo-random number generator which generates the unique IDs for each node in the tree. This is a deterministic process; so the same seed value will guarantee the same pseudo-random unique IDs being generated for your tree in the same order each time. Defaults to `0` | ||||
|   """ | ||||
|   @spec start_link(opts :: tree_init()) :: {:ok, pid()} | {:error, term()} | ||||
|   def start_link(opts) do | ||||
|     name = Keyword.get(opts, :name, DDRT) | ||||
|     GenServer.start_link(__MODULE__, opts, name: name) | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def init(opts) do | ||||
|     conf = filter_conf(opts[:conf]) | ||||
|     {t, meta} = tree_new(conf) | ||||
|     listeners = Node.list() | ||||
|  | ||||
|     t = | ||||
|       if %{metadata: meta} |> is_distributed? do | ||||
|         DeltaCrdt.set_neighbours(opts[:crdt], Enum.map(Node.list(), fn x -> {opts[:crdt], x} end)) | ||||
|  | ||||
|         crdt_value = DeltaCrdt.to_map(opts[:crdt]) | ||||
|         :net_kernel.monitor_nodes(true, node_type: :visible) | ||||
|         if crdt_value != %{}, do: reconstruct_from_crdt(crdt_value, t), else: t | ||||
|       else | ||||
|         t | ||||
|       end | ||||
|  | ||||
|     {:ok, | ||||
|      %__MODULE__{ | ||||
|        name: opts[:name], | ||||
|        metadata: meta, | ||||
|        tree: t, | ||||
|        listeners: listeners, | ||||
|        crdt: opts[:crdt] | ||||
|      }} | ||||
|   end | ||||
|  | ||||
|   @opt_values %{ | ||||
|     type: [Map, MerkleMap], | ||||
|     mode: [:standalone, :distributed] | ||||
|   } | ||||
|  | ||||
|   @defopts [ | ||||
|     width: 6, | ||||
|     type: Map, | ||||
|     mode: :standalone, | ||||
|     verbose: false, | ||||
|     seed: 0 | ||||
|   ] | ||||
|  | ||||
|   @spec new(opts :: Keyword.t(), name :: GenServer.name()) :: {:ok, map()} | ||||
|   def new(opts \\ @defopts, name \\ DDRT) when is_list(opts) do | ||||
|     GenServer.call(name, {:new, opts}) | ||||
|   end | ||||
|  | ||||
|   @spec insert(leaves :: leaf() | [leaf()], name :: GenServer.name()) :: | ||||
|           {:ok, map()} | {:badtree, map()} | ||||
|   def insert(_a, name \\ DDRT) | ||||
|  | ||||
|   @doc """ | ||||
|     Insert `leaves` into the r-tree with process with name `name` | ||||
|  | ||||
|     Returns `{:ok,map()}` | ||||
|  | ||||
|   ## Parameters | ||||
|  | ||||
|     - `leaves`: the data to insert. | ||||
|     - `name`: the r-tree name where you want to insert. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|     Individual insertion: | ||||
|  | ||||
|     ``` | ||||
|     iex> DynamicRtree.insert({"Griffin", [{4,5},{6,7}]}, :my_rtree) | ||||
|     iex> DynamicRtree.insert({"Parker", [{14,15},{16,17}]}, :my_rtree) | ||||
|  | ||||
|     {:ok, | ||||
|     %{ | ||||
|      43143342109176739 => {["Parker", "Griffin"], nil, [{4, 15}, {6, 17}]}, | ||||
|      :root => 43143342109176739, | ||||
|      :ticket => [19125803434255161 | 82545666616502197], | ||||
|      "Griffin" => {:leaf, 43143342109176739, [{4, 5}, {6, 7}]}, | ||||
|      "Parker" => {:leaf, 43143342109176739, [{14, 15}, {16, 17}]} | ||||
|     }} | ||||
|     ``` | ||||
|  | ||||
|     Bulk Insertion: | ||||
|  | ||||
|     ``` | ||||
|     iex> DynamicRtree.insert([{"Griffin", [{4,5},{6,7}]}, {"Parker", [{14,15},{16,17}]}], :my_rtree) | ||||
|  | ||||
|     {:ok, | ||||
|     %{ | ||||
|      43143342109176739 => {["Parker", "Griffin"], nil, [{4, 15}, {6, 17}]}, | ||||
|      :root => 43143342109176739, | ||||
|      :ticket => [19125803434255161 | 82545666616502197], | ||||
|      "Griffin" => {:leaf, 43143342109176739, [{4, 5}, {6, 7}]}, | ||||
|      "Parker" => {:leaf, 43143342109176739, [{14, 15}, {16, 17}]} | ||||
|     }} | ||||
|     ``` | ||||
|   """ | ||||
|  | ||||
|   def insert(leaves, name) when is_list(leaves) do | ||||
|     GenServer.call(name, {:bulk_insert, leaves}, :infinity) | ||||
|   end | ||||
|  | ||||
|   def insert(leaf, name) do | ||||
|     GenServer.call(name, {:insert, leaf}, :infinity) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|     Query to get every leaf id overlapped by `box`. | ||||
|  | ||||
|     Returns `[id's]`. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> DynamicRtree.query([{0,7},{4,8}],:my_rtree) | ||||
|       {:ok, ["Griffin"]} | ||||
|  | ||||
|   """ | ||||
|  | ||||
|   @spec query(box :: bounding_box(), name :: GenServer.name()) :: | ||||
|           {:ok, [id()]} | {:badtree, map()} | ||||
|   def query(box, name \\ DDRT) do | ||||
|     GenServer.call(name, {:query, box}) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|     Query to get every node id overlapped by `box` at the defined `depth`. | ||||
|  | ||||
|     Returns `[id's]`. | ||||
|   """ | ||||
|  | ||||
|   @spec pquery(box :: bounding_box(), depth :: integer(), name :: GenServer.name()) :: | ||||
|           {:ok, [id()]} | {:badtree, map()} | ||||
|   def pquery(box, depth, name \\ DDRT) do | ||||
|     GenServer.call(name, {:query_depth, {box, depth}}) | ||||
|   end | ||||
|  | ||||
|   @spec delete(ids :: id() | [id()], name :: GenServer.name()) :: | ||||
|           {:ok, map()} | {:badtree, map()} | ||||
|   def delete(_a, name \\ DDRT) | ||||
|  | ||||
|   @doc """ | ||||
|   Delete the leaves with the given `ids`. | ||||
|  | ||||
|   Returns `{:ok,map()}` | ||||
|  | ||||
|   ## Parameters | ||||
|  | ||||
|     - `ids`: Id or list of Id that you want to delete. | ||||
|     - `name`: the name of the rtree process. | ||||
|  | ||||
|   ## Examples | ||||
|     Individual deletion: | ||||
|  | ||||
|     ``` | ||||
|     iex> DynamicRtree.delete("Griffin",:my_rtree) | ||||
|     iex> DynamicRtree.delete("Parker",:my_rtree) | ||||
|     ``` | ||||
|  | ||||
|     Bulk Deletion: | ||||
|  | ||||
|     ``` | ||||
|     iex> DynamicRtree.delete(["Griffin","Parker"],:my_rtree) | ||||
|     ``` | ||||
|   """ | ||||
|  | ||||
|   def delete(ids, name) when is_list(ids) do | ||||
|     GenServer.call(name, {:bulk_delete, ids}, :infinity) | ||||
|   end | ||||
|  | ||||
|   def delete(id, name) do | ||||
|     GenServer.call(name, {:delete, id}) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Update a bunch of r-tree leaves to the new bounding boxes defined. | ||||
|  | ||||
|   Returns `{:ok,map()}` | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|   ``` | ||||
|   iex> DynamicRtree.bulk_update([{"Griffin",[{0,1},{0,1}]},{"Parker",[{10,11},{10,11}]}],:my_rtree) | ||||
|  | ||||
|   {:ok, | ||||
|   %{ | ||||
|    43143342109176739 => {["Parker", "Griffin"], nil, [{0, 11}, {0, 11}]}, | ||||
|    :root => 43143342109176739, | ||||
|    :ticket => [19125803434255161 | 82545666616502197], | ||||
|    "Griffin" => {:leaf, 43143342109176739, [{0, 1}, {0, 1}]}, | ||||
|    "Parker" => {:leaf, 43143342109176739, [{10, 11}, {10, 11}]} | ||||
|   }} | ||||
|   ``` | ||||
|   """ | ||||
|   @spec bulk_update(leaves :: [leaf()], name :: GenServer.name()) :: | ||||
|           {:ok, map()} | {:badtree, map()} | ||||
|   def bulk_update(updates, name \\ DDRT) when is_list(updates) do | ||||
|     GenServer.call(name, {:bulk_update, updates}, :infinity) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Update a single leaf bounding box | ||||
|  | ||||
|   Returns `{:ok,map()}` | ||||
|  | ||||
|   ## Examples | ||||
|   ``` | ||||
|   iex> DynamicRtree.update({"Griffin",[{0,1},{0,1}]},:my_rtree) | ||||
|  | ||||
|   {:ok, | ||||
|   %{ | ||||
|    43143342109176739 => {["Parker", "Griffin"], nil, [{0, 11}, {0, 11}]}, | ||||
|    :root => 43143342109176739, | ||||
|    :ticket => [19125803434255161 | 82545666616502197], | ||||
|    "Griffin" => {:leaf, 43143342109176739, [{0, 1}, {0, 1}]}, | ||||
|    "Parker" => {:leaf, 43143342109176739, [{10, 11}, {16, 17}]} | ||||
|   }} | ||||
|   ``` | ||||
|   """ | ||||
|  | ||||
|   @spec update( | ||||
|           ids :: id(), | ||||
|           box :: bounding_box() | {bounding_box(), bounding_box()}, | ||||
|           name :: GenServer.name() | ||||
|         ) :: {:ok, map()} | {:badtree, map()} | ||||
|   def update(id, update, name \\ DDRT) do | ||||
|     GenServer.call(name, {:update, {id, update}}) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Get the r-tree metadata | ||||
|  | ||||
|   Returns `map()` | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> DynamicRtree.metadata(:my_rtree) | ||||
|  | ||||
|       %{ | ||||
|         params: %{mode: :standalone, seed: 0, type: Map, verbose: false, width: 6}, | ||||
|         seeding: %{ | ||||
|           bits: 58, | ||||
|           jump: #Function<3.53802439/1 in :rand.mk_alg/1>, | ||||
|           next: #Function<0.53802439/1 in :rand.mk_alg/1>, | ||||
|           type: :exrop, | ||||
|           uniform: #Function<1.53802439/1 in :rand.mk_alg/1>, | ||||
|           uniform_n: #Function<2.53802439/2 in :rand.mk_alg/1>, | ||||
|           weak_low_bits: 1 | ||||
|         } | ||||
|       } | ||||
|  | ||||
|  | ||||
|   """ | ||||
|   @spec metadata(name :: GenServer.name()) :: map() | ||||
|   def metadata(name \\ DDRT) | ||||
|  | ||||
|   def metadata(name) do | ||||
|     GenServer.call(name, :metadata) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Get the r-tree representation | ||||
|  | ||||
|   Returns `map()` | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|   ``` | ||||
|   iex> DynamicRtree.metadata(:my_rtree) | ||||
|  | ||||
|   %{ | ||||
|     43143342109176739 => {["Parker", "Griffin"], nil, [{0, 11}, {0, 11}]}, | ||||
|     :root => 43143342109176739, | ||||
|     :ticket => [19125803434255161 | 82545666616502197], | ||||
|     "Griffin" => {:leaf, 43143342109176739, [{0, 1}, {0, 1}]}, | ||||
|     "Parker" => {:leaf, 43143342109176739, [{10, 11}, {10, 11}]} | ||||
|   } | ||||
|   ``` | ||||
|  | ||||
|   """ | ||||
|   @spec tree(name :: GenServer.name()) :: map() | ||||
|   def tree(name \\ DDRT) | ||||
|  | ||||
|   def tree(name) do | ||||
|     GenServer.call(name, :tree) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Set the members of the `DDRT` cluster. | ||||
|  | ||||
|   `members` should be in the format `{GenServer.name(), node()}`. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|   ``` | ||||
|   DDRT.set_members(DDRT, [{DDRT.A, :yournode@foreignhost}, {DDRT.B, :yournode@foreignhost}]) | ||||
|   ``` | ||||
|  | ||||
|   """ | ||||
|   @spec set_members(name :: GenServer.name(), [member()]) :: :ok | ||||
|   def set_members(name, members) do | ||||
|     :ok = GenServer.call(name, {:set_members, members}) | ||||
|     :ok | ||||
|   end | ||||
|  | ||||
|   def merge_diffs(_a, name \\ DDRT) | ||||
|   @doc false | ||||
|   def merge_diffs(diffs, name) do | ||||
|     send(name, {:merge_diff, diffs}) | ||||
|   end | ||||
|  | ||||
|   ## PRIVATE METHODS | ||||
|  | ||||
|   defp fully_qualified_name({_name, _node} = fq_pair), do: fq_pair | ||||
|  | ||||
|   defp fully_qualified_name(name) do | ||||
|     {name, Node.self()} | ||||
|   end | ||||
|  | ||||
|   defp is_distributed?(state) do | ||||
|     state.metadata[:params][:mode] == :distributed | ||||
|   end | ||||
|  | ||||
|   defp constraints() do | ||||
|     %{ | ||||
|       width: fn v -> v > 0 end, | ||||
|       type: fn v -> v in (@opt_values |> Map.get(:type)) end, | ||||
|       mode: fn v -> v in (@opt_values |> Map.get(:mode)) end, | ||||
|       verbose: fn v -> is_boolean(v) end, | ||||
|       seed: fn v -> is_integer(v) end | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   defp filter_conf(opts) do | ||||
|     # set default :mode to :standalone | ||||
|     opts = Keyword.put_new(opts, :mode, :standalone) | ||||
|  | ||||
|     new_opts = | ||||
|       case opts[:mode] do | ||||
|         :distributed -> Keyword.put(opts, :type, MerkleMap) | ||||
|         _ -> opts | ||||
|       end | ||||
|  | ||||
|     good_keys = | ||||
|       new_opts | ||||
|       |> Keyword.keys() | ||||
|       |> Enum.filter(fn k -> | ||||
|         constraints() |> Map.has_key?(k) and constraints()[k].(new_opts[k]) | ||||
|       end) | ||||
|  | ||||
|     good_keys | ||||
|     |> Enum.reduce(@defopts, fn k, acc -> | ||||
|       acc |> Keyword.put(k, new_opts[k]) | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   defp get_rbundle(state) do | ||||
|     meta = state.metadata | ||||
|     params = meta.params | ||||
|  | ||||
|     %{ | ||||
|       tree: state.tree, | ||||
|       width: params[:width], | ||||
|       verbose: params[:verbose], | ||||
|       type: params[:type], | ||||
|       seeding: meta[:seeding] | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:set_members, members}, _from, state) do | ||||
|     self_crdt = | ||||
|       Module.concat([state.name, Crdt]) | ||||
|       |> fully_qualified_name() | ||||
|  | ||||
|     member_crdts = | ||||
|       members | ||||
|       |> Enum.map(&fully_qualified_name(&1)) | ||||
|       |> Enum.map(fn {pname, node} -> | ||||
|         {Module.concat([pname, Crdt]), node} | ||||
|       end) | ||||
|  | ||||
|     result = DeltaCrdt.set_neighbours(self_crdt, member_crdts) | ||||
|     {:reply, result, state} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:new, config}, _from, state) do | ||||
|     conf = config |> filter_conf | ||||
|     {t, meta} = tree_new(conf) | ||||
|     {:reply, {:ok, t}, %__MODULE__{state | metadata: meta, tree: t}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:insert, leaf}, _from, state) do | ||||
|     r = | ||||
|       {_atom, t} = | ||||
|       case state.tree do | ||||
|         nil -> {:badtree, state.tree} | ||||
|         _ -> {:ok, get_rbundle(state) |> tree_insert(leaf)} | ||||
|       end | ||||
|  | ||||
|     if is_distributed?(state) do | ||||
|       diffs = tree_diffs(state.tree, t) | ||||
|       sync_crdt(diffs, state.crdt) | ||||
|     end | ||||
|  | ||||
|     {:reply, r, %__MODULE__{state | tree: t}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:bulk_insert, leaves}, _from, state) do | ||||
|     r = | ||||
|       {_atom, t} = | ||||
|       case state.tree do | ||||
|         nil -> | ||||
|           {:badtree, state.tree} | ||||
|  | ||||
|         _ -> | ||||
|           final_rbundle = | ||||
|             leaves | ||||
|             |> Enum.reduce(get_rbundle(state), fn l, acc -> | ||||
|               %{acc | tree: acc |> tree_insert(l)} | ||||
|             end) | ||||
|  | ||||
|           {:ok, final_rbundle.tree} | ||||
|       end | ||||
|  | ||||
|     if is_distributed?(state) do | ||||
|       diffs = tree_diffs(state.tree, t) | ||||
|       sync_crdt(diffs, state.crdt) | ||||
|     end | ||||
|  | ||||
|     {:reply, r, %__MODULE__{state | tree: t}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:query, box}, _from, state) do | ||||
|     r = | ||||
|       {_atom, _t} = | ||||
|       case state.tree do | ||||
|         nil -> {:badtree, state.tree} | ||||
|         _ -> {:ok, get_rbundle(state) |> tree_query(box)} | ||||
|       end | ||||
|  | ||||
|     {:reply, r, state} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:query_depth, {box, depth}}, _from, state) do | ||||
|     r = | ||||
|       {_atom, _t} = | ||||
|       case state.tree do | ||||
|         nil -> {:badtree, state.tree} | ||||
|         _ -> {:ok, get_rbundle(state) |> tree_query(box, depth)} | ||||
|       end | ||||
|  | ||||
|     {:reply, r, state} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:delete, id}, _from, state) do | ||||
|     r = | ||||
|       {_atom, t} = | ||||
|       case state.tree do | ||||
|         nil -> {:badtree, state.tree} | ||||
|         _ -> {:ok, get_rbundle(state) |> tree_delete(id)} | ||||
|       end | ||||
|  | ||||
|     if is_distributed?(state) do | ||||
|       diffs = tree_diffs(state.tree, t) | ||||
|       sync_crdt(diffs, state.crdt) | ||||
|     end | ||||
|  | ||||
|     {:reply, r, %__MODULE__{state | tree: t}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:bulk_delete, ids}, _from, state) do | ||||
|     r = | ||||
|       {_atom, t} = | ||||
|       case state.tree do | ||||
|         nil -> | ||||
|           {:badtree, state.tree} | ||||
|  | ||||
|         _ -> | ||||
|           final_rbundle = | ||||
|             ids | ||||
|             |> Enum.reduce(get_rbundle(state), fn id, acc -> | ||||
|               %{acc | tree: acc |> tree_delete(id)} | ||||
|             end) | ||||
|  | ||||
|           {:ok, final_rbundle.tree} | ||||
|       end | ||||
|  | ||||
|     if is_distributed?(state) do | ||||
|       diffs = tree_diffs(state.tree, t) | ||||
|       sync_crdt(diffs, state.crdt) | ||||
|     end | ||||
|  | ||||
|     {:reply, r, %__MODULE__{state | tree: t}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:update, {id, update}}, _from, state) do | ||||
|     r = | ||||
|       {_atom, t} = | ||||
|       case state.tree do | ||||
|         nil -> {:badtree, state.tree} | ||||
|         _ -> {:ok, get_rbundle(state) |> tree_update_leaf(id, update)} | ||||
|       end | ||||
|  | ||||
|     if is_distributed?(state) do | ||||
|       diffs = tree_diffs(state.tree, t) | ||||
|       sync_crdt(diffs, state.crdt) | ||||
|     end | ||||
|  | ||||
|     {:reply, r, %__MODULE__{state | tree: t}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call({:bulk_update, updates}, _from, state) do | ||||
|     r = | ||||
|       {_atom, t} = | ||||
|       case state.tree do | ||||
|         nil -> | ||||
|           {:badtree, state.tree} | ||||
|  | ||||
|         _ -> | ||||
|           final_rbundle = | ||||
|             updates | ||||
|             |> Enum.reduce(get_rbundle(state), fn {id, update} = _u, acc -> | ||||
|               %{acc | tree: acc |> tree_update_leaf(id, update)} | ||||
|             end) | ||||
|  | ||||
|           {:ok, final_rbundle.tree} | ||||
|       end | ||||
|  | ||||
|     if is_distributed?(state) do | ||||
|       diffs = tree_diffs(state.tree, t) | ||||
|       sync_crdt(diffs, state.crdt) | ||||
|     end | ||||
|  | ||||
|     {:reply, r, %__MODULE__{state | tree: t}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call(:metadata, _from, state) do | ||||
|     {:reply, state.metadata, state} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call(:tree, _from, state) do | ||||
|     {:reply, state.tree, state} | ||||
|   end | ||||
|  | ||||
|   # Distributed things | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info({:merge_diff, diff}, state) do | ||||
|     new_tree = | ||||
|       diff | ||||
|       |> Enum.reduce(state.tree, fn x, acc -> | ||||
|         case x do | ||||
|           {:add, k, v} -> acc |> MerkleMap.put(k, v) | ||||
|           {:remove, k} -> acc |> MerkleMap.delete(k) | ||||
|         end | ||||
|       end) | ||||
|  | ||||
|     {:noreply, %__MODULE__{state | tree: new_tree}} | ||||
|   end | ||||
|  | ||||
|   def handle_info({:nodeup, _node, _opts}, state) do | ||||
|     DeltaCrdt.set_neighbours(state.crdt, Enum.map(Node.list(), fn x -> {state.crdt, x} end)) | ||||
|     {:noreply, %__MODULE__{state | listeners: Node.list()}} | ||||
|   end | ||||
|  | ||||
|   def handle_info({:nodedown, _node, _opts}, state) do | ||||
|     DeltaCrdt.set_neighbours(state.crdt, Enum.map(Node.list(), fn x -> {state.crdt, x} end)) | ||||
|     {:noreply, %__MODULE__{state | listeners: Node.list()}} | ||||
|   end | ||||
|  | ||||
|   @doc false | ||||
|   def sync_crdt(diffs, crdt) when length(diffs) > 0 do | ||||
|     diffs | ||||
|     |> Enum.each(fn {k, v} -> | ||||
|       if v do | ||||
|         DeltaCrdt.put(crdt, k, v) | ||||
|       else | ||||
|         DeltaCrdt.delete(crdt, k) | ||||
|       end | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   @doc false | ||||
|   def sync_crdt(_diffs, _crdt) do | ||||
|   end | ||||
|  | ||||
|   @doc false | ||||
|   def reconstruct_from_crdt(map, t) do | ||||
|     map | ||||
|     |> Enum.reduce(t, fn {x, y}, acc -> | ||||
|       acc |> MerkleMap.put(x, y) | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   @doc false | ||||
|   def tree_diffs(old_tree, new_tree) when not is_nil(old_tree) and not is_nil(new_tree) do | ||||
|     case MerkleMap.diff_keys( | ||||
|            old_tree |> MerkleMap.update_hashes(), | ||||
|            new_tree |> MerkleMap.update_hashes() | ||||
|          ) do | ||||
|       {:ok, keys} -> keys |> Enum.map(fn x -> {x, new_tree |> MerkleMap.get(x)} end) | ||||
|       _ -> [] | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def tree_diffs(_old_tree, _new_tree), do: [] | ||||
| end | ||||
| @@ -1,687 +0,0 @@ | ||||
| defmodule DDRT.DynamicRtreeImpl do | ||||
|   alias DDRT.DynamicRtreeImpl.{Node, Utils} | ||||
|  | ||||
|   require Logger | ||||
|   import IO.ANSI | ||||
|  | ||||
|   # Between 1 y 64800. Bigger value => ^ updates speed, ~v query speed. | ||||
|   @max_area 20000 | ||||
|  | ||||
|   defmacro __using__(_) do | ||||
|     quote do | ||||
|       alias DDRT.DynamicRtreeImpl | ||||
|  | ||||
|       @doc false | ||||
|       defdelegate tree_new(opts), to: DynamicRtreeImpl | ||||
|  | ||||
|       @doc false | ||||
|       defdelegate tree_insert(tree, leaf), to: DynamicRtreeImpl | ||||
|  | ||||
|       @doc false | ||||
|       defdelegate tree_query(tree, box), to: DynamicRtreeImpl | ||||
|  | ||||
|       @doc false | ||||
|       defdelegate tree_query(tree, box, depth), to: DynamicRtreeImpl | ||||
|  | ||||
|       @doc false | ||||
|       defdelegate tree_delete(tree, id), to: DynamicRtreeImpl | ||||
|  | ||||
|       @doc false | ||||
|       defdelegate tree_update_leaf(tree, id, update), to: DynamicRtreeImpl | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # PUBLIC METHODS | ||||
|  | ||||
|   def tree_new(opts) do | ||||
|     {f, s} = :rand.seed(:exrop, opts[:seed]) | ||||
|     {node, new_ticket} = Node.new(f, s) | ||||
|  | ||||
|     tree_init = | ||||
|       case opts[:type] do | ||||
|         Map -> %{} | ||||
|         MerkleMap -> %MerkleMap{} | ||||
|       end | ||||
|  | ||||
|     tree = | ||||
|       tree_init | ||||
|       |> opts[:type].put(:ticket, new_ticket) | ||||
|       |> opts[:type].put(:root, node) | ||||
|       |> opts[:type].put(node, {[], nil, [{0, 0}, {0, 0}]}) | ||||
|  | ||||
|     {tree, %{params: opts, seeding: f}} | ||||
|   end | ||||
|  | ||||
|   def tree_insert(rbundle, {id, _box} = leaf) do | ||||
|     if rbundle.tree |> rbundle[:type].get(id) do | ||||
|       if rbundle.verbose, | ||||
|         do: | ||||
|           Logger.debug( | ||||
|             cyan() <> | ||||
|               "[" <> | ||||
|               green() <> | ||||
|               "Insertion" <> | ||||
|               cyan() <> | ||||
|               "] failed:" <> | ||||
|               yellow() <> | ||||
|               " [#{id}] " <> | ||||
|               cyan() <> | ||||
|               "already exists at tree." <> | ||||
|               yellow() <> " [Tip]" <> cyan() <> " use " <> yellow() <> "update_leaf/3" | ||||
|           ) | ||||
|  | ||||
|       rbundle.tree | ||||
|     else | ||||
|       path = best_subtree(rbundle, leaf) | ||||
|       t1 = :os.system_time(:microsecond) | ||||
|  | ||||
|       r = | ||||
|         insertion(rbundle, path, leaf) | ||||
|         |> recursive_update(tl(path), leaf, :insertion) | ||||
|  | ||||
|       t2 = :os.system_time(:microsecond) | ||||
|  | ||||
|       if rbundle.verbose, | ||||
|         do: | ||||
|           Logger.debug( | ||||
|             cyan() <> | ||||
|               "[" <> | ||||
|               green() <> | ||||
|               "Insertion" <> | ||||
|               cyan() <> | ||||
|               "] success: " <> | ||||
|               yellow() <> | ||||
|               "[#{id}]" <> cyan() <> " was inserted at" <> yellow() <> " ['#{hd(path)}']" | ||||
|           ) | ||||
|  | ||||
|       if rbundle.verbose, | ||||
|         do: | ||||
|           Logger.info( | ||||
|             cyan() <> | ||||
|               "[" <> green() <> "Insertion" <> cyan() <> "] took" <> yellow() <> " #{t2 - t1} µs" | ||||
|           ) | ||||
|  | ||||
|       r | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def tree_query(rbundle, box) do | ||||
|     t1 = :os.system_time(:microsecond) | ||||
|     r = find_match_leaves(rbundle, box, [get_root(rbundle)], [], []) | ||||
|     t2 = :os.system_time(:microsecond) | ||||
|  | ||||
|     if rbundle.verbose, | ||||
|       do: | ||||
|         Logger.info( | ||||
|           cyan() <> | ||||
|             "[" <> | ||||
|             color(201) <> | ||||
|             "Query" <> | ||||
|             cyan() <> | ||||
|             "] box " <> | ||||
|             yellow() <> | ||||
|             "#{box |> Kernel.inspect()} " <> cyan() <> "took " <> yellow() <> "#{t2 - t1} µs" | ||||
|         ) | ||||
|  | ||||
|     r | ||||
|   end | ||||
|  | ||||
|   def tree_query(rbundle, box, depth) do | ||||
|     find_match_depth(rbundle, box, [{get_root(rbundle), 0}], [], depth) | ||||
|   end | ||||
|  | ||||
|   def tree_delete(rbundle, id) do | ||||
|     t1 = :os.system_time(:microsecond) | ||||
|  | ||||
|     r = | ||||
|       if rbundle.tree |> rbundle[:type].get(id) do | ||||
|         remove(rbundle, id) | ||||
|       else | ||||
|         rbundle.tree | ||||
|       end | ||||
|  | ||||
|     t2 = :os.system_time(:microsecond) | ||||
|  | ||||
|     if rbundle.verbose, | ||||
|       do: | ||||
|         Logger.info( | ||||
|           cyan() <> | ||||
|             "[" <> | ||||
|             color(124) <> | ||||
|             "Delete" <> | ||||
|             cyan() <> | ||||
|             "] leaf " <> | ||||
|             yellow() <> "[#{id}]" <> cyan() <> " took " <> yellow() <> "#{t2 - t1} µs" | ||||
|         ) | ||||
|  | ||||
|     r | ||||
|   end | ||||
|  | ||||
|   def tree_update_leaf(rbundle, id, {old_box, new_box} = boxes) do | ||||
|     if rbundle.tree |> rbundle[:type].get(id) do | ||||
|       t1 = :os.system_time(:microsecond) | ||||
|       r = update(rbundle, id, boxes) | ||||
|       t2 = :os.system_time(:microsecond) | ||||
|  | ||||
|       if rbundle.verbose, | ||||
|         do: | ||||
|           Logger.info( | ||||
|             cyan() <> | ||||
|               "[" <> | ||||
|               color(195) <> | ||||
|               "Update" <> | ||||
|               cyan() <> | ||||
|               "] " <> | ||||
|               yellow() <> | ||||
|               "[#{id}]" <> | ||||
|               cyan() <> | ||||
|               " from " <> | ||||
|               yellow() <> | ||||
|               "#{old_box |> Kernel.inspect()}" <> | ||||
|               cyan() <> | ||||
|               " to " <> | ||||
|               yellow() <> | ||||
|               "#{new_box |> Kernel.inspect()}" <> | ||||
|               cyan() <> " took " <> yellow() <> "#{t2 - t1} µs" | ||||
|           ) | ||||
|  | ||||
|       r | ||||
|     else | ||||
|       if rbundle.verbose, | ||||
|         do: | ||||
|           Logger.warning( | ||||
|             cyan() <> | ||||
|               "[" <> | ||||
|               color(195) <> | ||||
|               "Update" <> cyan() <> "] " <> yellow() <> "[#{id}] doesn't exists" <> cyan() | ||||
|           ) | ||||
|  | ||||
|       rbundle.tree | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # You dont need to know old_box but is a BIT slower | ||||
|   def tree_update_leaf(rbundle, id, new_box) do | ||||
|     tree_update_leaf( | ||||
|       rbundle, | ||||
|       id, | ||||
|       {rbundle.tree |> rbundle[:type].get(id) |> Utils.tuple_value(:bbox), new_box} | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   ### PRIVATE METHODS | ||||
|  | ||||
|   # Helpers | ||||
|   defp get_root(rbundle) do | ||||
|     rbundle.tree |> rbundle[:type].get(:root) | ||||
|   end | ||||
|  | ||||
|   defp is_root?(rbundle, node) do | ||||
|     get_root(rbundle) == node | ||||
|   end | ||||
|  | ||||
|   ## Internal actions | ||||
|   ## Insert | ||||
|  | ||||
|   # triple - S (Structure Swifty Shift) | ||||
|   defp triple_s(rbundle, old_node, new_node, {id, box}) do | ||||
|     tuple_entry = | ||||
|       {old_node_childs_update, _daddy, _bbox} = | ||||
|       rbundle.tree |> rbundle[:type].get(old_node) |> (fn {n, d, b} -> {n -- [id], d, b} end).() | ||||
|  | ||||
|     tree_update = | ||||
|       rbundle.tree | ||||
|       |> rbundle[:type].update!(new_node, fn {ch, d, b} -> {[id] ++ ch, d, b} end) | ||||
|       |> rbundle[:type].update!(id, fn {ch, _d, b} -> {ch, new_node, b} end) | ||||
|  | ||||
|     if length(old_node_childs_update) > 0 do | ||||
|       %{rbundle | tree: tree_update |> rbundle[:type].put(old_node, tuple_entry)} | ||||
|       |> recursive_update(old_node, box, :deletion) | ||||
|     else | ||||
|       %{rbundle | tree: tree_update} |> remove(old_node) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp insertion(rbundle, branch, {_id, _box} = leaf) do | ||||
|     tree_update = add_entry(rbundle, hd(branch), leaf) | ||||
|  | ||||
|     childs = tree_update |> rbundle[:type].get(hd(branch)) |> Utils.tuple_value(:childs) | ||||
|  | ||||
|     final_tree = | ||||
|       if length(childs) > rbundle.width do | ||||
|         handle_overflow(%{rbundle | tree: tree_update}, branch) | ||||
|       else | ||||
|         tree_update | ||||
|       end | ||||
|  | ||||
|     %{rbundle | tree: final_tree} | ||||
|   end | ||||
|  | ||||
|   defp add_entry(rbundle, node, {id, box} = _leaf) do | ||||
|     rbundle.tree | ||||
|     |> rbundle[:type].update!(node, fn {ch, daddy, b} -> | ||||
|       {[id] ++ ch, daddy, Utils.combine_multiple([box, b])} | ||||
|     end) | ||||
|     |> rbundle[:type].put(id, {:leaf, node, box}) | ||||
|   end | ||||
|  | ||||
|   defp handle_overflow(rbundle, branch) do | ||||
|     n = hd(branch) | ||||
|     {node_n, new_node} = split(rbundle, n) | ||||
|     treeck = rbundle.tree |> rbundle[:type].put(:ticket, new_node.next_ticket) | ||||
|  | ||||
|     if is_root?(rbundle, n) do | ||||
|       {new_root, ticket} = Node.new(rbundle.seeding, treeck |> rbundle[:type].get(:ticket)) | ||||
|       treeck = treeck |> rbundle[:type].put(:ticket, ticket) | ||||
|       root_bbox = Utils.combine_multiple([node_n.bbox, new_node.bbox]) | ||||
|  | ||||
|       treeck = | ||||
|         treeck | ||||
|         |> rbundle[:type].put(new_node.id, {new_node.childs, new_root, new_node.bbox}) | ||||
|         |> rbundle[:type].replace!(node_n.id, {node_n.childs, new_root, node_n.bbox}) | ||||
|         |> rbundle[:type].replace!(:root, new_root) | ||||
|         |> rbundle[:type].put(new_root, {[node_n.id, new_node.id], nil, root_bbox}) | ||||
|  | ||||
|       new_node.childs | ||||
|       |> Enum.reduce(treeck, fn c, acc -> | ||||
|         acc |> rbundle[:type].update!(c, fn {ch, _d, b} -> {ch, new_node.id, b} end) | ||||
|       end) | ||||
|     else | ||||
|       parent = hd(tl(branch)) | ||||
|  | ||||
|       treeck = | ||||
|         treeck | ||||
|         |> rbundle[:type].put(new_node.id, {new_node.childs, parent, new_node.bbox}) | ||||
|         |> rbundle[:type].replace!(node_n.id, {node_n.childs, parent, node_n.bbox}) | ||||
|         |> rbundle[:type].update!(parent, fn {ch, d, b} -> | ||||
|           {[new_node.id] ++ ch, d, Utils.combine_multiple([b, new_node.bbox])} | ||||
|         end) | ||||
|  | ||||
|       updated_tree = | ||||
|         new_node.childs | ||||
|         |> Enum.reduce(treeck, fn c, acc -> | ||||
|           acc |> rbundle[:type].update!(c, fn {ch, _d, b} -> {ch, new_node.id, b} end) | ||||
|         end) | ||||
|  | ||||
|       if length(updated_tree |> rbundle[:type].get(parent) |> elem(0)) > rbundle.width, | ||||
|         do: handle_overflow(%{rbundle | tree: updated_tree}, tl(branch)), | ||||
|         else: updated_tree | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp split(rbundle, node) do | ||||
|     sorted_nodes = | ||||
|       rbundle.tree | ||||
|       |> rbundle[:type].get(node) | ||||
|       |> Utils.tuple_value(:childs) | ||||
|       |> Enum.map(fn n -> | ||||
|         box = rbundle.tree |> rbundle[:type].get(n) |> Utils.tuple_value(:bbox) | ||||
|         {box |> Utils.middle_value(), n, box} | ||||
|       end) | ||||
|       |> Enum.sort() | ||||
|       |> Enum.map(fn {_x, y, z} -> {y, z} end) | ||||
|  | ||||
|     {n_id, n_bbox} = | ||||
|       sorted_nodes |> Enum.slice(0..((rbundle.width / 2 - 1) |> Kernel.trunc())) |> Enum.unzip() | ||||
|  | ||||
|     {dn_id, dn_bbox} = | ||||
|       sorted_nodes | ||||
|       |> Enum.slice(((rbundle.width / 2) |> Kernel.trunc())..(length(sorted_nodes) - 1)) | ||||
|       |> Enum.unzip() | ||||
|  | ||||
|     {new_node, next_ticket} = | ||||
|       Node.new(rbundle.seeding, rbundle.tree |> rbundle[:type].get(:ticket)) | ||||
|  | ||||
|     n_bounds = n_bbox |> Utils.combine_multiple() | ||||
|     dn_bounds = dn_bbox |> Utils.combine_multiple() | ||||
|  | ||||
|     {%{id: node, childs: n_id, bbox: n_bounds}, | ||||
|      %{id: new_node, childs: dn_id, bbox: dn_bounds, next_ticket: next_ticket}} | ||||
|   end | ||||
|  | ||||
|   defp best_subtree(rbundle, leaf) do | ||||
|     find_best_subtree(rbundle, get_root(rbundle), leaf, []) | ||||
|   end | ||||
|  | ||||
|   defp find_best_subtree(rbundle, root, {_id, box} = leaf, track) do | ||||
|     childs = rbundle.tree |> rbundle[:type].get(root) |> Utils.tuple_value(:childs) | ||||
|  | ||||
|     if is_list(childs) and length(childs) > 0 do | ||||
|       winner = get_best_candidate(rbundle, childs, box) | ||||
|       new_track = [root] ++ track | ||||
|       find_best_subtree(rbundle, winner, leaf, new_track) | ||||
|     else | ||||
|       if is_atom(childs), do: track, else: [root] ++ track | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp get_best_candidate(rbundle, candidates, box) do | ||||
|     win_entry = | ||||
|       candidates | ||||
|       |> Enum.reduce_while(%{id: :not_id, cost: :infinity}, fn c, acc -> | ||||
|         cbox = rbundle.tree |> rbundle[:type].get(c) |> Utils.tuple_value(:bbox) | ||||
|  | ||||
|         if Utils.contained?(cbox, box) do | ||||
|           {:halt, %{id: c, cost: 0}} | ||||
|         else | ||||
|           enlargement = Utils.enlargement_area(cbox, box) | ||||
|  | ||||
|           if enlargement < acc |> Map.get(:cost) do | ||||
|             {:cont, %{id: c, cost: enlargement}} | ||||
|           else | ||||
|             {:cont, acc} | ||||
|           end | ||||
|         end | ||||
|       end) | ||||
|  | ||||
|     win_entry[:id] | ||||
|   end | ||||
|  | ||||
|   ## Query | ||||
|  | ||||
|   defp find_match_leaves(rbundle, box, dig, leaves, flood) do | ||||
|     f = hd(dig) | ||||
|     tail = if length(dig) > 1, do: tl(dig), else: [] | ||||
|     {content, _dad, fbox} = rbundle.tree |> rbundle[:type].get(f) | ||||
|  | ||||
|     {new_dig, new_leaves, new_flood} = | ||||
|       if Utils.overlap?(fbox, box) do | ||||
|         if is_atom(content) do | ||||
|           {tail, [f] ++ leaves, flood} | ||||
|         else | ||||
|           if Utils.contained?(box, fbox), | ||||
|             do: {tail, leaves, [f] ++ flood}, | ||||
|             else: {content ++ tail, leaves, flood} | ||||
|         end | ||||
|       else | ||||
|         {tail, leaves, flood} | ||||
|       end | ||||
|  | ||||
|     if length(new_dig) > 0 do | ||||
|       find_match_leaves(rbundle, box, new_dig, new_leaves, new_flood) | ||||
|     else | ||||
|       new_leaves ++ explore_flood(rbundle, new_flood) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp explore_flood(rbundle, flood) do | ||||
|     next_floor = | ||||
|       flood | ||||
|       |> Enum.flat_map(fn x -> | ||||
|         case rbundle.tree |> rbundle[:type].get(x) |> Utils.tuple_value(:childs) do | ||||
|           :leaf -> [] | ||||
|           any -> any | ||||
|         end | ||||
|       end) | ||||
|  | ||||
|     if length(next_floor) > 0, do: explore_flood(rbundle, next_floor), else: flood | ||||
|   end | ||||
|  | ||||
|   defp find_match_depth(rbundle, box, dig, leaves, depth) do | ||||
|     {f, cdepth} = hd(dig) | ||||
|     tail = if length(dig) > 1, do: tl(dig), else: [] | ||||
|     {content, _dad, fbox} = rbundle.tree |> rbundle[:type].get(f) | ||||
|  | ||||
|     {new_dig, new_leaves} = | ||||
|       if Utils.overlap?(fbox, box) do | ||||
|         if cdepth < depth and is_list(content) do | ||||
|           childs = content |> Enum.map(fn c -> {c, cdepth + 1} end) | ||||
|           {childs ++ tail, leaves} | ||||
|         else | ||||
|           {tail, [f] ++ leaves} | ||||
|         end | ||||
|       else | ||||
|         {tail, leaves} | ||||
|       end | ||||
|  | ||||
|     if length(new_dig) > 0, | ||||
|       do: find_match_depth(rbundle, box, new_dig, new_leaves, depth), | ||||
|       else: new_leaves | ||||
|   end | ||||
|  | ||||
|   ## Delete | ||||
|  | ||||
|   defp remove(rbundle, id) do | ||||
|     {_ch, parent, removed_bbox} = rbundle.tree |> rbundle[:type].get(id) | ||||
|  | ||||
|     if parent do | ||||
|       tree_updated = | ||||
|         rbundle.tree | ||||
|         |> rbundle[:type].delete(id) | ||||
|         |> rbundle[:type].update!(parent, fn {ch, daddy, b} -> {ch -- [id], daddy, b} end) | ||||
|  | ||||
|       parent_childs = tree_updated |> rbundle[:type].get(parent) |> elem(0) | ||||
|  | ||||
|       if length(parent_childs) > 0 do | ||||
|         %{rbundle | tree: tree_updated} |> recursive_update(parent, removed_bbox, :deletion) | ||||
|       else | ||||
|         remove(%{rbundle | tree: tree_updated}, parent) | ||||
|       end | ||||
|     else | ||||
|       rbundle.tree | ||||
|       |> rbundle[:type].update!(id, fn {ch, daddy, _b} -> {ch, daddy, [{0, 0}, {0, 0}]} end) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   ## Hard update | ||||
|  | ||||
|   defp update(rbundle, id, {old_box, new_box}) do | ||||
|     parent = rbundle.tree |> rbundle[:type].get(id) |> Utils.tuple_value(:dad) | ||||
|     parent_box = rbundle.tree |> rbundle[:type].get(parent) |> Utils.tuple_value(:bbox) | ||||
|  | ||||
|     updated_tree = | ||||
|       rbundle.tree |> rbundle[:type].update!(id, fn {ch, d, _b} -> {ch, d, new_box} end) | ||||
|  | ||||
|     local_rbundle = %{rbundle | tree: updated_tree} | ||||
|  | ||||
|     if Utils.contained?(parent_box, new_box) do | ||||
|       if Utils.in_border?(parent_box, old_box) do | ||||
|         if rbundle.verbose, | ||||
|           do: | ||||
|             Logger.debug( | ||||
|               cyan() <> | ||||
|                 "[" <> | ||||
|                 color(195) <> | ||||
|                 "Update" <> | ||||
|                 cyan() <> | ||||
|                 "] Good case: new box " <> | ||||
|                 yellow() <> | ||||
|                 "(#{new_box |> Kernel.inspect()})" <> | ||||
|                 cyan() <> | ||||
|                 " of " <> | ||||
|                 yellow() <> | ||||
|                 "[#{id}]" <> | ||||
|                 cyan() <> | ||||
|                 " reduce the parent " <> yellow() <> "(['#{parent}'])" <> cyan() <> " box" | ||||
|             ) | ||||
|  | ||||
|         local_rbundle |> recursive_update(parent, old_box, :deletion) | ||||
|       else | ||||
|         if rbundle.verbose, | ||||
|           do: | ||||
|             Logger.debug( | ||||
|               cyan() <> | ||||
|                 "[" <> | ||||
|                 color(195) <> | ||||
|                 "Update" <> | ||||
|                 cyan() <> | ||||
|                 "] Best case: new box " <> | ||||
|                 yellow() <> | ||||
|                 "(#{new_box |> Kernel.inspect()})" <> | ||||
|                 cyan() <> | ||||
|                 " of " <> | ||||
|                 yellow() <> | ||||
|                 "[#{id}]" <> | ||||
|                 cyan() <> " was contained by his parent " <> yellow() <> "(['#{parent}'])" | ||||
|             ) | ||||
|  | ||||
|         local_rbundle.tree | ||||
|       end | ||||
|     else | ||||
|       case local_rbundle | ||||
|            |> node_brothers(parent) | ||||
|            |> (fn b -> good_slot?(local_rbundle, b, new_box) end).() do | ||||
|         {new_parent, _new_brothers, _new_parent_box} -> | ||||
|           if rbundle.verbose, | ||||
|             do: | ||||
|               Logger.debug( | ||||
|                 cyan() <> | ||||
|                   "[" <> | ||||
|                   color(195) <> | ||||
|                   "Update" <> | ||||
|                   cyan() <> | ||||
|                   "] Neutral case: new box " <> | ||||
|                   yellow() <> | ||||
|                   "(#{new_box |> Kernel.inspect()})" <> | ||||
|                   cyan() <> | ||||
|                   " of " <> | ||||
|                   yellow() <> | ||||
|                   "[#{id}]" <> | ||||
|                   cyan() <> | ||||
|                   " increases the parent box but there is an available slot at one uncle " <> | ||||
|                   yellow() <> "(['#{new_parent}'])" | ||||
|               ) | ||||
|  | ||||
|           triple_s(local_rbundle, parent, new_parent, {id, old_box}) | ||||
|  | ||||
|         nil -> | ||||
|           if Utils.area(parent_box) >= @max_area do | ||||
|             if rbundle.verbose, | ||||
|               do: | ||||
|                 Logger.debug( | ||||
|                   cyan() <> | ||||
|                     "[" <> | ||||
|                     color(195) <> | ||||
|                     "Update" <> | ||||
|                     cyan() <> | ||||
|                     "] Worst case: new box " <> | ||||
|                     yellow() <> | ||||
|                     "(#{new_box |> Kernel.inspect()})" <> | ||||
|                     cyan() <> | ||||
|                     " of " <> | ||||
|                     yellow() <> | ||||
|                     "[#{id}]" <> | ||||
|                     cyan() <> | ||||
|                     " increases the parent box which was so big " <> | ||||
|                     yellow() <> | ||||
|                     "#{((Utils.area(parent_box) |> Kernel.trunc()) / @max_area * 100) |> Kernel.trunc()} %. " <> | ||||
|                     cyan() <> | ||||
|                     "So we proceed to delete " <> | ||||
|                     yellow() <> "[#{id}]" <> cyan() <> " and reinsert at tree" | ||||
|                 ) | ||||
|  | ||||
|             local_rbundle |> top_down({id, new_box}) | ||||
|           else | ||||
|             if rbundle.verbose, | ||||
|               do: | ||||
|                 Logger.debug( | ||||
|                   cyan() <> | ||||
|                     "[" <> | ||||
|                     color(195) <> | ||||
|                     "Update" <> | ||||
|                     cyan() <> | ||||
|                     "] Bad case: new box " <> | ||||
|                     yellow() <> | ||||
|                     "(#{new_box |> Kernel.inspect()})" <> | ||||
|                     cyan() <> | ||||
|                     " of " <> | ||||
|                     yellow() <> | ||||
|                     "[#{id}]" <> | ||||
|                     cyan() <> | ||||
|                     " increases the parent box which isn't that big yet " <> | ||||
|                     yellow() <> | ||||
|                     "#{((Utils.area(parent_box) |> Kernel.trunc()) / @max_area * 100) |> Kernel.trunc()} %. " <> | ||||
|                     cyan() <> | ||||
|                     "So we proceed to increase parent " <> | ||||
|                     yellow() <> "(['#{parent}'])" <> cyan() <> " box" | ||||
|                 ) | ||||
|  | ||||
|             local_rbundle |> recursive_update(parent, new_box, :insertion) | ||||
|           end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   ## Common updates | ||||
|  | ||||
|   defp top_down(rbundle, {id, box}) do | ||||
|     %{rbundle | tree: rbundle |> remove(id)} |> tree_insert({id, box}) | ||||
|   end | ||||
|  | ||||
|   # Recursive bbox updates when you have node path from root (at insertion) | ||||
|   defp recursive_update(rbundle, path, {_id, box} = leaf, :insertion) when length(path) > 0 do | ||||
|     {modified, t} = update_node_bbox(rbundle, hd(path), box, :insertion) | ||||
|  | ||||
|     if modified and length(path) > 1, | ||||
|       do: recursive_update(%{rbundle | tree: t}, tl(path), leaf, :insertion), | ||||
|       else: rbundle.tree | ||||
|   end | ||||
|  | ||||
|   # Recursive bbox updates when u dont have node path from root, so you have to query parents map... (at delete) | ||||
|   defp recursive_update(rbundle, node, box, mode) when is_list(node) |> Kernel.not() do | ||||
|     {modified, t} = update_node_bbox(rbundle, node, box, mode) | ||||
|     next = rbundle.tree |> rbundle[:type].get(node) |> Utils.tuple_value(:dad) | ||||
|     if modified and next, do: recursive_update(%{rbundle | tree: t}, next, box, mode), else: t | ||||
|   end | ||||
|  | ||||
|   # Typical dumbass safe method | ||||
|   defp recursive_update(rbundle, _path, _leaf, :insertion) do | ||||
|     rbundle.tree | ||||
|   end | ||||
|  | ||||
|   defp update_node_bbox(rbundle, node, the_box, action) do | ||||
|     node_box = rbundle.tree |> rbundle[:type].get(node) |> Utils.tuple_value(:bbox) | ||||
|  | ||||
|     new_bbox = | ||||
|       case action do | ||||
|         :insertion -> | ||||
|           Utils.combine(node_box, the_box) | ||||
|  | ||||
|         :deletion -> | ||||
|           if Utils.in_border?(node_box, the_box) do | ||||
|             rbundle.tree | ||||
|             |> rbundle[:type].get(node) | ||||
|             |> Utils.tuple_value(:childs) | ||||
|             |> Enum.map(fn c -> | ||||
|               rbundle.tree |> rbundle[:type].get(c) |> Utils.tuple_value(:bbox) | ||||
|             end) | ||||
|             |> Utils.combine_multiple() | ||||
|           else | ||||
|             node_box | ||||
|           end | ||||
|       end | ||||
|  | ||||
|     bbox_mutation(rbundle, node, new_bbox, node_box) | ||||
|   end | ||||
|  | ||||
|   defp bbox_mutation(rbundle, node, new_bbox, node_box) do | ||||
|     if new_bbox == node_box do | ||||
|       {false, rbundle.tree} | ||||
|     else | ||||
|       t = rbundle.tree |> rbundle[:type].update!(node, fn {ch, d, _b} -> {ch, d, new_bbox} end) | ||||
|       {true, t} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # Return the brothers of the node [{brother_id, brother_childs, brother_box},...] | ||||
|   defp node_brothers(rbundle, node) do | ||||
|     parent = rbundle.tree |> rbundle[:type].get(node) |> Utils.tuple_value(:dad) | ||||
|  | ||||
|     rbundle.tree | ||||
|     |> rbundle[:type].get(parent) | ||||
|     |> Utils.tuple_value(:childs) | ||||
|     |> (fn c -> if c, do: c -- [node], else: [] end).() | ||||
|     |> Enum.map(fn b -> | ||||
|       tuple = rbundle.tree |> rbundle[:type].get(b) | ||||
|       {b, tuple |> Utils.tuple_value(:childs), tuple |> Utils.tuple_value(:bbox)} | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   # Find a good slot (at bros/brothers list) for the box, it means that the brother hasnt the max childs and the box is at the limits of his own | ||||
|   defp good_slot?(rbundle, bros, box) do | ||||
|     bros | ||||
|     |> Enum.find(fn {_bid, bchilds, bbox} -> | ||||
|       length(bchilds) < rbundle.width and Utils.contained?(bbox, box) | ||||
|     end) | ||||
|   end | ||||
| end | ||||
| @@ -1,13 +0,0 @@ | ||||
| defmodule DDRT.DynamicRtreeImpl.BoundingBoxGenerator do | ||||
|   @moduledoc false | ||||
|  | ||||
|   def generate(n, size, result) do | ||||
|     s = size / 2 | ||||
|     x = Enum.random(-180..180) | ||||
|     y = Enum.random(-90..90) | ||||
|  | ||||
|     if n > 0, | ||||
|       do: generate(n - 1, size, [[{x - s, x + s}, {y - s, y + s}]] ++ result), | ||||
|       else: result | ||||
|   end | ||||
| end | ||||
| @@ -1,7 +0,0 @@ | ||||
| defmodule DDRT.DynamicRtreeImpl.Node do | ||||
|   @moduledoc false | ||||
|  | ||||
|   def new(gen, seed) do | ||||
|     gen[:next].(seed) | ||||
|   end | ||||
| end | ||||
| @@ -1,118 +0,0 @@ | ||||
| defmodule DDRT.DynamicRtreeImpl.Utils do | ||||
|   @moduledoc false | ||||
|  | ||||
|   def format_bbox([{min_x, max_x} = x, {min_y, max_y} = y]) do | ||||
|     %{ | ||||
|       x: x, | ||||
|       y: y, | ||||
|       xm: min_x, | ||||
|       xM: max_x, | ||||
|       ym: min_y, | ||||
|       yM: max_y | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def tuple_value(raw, _atom) when raw == nil do | ||||
|     nil | ||||
|   end | ||||
|  | ||||
|   def tuple_value(raw, atom) do | ||||
|     case atom do | ||||
|       :childs -> raw |> elem(0) | ||||
|       :dad -> raw |> elem(1) | ||||
|       :bbox -> raw |> elem(2) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # Combine two bounding boxes into one | ||||
|   def combine(box1, box2) do | ||||
|     a = box1 |> format_bbox | ||||
|     b = box2 |> format_bbox | ||||
|     xm = Kernel.min(a.xm, b.xm) | ||||
|     xM = Kernel.max(a.xM, b.xM) | ||||
|     ym = Kernel.min(a.ym, b.ym) | ||||
|     yM = Kernel.max(a.yM, b.yM) | ||||
|     result = [{xm, xM}, {ym, yM}] | ||||
|     result = if area(box1) === 0, do: box2, else: result | ||||
|     if area(box2) === 0, do: box1, else: result | ||||
|   end | ||||
|  | ||||
|   # Combine multiple bbox | ||||
|   def combine_multiple(list) when length(list) > 1 do | ||||
|     real_list = list |> Enum.filter(fn x -> area(x) > 0 end) | ||||
|  | ||||
|     tl(real_list) | ||||
|     |> Enum.reduce(hd(real_list), fn [{a, b}, {c, d}] = _e, [{x, y}, {z, w}] = _acc -> | ||||
|       [{Kernel.min(a, x), Kernel.max(b, y)}, {Kernel.min(c, z), Kernel.max(d, w)}] | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   def combine_multiple(list) do | ||||
|     hd(list) | ||||
|   end | ||||
|  | ||||
|   # Returns de percent of the overlap area (of the box1) between box1 and box2 | ||||
|   def overlap_area(box1, box2) do | ||||
|     a = box1 |> format_bbox | ||||
|     b = box2 |> format_bbox | ||||
|     x_overlap = Kernel.max(0, Kernel.min(a.xM, b.xM) - Kernel.max(a.xm, b.xm)) | ||||
|     y_overlap = Kernel.max(0, Kernel.min(a.yM, b.yM) - Kernel.max(a.ym, b.ym)) | ||||
|     x_overlap * y_overlap / area(box1) * 100 | ||||
|   end | ||||
|  | ||||
|   # Return if those 2 boxes are overlapping | ||||
|   def overlap?(box1, box2) do | ||||
|     if overlap_area(box1, box2) > 0, do: true, else: false | ||||
|   end | ||||
|  | ||||
|   # Return if box 1 contains box 2 | ||||
|   def contained?(box1, box2) do | ||||
|     a = box1 |> format_bbox | ||||
|     b = box2 |> format_bbox | ||||
|  | ||||
|     a.xm <= b.xm and a.xM >= b.xM and a.ym <= b.ym and a.yM >= b.yM | ||||
|   end | ||||
|  | ||||
|   # Enlargement area after adding new box | ||||
|   def enlargement_area(box, new_box) do | ||||
|     a1 = area(box) | ||||
|     a2 = combine_multiple([box, new_box]) |> area | ||||
|     a2 - a1 | ||||
|   end | ||||
|  | ||||
|   # Checks if box is at some border of parent_box | ||||
|   def in_border?(parent_box, box) do | ||||
|     p = parent_box |> format_bbox | ||||
|     b = box |> format_bbox | ||||
|  | ||||
|     p.xm == b.xm or p.xM == b.xM or p.ym == b.ym or p.yM == b.yM | ||||
|   end | ||||
|  | ||||
|   # Return the area of a bounding box | ||||
|   def area([{a, b}, {c, d}]) do | ||||
|     ab = b - a | ||||
|     cd = d - c | ||||
|  | ||||
|     cond do | ||||
|       ab == 0 and cd != 0 -> cd | ||||
|       ab != 0 and cd == 0 -> ab | ||||
|       ab != 0 and cd != 0 -> ab * cd | ||||
|       ab == 0 and cd == 0 -> -1 | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # Return the middle bounding box value | ||||
|   def middle_value([{a, b}, {c, d}]) do | ||||
|     (a + b + c + d) / 2 | ||||
|   end | ||||
|  | ||||
|   def get_posxy([{a, b}, {c, d}]) do | ||||
|     %{x: (b + a) / 2, y: (c + d) / 2} | ||||
|   end | ||||
|  | ||||
|   def box_move([{a, b}, {c, d}], move) do | ||||
|     x = move[:x] | ||||
|     y = move[:y] | ||||
|     [{a + x, b + x}, {c + y, d + y}] | ||||
|   end | ||||
| end | ||||
| @@ -274,7 +274,7 @@ defmodule WandererApp.Esi.ApiClient do | ||||
|             ) | ||||
|   def get_alliance_info(eve_id, opts \\ []) do | ||||
|     case _get_alliance_info(eve_id, "", opts) do | ||||
|       {:ok, result} -> {:ok, result |> Map.merge(%{"eve_id" => eve_id})} | ||||
|       {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)} | ||||
|       {:error, error} -> {:error, error} | ||||
|     end | ||||
|   end | ||||
| @@ -286,7 +286,7 @@ defmodule WandererApp.Esi.ApiClient do | ||||
|             ) | ||||
|   def get_corporation_info(eve_id, opts \\ []) do | ||||
|     case _get_corporation_info(eve_id, "", opts) do | ||||
|       {:ok, result} -> {:ok, result |> Map.merge(%{"eve_id" => eve_id})} | ||||
|       {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)} | ||||
|       {:error, error} -> {:error, error} | ||||
|     end | ||||
|   end | ||||
| @@ -301,7 +301,7 @@ defmodule WandererApp.Esi.ApiClient do | ||||
|            "/characters/#{eve_id}/", | ||||
|            opts |> _with_cache_opts() | ||||
|          ) do | ||||
|       {:ok, result} -> {:ok, result |> Map.merge(%{"eve_id" => eve_id})} | ||||
|       {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)} | ||||
|       {:error, error} -> {:error, error} | ||||
|     end | ||||
|   end | ||||
|   | ||||
| @@ -67,8 +67,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_wormhole_types() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholes.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholes.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         id: row["typeID"], | ||||
|         name: row["name"], | ||||
| @@ -85,8 +85,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_wormhole_classes() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeClasses.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeClasses.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         id: row["id"], | ||||
|         short_name: row["shortName"], | ||||
| @@ -98,8 +98,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_wormhole_systems() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeSystems.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeSystems.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         solar_system_id: row["solarSystemID"], | ||||
|         wanderers: row["wanderers"], | ||||
| @@ -111,8 +111,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_effects() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/effects.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/effects.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         id: row["name"] |> Slug.slugify(), | ||||
|         name: row["name"], | ||||
| @@ -130,8 +130,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_triglavian_systems() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/triglavianSystems.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/triglavianSystems.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         solar_system_id: row["solarSystemID"], | ||||
|         solar_system_name: row["solarSystemName"], | ||||
| @@ -377,9 +377,21 @@ defmodule WandererApp.EveDataService do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp get_true_security(security) when is_float(security) and security > 0.0 and security < 0.05, do: security |> Float.ceil(1) | ||||
|   defp truncate_to_two_digits(value) when is_float(value), do: Float.floor(value * 100) / 100 | ||||
|  | ||||
|   defp get_true_security(security) when is_float(security), do: security |> Float.floor(1) | ||||
|   defp get_true_security(security) when is_float(security) and security > 0.0 and security < 0.05, | ||||
|     do: security |> Float.ceil(1) | ||||
|  | ||||
|   defp get_true_security(security) when is_float(security) do | ||||
|     truncated_value = security |> truncate_to_two_digits() | ||||
|     floor_value = truncated_value |> Float.floor(1) | ||||
|  | ||||
|     if Float.round(truncated_value - floor_value, 2) < 0.05 do | ||||
|       floor_value | ||||
|     else | ||||
|       Float.ceil(truncated_value, 1) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp get_class_title(wormhole_classes_info, wormhole_class_id, security, wormhole_class) do | ||||
|     case wormhole_class_id in [ | ||||
|   | ||||
| @@ -375,17 +375,21 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|  | ||||
|     case not is_nil(user_id) do | ||||
|       true -> | ||||
|         :telemetry.execute( | ||||
|           [:wanderer_app, :map, :systems, :remove], | ||||
|           %{count: removed_ids |> Enum.count()}, | ||||
|           %{ | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_map_event(:systems_removed, %{ | ||||
|             character_id: character_id, | ||||
|             user_id: user_id, | ||||
|             map_id: map_id, | ||||
|             solar_system_ids: removed_ids | ||||
|           } | ||||
|           }) | ||||
|  | ||||
|         :telemetry.execute( | ||||
|           [:wanderer_app, :map, :systems, :remove], | ||||
|           %{count: removed_ids |> Enum.count()} | ||||
|         ) | ||||
|  | ||||
|         :ok | ||||
|  | ||||
|       _ -> | ||||
|         :ok | ||||
|     end | ||||
| @@ -1165,12 +1169,15 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|  | ||||
|     broadcast!(map_id, :add_system, system) | ||||
|  | ||||
|     :telemetry.execute([:wanderer_app, :map, :system, :add], %{count: 1}, %{ | ||||
|       character_id: character_id, | ||||
|       user_id: user_id, | ||||
|       map_id: map_id, | ||||
|       solar_system_id: solar_system_id | ||||
|     }) | ||||
|     {:ok, _} = | ||||
|       WandererApp.User.ActivityTracker.track_map_event(:system_added, %{ | ||||
|         character_id: character_id, | ||||
|         user_id: user_id, | ||||
|         map_id: map_id, | ||||
|         solar_system_id: solar_system_id | ||||
|       }) | ||||
|  | ||||
|     :telemetry.execute([:wanderer_app, :map, :system, :add], %{count: 1}) | ||||
|  | ||||
|     state | ||||
|   end | ||||
| @@ -1586,12 +1593,17 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|         :ok | ||||
|  | ||||
|       _ -> | ||||
|         :telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{ | ||||
|           map_id: map_id, | ||||
|           character: character, | ||||
|           solar_system_source_id: old_location.solar_system_id, | ||||
|           solar_system_target_id: location.solar_system_id | ||||
|         }) | ||||
|         :telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{}) | ||||
|  | ||||
|         {:ok, _} = | ||||
|           WandererApp.Api.MapChainPassages.new(%{ | ||||
|             map_id: map_id, | ||||
|             character_id: character.id, | ||||
|             ship_type_id: character.ship, | ||||
|             ship_name: character.ship_name, | ||||
|             solar_system_source_id: old_location.solar_system_id, | ||||
|             solar_system_target_id: location.solar_system_id | ||||
|           }) | ||||
|     end | ||||
|  | ||||
|     case WandererApp.Map.check_connection(map_id, location, old_location) do | ||||
|   | ||||
| @@ -55,13 +55,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do | ||||
|   def handle_info({ref, result}, state) do | ||||
|     Process.demonitor(ref, [:flush]) | ||||
|  | ||||
|     case result do | ||||
|       :ok -> | ||||
|         {:noreply, state} | ||||
|  | ||||
|       _ -> | ||||
|         {:noreply, state} | ||||
|     end | ||||
|     {:noreply, state} | ||||
|   end | ||||
|  | ||||
|   defp _update_map_kills(map_id) do | ||||
| @@ -70,10 +64,9 @@ defmodule WandererApp.Map.ZkbDataFetcher do | ||||
|         map_id | ||||
|         |> WandererApp.Map.get_map!() | ||||
|         |> Map.get(:systems, Map.new()) | ||||
|         |> Map.keys() | ||||
|         |> Enum.reduce(Map.new(), fn solar_system_id, acc -> | ||||
|         |> Enum.reduce(Map.new(), fn {solar_system_id, _system}, acc -> | ||||
|           kills_count = WandererApp.Cache.get("zkb_kills_#{solar_system_id}") | ||||
|           acc |> Map.put_new(solar_system_id, kills_count || 0) | ||||
|           acc |> Map.put(solar_system_id, kills_count || 0) | ||||
|         end) | ||||
|         |> _maybe_broadcast_map_kills(map_id) | ||||
|  | ||||
| @@ -87,28 +80,24 @@ defmodule WandererApp.Map.ZkbDataFetcher do | ||||
|  | ||||
|     updated_kills_system_ids = | ||||
|       new_kills_map | ||||
|       |> Map.keys() | ||||
|       |> Enum.filter(fn solar_system_id -> | ||||
|         kills_count = new_kills_map |> Map.get(solar_system_id, 0) | ||||
|         old_kills_count = old_kills_map |> Map.get(solar_system_id, 0) | ||||
|  | ||||
|         kills_count != old_kills_count and | ||||
|           kills_count > 0 | ||||
|       end) | ||||
|  | ||||
|     removed_kills_system_ids = | ||||
|       old_kills_map | ||||
|       |> Map.keys() | ||||
|       |> Enum.filter(fn solar_system_id -> | ||||
|         new_kills_count = new_kills_map |> Map.get(solar_system_id, 0) | ||||
|       |> Map.filter(fn {solar_system_id, new_kills_count} -> | ||||
|         old_kills_count = old_kills_map |> Map.get(solar_system_id, 0) | ||||
|  | ||||
|         new_kills_count != old_kills_count and | ||||
|           old_kills_count > 0 and new_kills_count == 0 | ||||
|           new_kills_count > 0 | ||||
|       end) | ||||
|       |> Map.keys() | ||||
|  | ||||
|     [updated_kills_system_ids | removed_kills_system_ids] | ||||
|     |> List.flatten() | ||||
|     removed_kills_system_ids = | ||||
|       old_kills_map | ||||
|       |> Map.filter(fn {solar_system_id, old_kills_count} -> | ||||
|         new_kills_count = new_kills_map |> Map.get(solar_system_id, 0) | ||||
|  | ||||
|         old_kills_count > 0 and new_kills_count == 0 | ||||
|       end) | ||||
|       |> Map.keys() | ||||
|  | ||||
|     (updated_kills_system_ids ++ removed_kills_system_ids) | ||||
|     |> case do | ||||
|       [] -> | ||||
|         :ok | ||||
|   | ||||
| @@ -9,7 +9,11 @@ defmodule WandererApp.MapConnectionRepo do | ||||
|     do: WandererApp.Api.MapConnection.read_by_map(%{map_id: map_id}) | ||||
|  | ||||
|   def get_by_locations(map_id, solar_system_source, solar_system_target) do | ||||
|     WandererApp.Api.MapConnection.by_locations(%{map_id: map_id, solar_system_source: solar_system_source, solar_system_target: solar_system_target}) | ||||
|     WandererApp.Api.MapConnection.by_locations(%{ | ||||
|       map_id: map_id, | ||||
|       solar_system_source: solar_system_source, | ||||
|       solar_system_target: solar_system_target | ||||
|     }) | ||||
|     |> case do | ||||
|       {:ok, connections} -> | ||||
|         {:ok, connections} | ||||
| @@ -26,8 +30,11 @@ defmodule WandererApp.MapConnectionRepo do | ||||
|   def create!(connection), do: connection |> WandererApp.Api.MapConnection.create!() | ||||
|  | ||||
|   def destroy(map_id, connection) do | ||||
|     {:ok, from_connections} = get_by_locations(map_id, connection.solar_system_source, connection.solar_system_target) | ||||
|     {:ok, to_connections} = get_by_locations(map_id, connection.solar_system_target, connection.solar_system_source) | ||||
|     {:ok, from_connections} = | ||||
|       get_by_locations(map_id, connection.solar_system_source, connection.solar_system_target) | ||||
|  | ||||
|     {:ok, to_connections} = | ||||
|       get_by_locations(map_id, connection.solar_system_target, connection.solar_system_source) | ||||
|  | ||||
|     [from_connections ++ to_connections] | ||||
|     |> List.flatten() | ||||
| @@ -42,8 +49,7 @@ defmodule WandererApp.MapConnectionRepo do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def destroy!(connection), do: | ||||
|     connection |> WandererApp.Api.MapConnection.destroy!() | ||||
|   def destroy!(connection), do: connection |> WandererApp.Api.MapConnection.destroy!() | ||||
|  | ||||
|   def bulk_destroy!(connections) do | ||||
|     connections | ||||
| @@ -51,6 +57,7 @@ defmodule WandererApp.MapConnectionRepo do | ||||
|     |> case do | ||||
|       %Ash.BulkResult{status: :success} -> | ||||
|         :ok | ||||
|  | ||||
|       error -> | ||||
|         error | ||||
|     end | ||||
|   | ||||
							
								
								
									
										9
									
								
								lib/wanderer_app/task_wrapper.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/wanderer_app/task_wrapper.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| defmodule WandererApp.TaskWrapper do | ||||
|   def start_link(module, func, args) do | ||||
|     if Mix.env() == :test do | ||||
|       apply(module, func, args) | ||||
|     else | ||||
|       Task.start_link(module, func, args) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -1,114 +1,16 @@ | ||||
| defmodule WandererApp.User.ActivityTracker do | ||||
|   @moduledoc false | ||||
|   use GenServer | ||||
|  | ||||
|   require Logger | ||||
|  | ||||
|   @name __MODULE__ | ||||
|   def track_map_event( | ||||
|         event_type, | ||||
|         metadata | ||||
|       ), | ||||
|       do: WandererApp.Map.Audit.track_map_event(event_type, metadata) | ||||
|  | ||||
|   def start_link(args) do | ||||
|     GenServer.start(__MODULE__, args, name: @name) | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def init(_args) do | ||||
|     Logger.info("#{__MODULE__} started") | ||||
|  | ||||
|     {:ok, %{}, {:continue, :start}} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_continue(:start, state) do | ||||
|     :telemetry.attach_many( | ||||
|       "map_user_activity", | ||||
|       [ | ||||
|         [:wanderer_app, :map, :hub, :add], | ||||
|         [:wanderer_app, :map, :hub, :remove], | ||||
|         [:wanderer_app, :map, :system, :add], | ||||
|         [:wanderer_app, :map, :system, :update], | ||||
|         [:wanderer_app, :map, :systems, :remove], | ||||
|         [:wanderer_app, :map, :connection, :add], | ||||
|         [:wanderer_app, :map, :connection, :update], | ||||
|         [:wanderer_app, :map, :connection, :remove], | ||||
|         [:wanderer_app, :map, :acl, :add], | ||||
|         [:wanderer_app, :map, :acl, :remove], | ||||
|         [:wanderer_app, :acl, :member, :add], | ||||
|         [:wanderer_app, :acl, :member, :remove], | ||||
|         [:wanderer_app, :acl, :member, :update] | ||||
|       ], | ||||
|       &handle_event/4, | ||||
|       nil | ||||
|     ) | ||||
|  | ||||
|     {:noreply, state} | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :system, :add], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_map_event(:system_added, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :hub, :add], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_map_event(:hub_added, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :hub, :remove], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_map_event(:hub_removed, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :system, :update], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_map_event(:system_updated, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :systems, :remove], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_map_event(:systems_removed, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :connection, :add], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_map_event(:map_connection_added, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :connection, :update], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_map_event(:map_connection_updated, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :connection, :remove], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_map_event(:map_connection_removed, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :acl, :member, :add], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_acl_event(:map_acl_member_added, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :acl, :member, :remove], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_acl_event(:map_acl_member_removed, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :acl, :member, :update], _event_data, metadata, _config) do | ||||
|     {:ok, _} = _track_acl_event(:map_acl_member_updated, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :acl, :add], _event_data, _metadata, _config) do | ||||
|     # {:ok, _} = _track_map_event(:map_acl_added, metadata) | ||||
|   end | ||||
|  | ||||
|   def handle_event([:wanderer_app, :map, :acl, :remove], _event_data, _metadata, _config) do | ||||
|     # {:ok, _} = _track_map_event(:map_acl_removed, metadata) | ||||
|   end | ||||
|  | ||||
|   defp _track_map_event( | ||||
|          event_type, | ||||
|          metadata | ||||
|        ), | ||||
|        do: WandererApp.Map.Audit.track_map_event(event_type, metadata) | ||||
|  | ||||
|   defp _track_acl_event( | ||||
|          event_type, | ||||
|          metadata | ||||
|        ), | ||||
|        do: WandererApp.Map.Audit.track_acl_event(event_type, metadata) | ||||
|  | ||||
|   @impl true | ||||
|   def terminate(_reason, _state) do | ||||
|     :ok | ||||
|   end | ||||
|   def track_acl_event( | ||||
|         event_type, | ||||
|         metadata | ||||
|       ), | ||||
|       do: WandererApp.Map.Audit.track_acl_event(event_type, metadata) | ||||
| end | ||||
|   | ||||
| @@ -6,14 +6,9 @@ defmodule WandererApp.Utils.JSONUtil do | ||||
|     Jason.decode(body) | ||||
|   end | ||||
|  | ||||
|   def map_json({:ok, json}, mapper) do | ||||
|     Enum.map(json, mapper) | ||||
|   end | ||||
|  | ||||
|   def compress(data) do | ||||
|     data | ||||
|     |> Jason.encode!() | ||||
|     |> :zlib.compress() | ||||
|     |> Base.encode64() | ||||
|   end | ||||
|   def read_json!(filename), | ||||
|     do: | ||||
|       filename | ||||
|       |> File.read!() | ||||
|       |> Jason.decode!() | ||||
| end | ||||
|   | ||||
| @@ -63,7 +63,7 @@ defmodule WandererApp.Zkb.KillsProvider do | ||||
|   end | ||||
|  | ||||
|   defp handle_websocket(message, state) do | ||||
|     case message |> _parse_message() do | ||||
|     case message |> parse_message() do | ||||
|       nil -> | ||||
|         {:ok, state} | ||||
|  | ||||
| @@ -109,7 +109,7 @@ defmodule WandererApp.Zkb.KillsProvider do | ||||
|     Logger.warning(fn -> "Terminating client process with reason : #{inspect(reason)}" end) | ||||
|   end | ||||
|  | ||||
|   defp _parse_message( | ||||
|   defp parse_message( | ||||
|          %{ | ||||
|            "solar_system_id" => solar_system_id, | ||||
|            "killmail_time" => killmail_time | ||||
| @@ -123,5 +123,5 @@ defmodule WandererApp.Zkb.KillsProvider do | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   defp _parse_message(_message), do: nil | ||||
|   defp parse_message(_message), do: nil | ||||
| end | ||||
|   | ||||
| @@ -605,9 +605,7 @@ defmodule WandererAppWeb.CoreComponents do | ||||
|             phx-click={@row_click && @row_click.(row)} | ||||
|             class={"hover #{if @row_selected && @row_selected.(row), do: "!bg-slate-600", else: ""} #{if @row_click, do: "cursor-pointer", else: ""}"} | ||||
|           > | ||||
|             <td | ||||
|               :for={{col, _index} <- Enum.with_index(@col)} | ||||
|             > | ||||
|             <td :for={{col, _index} <- Enum.with_index(@col)}> | ||||
|               <%= render_slot(col, @row_item.(row)) %> | ||||
|             </td> | ||||
|             <td :if={@action != []}> | ||||
|   | ||||
| @@ -3,11 +3,15 @@ defmodule WandererAppWeb.BasicAuth do | ||||
|  | ||||
|   def admin_basic_auth(conn, _opts) do | ||||
|     admin_password = WandererApp.Env.admin_password() | ||||
|  | ||||
|     if is_nil(admin_password) do | ||||
|       conn | ||||
|     else | ||||
|       conn | ||||
|       |> Plug.BasicAuth.basic_auth(username: WandererApp.Env.admin_username(), password: admin_password) | ||||
|       |> Plug.BasicAuth.basic_auth( | ||||
|         username: WandererApp.Env.admin_username(), | ||||
|         password: admin_password | ||||
|       ) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -99,8 +99,9 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :add_members, %{"id" => acl_id} = _params) do | ||||
|     with {:ok, %{owner: %{id: _character_id}} = access_list} <- socket.assigns.access_lists |> Enum.find(&(&1.id == acl_id)) |> Ash.load(:owner), | ||||
|      user_character_ids <- socket.assigns.current_user.characters |> Enum.map(& &1.id) do | ||||
|     with {:ok, %{owner: %{id: _character_id}} = access_list} <- | ||||
|            socket.assigns.access_lists |> Enum.find(&(&1.id == acl_id)) |> Ash.load(:owner), | ||||
|          user_character_ids <- socket.assigns.current_user.characters |> Enum.map(& &1.id) do | ||||
|       user_character_ids | ||||
|       |> Enum.each(fn user_character_id -> | ||||
|         :ok = WandererApp.Character.TrackerManager.start_tracking(user_character_id) | ||||
| @@ -125,7 +126,7 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|       ) | ||||
|     else | ||||
|       _ -> | ||||
|       socket | ||||
|         socket | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -373,12 +374,16 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|           member | ||||
|           |> WandererApp.Api.AccessListMember.update_role!(%{role: role_atom}) | ||||
|  | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :update], %{count: 1}, %{ | ||||
|           user_id: socket.assigns.current_user.id, | ||||
|           acl_id: socket.assigns.selected_acl_id, | ||||
|           member: | ||||
|             member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|         }) | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_updated, %{ | ||||
|             user_id: socket.assigns.current_user.id, | ||||
|             acl_id: socket.assigns.selected_acl_id, | ||||
|             member: | ||||
|               member | ||||
|               |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|           }) | ||||
|  | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :update], %{count: 1}) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
|           WandererApp.PubSub, | ||||
| @@ -488,12 +493,16 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|            eve_corporation_id: nil | ||||
|          }) do | ||||
|       {:ok, member} -> | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ | ||||
|           user_id: socket.assigns.current_user.id, | ||||
|           acl_id: access_list_id, | ||||
|           member: | ||||
|             member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|         }) | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{ | ||||
|             user_id: socket.assigns.current_user.id, | ||||
|             acl_id: access_list_id, | ||||
|             member: | ||||
|               member | ||||
|               |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|           }) | ||||
|  | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}) | ||||
|  | ||||
|         {:ok, member} | ||||
|  | ||||
| @@ -515,12 +524,16 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|            eve_corporation_id: eve_id | ||||
|          }) do | ||||
|       {:ok, member} -> | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ | ||||
|           user_id: socket.assigns.current_user.id, | ||||
|           acl_id: access_list_id, | ||||
|           member: | ||||
|             member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|         }) | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{ | ||||
|             user_id: socket.assigns.current_user.id, | ||||
|             acl_id: access_list_id, | ||||
|             member: | ||||
|               member | ||||
|               |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|           }) | ||||
|  | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}) | ||||
|  | ||||
|         {:ok, member} | ||||
|  | ||||
| @@ -543,12 +556,16 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|            role: :viewer | ||||
|          }) do | ||||
|       {:ok, member} -> | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ | ||||
|           user_id: socket.assigns.current_user.id, | ||||
|           acl_id: access_list_id, | ||||
|           member: | ||||
|             member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|         }) | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{ | ||||
|             user_id: socket.assigns.current_user.id, | ||||
|             acl_id: access_list_id, | ||||
|             member: | ||||
|               member | ||||
|               |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|           }) | ||||
|  | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}) | ||||
|  | ||||
|         {:ok, member} | ||||
|  | ||||
| @@ -647,6 +664,6 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|   end | ||||
|  | ||||
|   defp map_ui_acl(acl, selected_id) do | ||||
|     acl |> Map.merge(%{selected: acl.id == selected_id}) | ||||
|     acl |> Map.put(:selected, acl.id == selected_id) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -44,6 +44,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do | ||||
|       case WandererApp.Api.MapCharacterSettings.read_by_map(%{map_id: selected_map.id}) do | ||||
|         {:ok, settings} -> | ||||
|           {:ok, settings} | ||||
|  | ||||
|         _ -> | ||||
|           {:ok, []} | ||||
|       end | ||||
|   | ||||
| @@ -44,7 +44,6 @@ defmodule WandererAppWeb.MapLive do | ||||
|          id: map_id, | ||||
|          deleted: false | ||||
|        } = map} -> | ||||
|  | ||||
|         Process.send_after(self(), {:init_map, map}, 10) | ||||
|  | ||||
|         socket | ||||
| @@ -130,28 +129,28 @@ defmodule WandererAppWeb.MapLive do | ||||
|   def handle_info(%{event: :add_system, payload: system}, socket) do | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event("add_systems", [map_ui_system(system)])} | ||||
|      |> push_map_event("add_systems", [map_ui_system(system)])} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info(%{event: :update_system, payload: system}, socket) do | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event("update_systems", [map_ui_system(system)])} | ||||
|      |> push_map_event("update_systems", [map_ui_system(system)])} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info(%{event: :update_connection, payload: connection}, socket) do | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event("update_connection", map_ui_connection(connection))} | ||||
|      |> push_map_event("update_connection", map_ui_connection(connection))} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info(%{event: :systems_removed, payload: solar_system_ids}, socket) do | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event("remove_systems", solar_system_ids)} | ||||
|      |> push_map_event("remove_systems", solar_system_ids)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
| @@ -160,7 +159,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|  | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "remove_connections", | ||||
|        connection_ids | ||||
|      )} | ||||
| @@ -172,7 +171,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|  | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "add_connections", | ||||
|        connections | ||||
|      )} | ||||
| @@ -182,7 +181,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|   def handle_info(%{event: :update_map, payload: map_diff}, socket) do | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "map_updated", | ||||
|        map_diff | ||||
|      )} | ||||
| @@ -196,7 +195,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|  | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "kills_updated", | ||||
|        kills | ||||
|      )} | ||||
| @@ -218,7 +217,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|  | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "characters_updated", | ||||
|        characters | ||||
|      )} | ||||
| @@ -228,7 +227,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|   def handle_info(%{event: :character_added, payload: character}, socket) do | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "character_added", | ||||
|        character |> map_ui_character() | ||||
|      )} | ||||
| @@ -238,7 +237,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|   def handle_info(%{event: :character_removed, payload: character}, socket) do | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "character_removed", | ||||
|        character |> map_ui_character() | ||||
|      )} | ||||
| @@ -248,7 +247,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|   def handle_info(%{event: :character_updated, payload: character}, socket) do | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "character_updated", | ||||
|        character |> map_ui_character() | ||||
|      )} | ||||
| @@ -262,7 +261,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|       do: | ||||
|         {:noreply, | ||||
|          socket | ||||
|          |> _push_map_event( | ||||
|          |> push_map_event( | ||||
|            "present_characters", | ||||
|            present_character_eve_ids | ||||
|          )} | ||||
| @@ -336,7 +335,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|  | ||||
|           socket | ||||
|           |> assign(user_permissions: user_permissions) | ||||
|           |> _push_map_event( | ||||
|           |> push_map_event( | ||||
|             "user_permissions", | ||||
|             user_permissions | ||||
|           ) | ||||
| @@ -376,17 +375,24 @@ defmodule WandererAppWeb.MapLive do | ||||
|       tracked_character_ids = | ||||
|         availaible_map_characters |> Enum.filter(& &1.tracked) |> Enum.map(& &1.id) | ||||
|  | ||||
|       all_character_tracked? = (not (availaible_map_characters |> Enum.empty?())) and availaible_map_characters |> Enum.all?(& &1.tracked) | ||||
|       all_character_tracked? = | ||||
|         not (availaible_map_characters |> Enum.empty?()) and | ||||
|           availaible_map_characters |> Enum.all?(& &1.tracked) | ||||
|  | ||||
|       cond do | ||||
|         (only_tracked_characters and can_track? and all_character_tracked?) or | ||||
|             (not only_tracked_characters and can_view?) -> | ||||
|             Process.send_after(self(), {:map_init, %{ | ||||
|               map_id: map_id, | ||||
|               page_title: map_name, | ||||
|               user_permissions: user_permissions, | ||||
|               tracked_character_ids: tracked_character_ids | ||||
|             }}, 10) | ||||
|           Process.send_after( | ||||
|             self(), | ||||
|             {:map_init, | ||||
|              %{ | ||||
|                map_id: map_id, | ||||
|                page_title: map_name, | ||||
|                user_permissions: user_permissions, | ||||
|                tracked_character_ids: tracked_character_ids | ||||
|              }}, | ||||
|             10 | ||||
|           ) | ||||
|  | ||||
|         only_tracked_characters and can_track? and not all_character_tracked? -> | ||||
|           Process.send_after(self(), :not_all_characters_tracked, 10) | ||||
| @@ -406,17 +412,20 @@ defmodule WandererAppWeb.MapLive do | ||||
|     Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id) | ||||
|  | ||||
|     {:noreply, | ||||
|       socket | ||||
|       |> assign(initial_data)} | ||||
|      socket | ||||
|      |> assign(initial_data)} | ||||
|   end | ||||
|  | ||||
|   def handle_info({:map_start, | ||||
|    %{ | ||||
|      map_id: map_id, | ||||
|      user_characters: user_character_eve_ids, | ||||
|      initial_data: initial_data, | ||||
|      events: events | ||||
|    } = _started_data}, socket) do | ||||
|   def handle_info( | ||||
|         {:map_start, | ||||
|          %{ | ||||
|            map_id: map_id, | ||||
|            user_characters: user_character_eve_ids, | ||||
|            initial_data: initial_data, | ||||
|            events: events | ||||
|          } = _started_data}, | ||||
|         socket | ||||
|       ) do | ||||
|     socket = | ||||
|       events | ||||
|       |> Enum.reduce(socket, fn event, socket -> | ||||
| @@ -452,66 +461,78 @@ defmodule WandererAppWeb.MapLive do | ||||
|         end | ||||
|       end) | ||||
|  | ||||
|     Process.send_after(self(), {:map_loaded, | ||||
|       %{ | ||||
|         map_id: map_id, | ||||
|         user_characters: user_character_eve_ids, | ||||
|         initial_data: initial_data | ||||
|       }}, 10) | ||||
|     Process.send_after( | ||||
|       self(), | ||||
|       {:map_loaded, | ||||
|        %{ | ||||
|          map_id: map_id, | ||||
|          user_characters: user_character_eve_ids, | ||||
|          initial_data: initial_data | ||||
|        }}, | ||||
|       10 | ||||
|     ) | ||||
|  | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   def handle_info({:map_loaded, | ||||
|    %{ | ||||
|      map_id: map_id, | ||||
|      user_characters: user_character_eve_ids, | ||||
|      initial_data: initial_data | ||||
|    } = _loaded_data}, socket) do | ||||
|   def handle_info( | ||||
|         {:map_loaded, | ||||
|          %{ | ||||
|            map_id: map_id, | ||||
|            user_characters: user_character_eve_ids, | ||||
|            initial_data: initial_data | ||||
|          } = _loaded_data}, | ||||
|         socket | ||||
|       ) do | ||||
|     map_characters = map_id |> WandererApp.Map.list_characters() | ||||
|  | ||||
|     {:noreply, | ||||
|       socket | ||||
|       |> assign( | ||||
|         map_loaded?: true, | ||||
|         user_characters: user_character_eve_ids, | ||||
|         has_tracked_characters?: _has_tracked_characters?(user_character_eve_ids) | ||||
|       ) | ||||
|       |> _push_map_event("init", initial_data |> Map.merge(%{ | ||||
|         characters: map_characters |> Enum.map(&map_ui_character/1) | ||||
|       })) | ||||
|       |> push_event("js-exec", %{ | ||||
|         to: "#map-loader", | ||||
|         attr: "data-loaded" | ||||
|       })} | ||||
|      socket | ||||
|      |> assign( | ||||
|        map_loaded?: true, | ||||
|        user_characters: user_character_eve_ids, | ||||
|        has_tracked_characters?: _has_tracked_characters?(user_character_eve_ids) | ||||
|      ) | ||||
|      |> push_map_event( | ||||
|        "init", | ||||
|        initial_data |> Map.put(:characters, map_characters |> Enum.map(&map_ui_character/1)) | ||||
|      ) | ||||
|      |> push_event("js-exec", %{ | ||||
|        to: "#map-loader", | ||||
|        attr: "data-loaded" | ||||
|      })} | ||||
|   end | ||||
|  | ||||
|   def handle_info(:no_access, socket), do: | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> put_flash(:error, "You don't have an access to this map.") | ||||
|      |> push_navigate(to: ~p"/maps")} | ||||
|   def handle_info(:no_access, socket), | ||||
|     do: | ||||
|       {:noreply, | ||||
|        socket | ||||
|        |> put_flash(:error, "You don't have an access to this map.") | ||||
|        |> push_navigate(to: ~p"/maps")} | ||||
|  | ||||
|   def handle_info(:no_permissions, socket), do: | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> put_flash(:error, "You don't have permissions to use this map.") | ||||
|      |> push_navigate(to: ~p"/maps")} | ||||
|   def handle_info(:no_permissions, socket), | ||||
|     do: | ||||
|       {:noreply, | ||||
|        socket | ||||
|        |> put_flash(:error, "You don't have permissions to use this map.") | ||||
|        |> push_navigate(to: ~p"/maps")} | ||||
|  | ||||
|   def handle_info(:not_all_characters_tracked, socket), do: | ||||
|     {:noreply, | ||||
|      socket | ||||
|      |> put_flash( | ||||
|        :error, | ||||
|        "You should enable tracking for all characters that have access to this map first!" | ||||
|      ) | ||||
|      |> push_navigate(to: ~p"/tracking/#{socket.assigns.map_slug}")} | ||||
|   def handle_info(:not_all_characters_tracked, socket), | ||||
|     do: | ||||
|       {:noreply, | ||||
|        socket | ||||
|        |> put_flash( | ||||
|          :error, | ||||
|          "You should enable tracking for all characters that have access to this map first!" | ||||
|        ) | ||||
|        |> push_navigate(to: ~p"/tracking/#{socket.assigns.map_slug}")} | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info( | ||||
|         {ref, result}, | ||||
|         socket | ||||
|       ) when is_reference(ref) do | ||||
|       ) | ||||
|       when is_reference(ref) do | ||||
|     Process.demonitor(ref, [:flush]) | ||||
|  | ||||
|     case result do | ||||
| @@ -539,7 +560,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|       {:routes, {solar_system_id, %{routes: routes, systems_static_data: systems_static_data}}} -> | ||||
|         {:noreply, | ||||
|          socket | ||||
|          |> _push_map_event( | ||||
|          |> push_map_event( | ||||
|            "routes", | ||||
|            %{ | ||||
|              solar_system_id: solar_system_id, | ||||
| @@ -571,12 +592,11 @@ defmodule WandererAppWeb.MapLive do | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("reconnected", _body, socket) do | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("change_map", %{"map_slug" => map_slug} = _event, %{assigns: %{map_id: map_id}} = socket) do | ||||
|   def handle_event( | ||||
|         "change_map", | ||||
|         %{"map_slug" => map_slug} = _event, | ||||
|         %{assigns: %{map_id: map_id}} = socket | ||||
|       ) do | ||||
|     Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id) | ||||
|     {:noreply, push_navigate(socket, to: ~p"/#{map_slug}")} | ||||
|   end | ||||
| @@ -637,13 +657,16 @@ defmodule WandererAppWeb.MapLive do | ||||
|               solar_system_target_id: solar_system_target_id |> String.to_integer() | ||||
|             }) | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :connection, :add], %{count: 1}, %{ | ||||
|               character_id: tracked_character_ids |> List.first(), | ||||
|               user_id: current_user.id, | ||||
|               map_id: map_id, | ||||
|               solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), | ||||
|               solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer() | ||||
|             }) | ||||
|             {:ok, _} = | ||||
|               WandererApp.User.ActivityTracker.track_map_event(:map_connection_added, %{ | ||||
|                 character_id: tracked_character_ids |> List.first(), | ||||
|                 user_id: current_user.id, | ||||
|                 map_id: map_id, | ||||
|                 solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), | ||||
|                 solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer() | ||||
|               }) | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :connection, :add], %{count: 1}) | ||||
|  | ||||
|             {:noreply, socket} | ||||
|  | ||||
| @@ -682,18 +705,21 @@ defmodule WandererAppWeb.MapLive do | ||||
|               socket | ||||
|               |> map_id() | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :hub, :add], %{count: 1}, %{ | ||||
|               character_id: tracked_character_ids |> List.first(), | ||||
|               user_id: current_user.id, | ||||
|               map_id: map_id, | ||||
|               solar_system_id: solar_system_id | ||||
|             }) | ||||
|  | ||||
|             map_id | ||||
|             |> WandererApp.Map.Server.add_hub(%{ | ||||
|               solar_system_id: solar_system_id | ||||
|             }) | ||||
|  | ||||
|             {:ok, _} = | ||||
|               WandererApp.User.ActivityTracker.track_map_event(:hub_added, %{ | ||||
|                 character_id: tracked_character_ids |> List.first(), | ||||
|                 user_id: current_user.id, | ||||
|                 map_id: map_id, | ||||
|                 solar_system_id: solar_system_id | ||||
|               }) | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :hub, :add], %{count: 1}) | ||||
|  | ||||
|             {:noreply, socket} | ||||
|  | ||||
|           _ -> | ||||
| @@ -731,18 +757,21 @@ defmodule WandererAppWeb.MapLive do | ||||
|               socket | ||||
|               |> map_id() | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :hub, :remove], %{count: 1}, %{ | ||||
|               character_id: tracked_character_ids |> List.first(), | ||||
|               user_id: current_user.id, | ||||
|               map_id: map_id, | ||||
|               solar_system_id: solar_system_id | ||||
|             }) | ||||
|  | ||||
|             map_id | ||||
|             |> WandererApp.Map.Server.remove_hub(%{ | ||||
|               solar_system_id: solar_system_id | ||||
|             }) | ||||
|  | ||||
|             {:ok, _} = | ||||
|               WandererApp.User.ActivityTracker.track_map_event(:hub_removed, %{ | ||||
|                 character_id: tracked_character_ids |> List.first(), | ||||
|                 user_id: current_user.id, | ||||
|                 map_id: map_id, | ||||
|                 solar_system_id: solar_system_id | ||||
|               }) | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :hub, :remove], %{count: 1}) | ||||
|  | ||||
|             {:noreply, socket} | ||||
|  | ||||
|           _ -> | ||||
| @@ -802,15 +831,6 @@ defmodule WandererAppWeb.MapLive do | ||||
|               socket | ||||
|               |> map_id() | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :system, :update], %{count: 1}, %{ | ||||
|               character_id: tracked_character_ids |> List.first(), | ||||
|               user_id: current_user.id, | ||||
|               map_id: map_id, | ||||
|               solar_system_id: "#{solar_system_id}" |> String.to_integer(), | ||||
|               key: key_atom, | ||||
|               value: value | ||||
|             }) | ||||
|  | ||||
|             apply(WandererApp.Map.Server, method_atom, [ | ||||
|               map_id, | ||||
|               %{ | ||||
| @@ -819,6 +839,18 @@ defmodule WandererAppWeb.MapLive do | ||||
|               |> Map.put_new(key_atom, value) | ||||
|             ]) | ||||
|  | ||||
|             {:ok, _} = | ||||
|               WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{ | ||||
|                 character_id: tracked_character_ids |> List.first(), | ||||
|                 user_id: current_user.id, | ||||
|                 map_id: map_id, | ||||
|                 solar_system_id: "#{solar_system_id}" |> String.to_integer(), | ||||
|                 key: key_atom, | ||||
|                 value: value | ||||
|               }) | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :system, :update], %{count: 1}) | ||||
|  | ||||
|             {:noreply, socket} | ||||
|  | ||||
|           _ -> | ||||
| @@ -878,15 +910,18 @@ defmodule WandererAppWeb.MapLive do | ||||
|               socket | ||||
|               |> map_id() | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :connection, :update], %{count: 1}, %{ | ||||
|               character_id: tracked_character_ids |> List.first(), | ||||
|               user_id: current_user.id, | ||||
|               map_id: map_id, | ||||
|               solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), | ||||
|               solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer(), | ||||
|               key: key_atom, | ||||
|               value: value | ||||
|             }) | ||||
|             {:ok, _} = | ||||
|               WandererApp.User.ActivityTracker.track_map_event(:map_connection_updated, %{ | ||||
|                 character_id: tracked_character_ids |> List.first(), | ||||
|                 user_id: current_user.id, | ||||
|                 map_id: map_id, | ||||
|                 solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), | ||||
|                 solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer(), | ||||
|                 key: key_atom, | ||||
|                 value: value | ||||
|               }) | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :connection, :update], %{count: 1}) | ||||
|  | ||||
|             apply(WandererApp.Map.Server, method_atom, [ | ||||
|               map_id, | ||||
| @@ -1230,13 +1265,16 @@ defmodule WandererAppWeb.MapLive do | ||||
|               solar_system_target_id: solar_system_target_id |> String.to_integer() | ||||
|             }) | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :connection, :remove], %{count: 1}, %{ | ||||
|               character_id: tracked_character_ids |> List.first(), | ||||
|               user_id: current_user.id, | ||||
|               map_id: map_id, | ||||
|               solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), | ||||
|               solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer() | ||||
|             }) | ||||
|             {:ok, _} = | ||||
|               WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{ | ||||
|                 character_id: tracked_character_ids |> List.first(), | ||||
|                 user_id: current_user.id, | ||||
|                 map_id: map_id, | ||||
|                 solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), | ||||
|                 solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer() | ||||
|               }) | ||||
|  | ||||
|             :telemetry.execute([:wanderer_app, :map, :connection, :remove], %{count: 1}) | ||||
|  | ||||
|             {:noreply, socket} | ||||
|  | ||||
| @@ -1424,7 +1462,7 @@ defmodule WandererAppWeb.MapLive do | ||||
|      |> assign_async(:characters, fn -> | ||||
|        {:ok, %{characters: characters}} | ||||
|      end) | ||||
|      |> _push_map_event( | ||||
|      |> push_map_event( | ||||
|        "init", | ||||
|        %{ | ||||
|          user_characters: user_character_eve_ids, | ||||
| @@ -1564,16 +1602,20 @@ defmodule WandererAppWeb.MapLive do | ||||
|           ) | ||||
|           |> Map.put(:reset, true) | ||||
|  | ||||
|         Process.send_after(self(), {:map_start, %{ | ||||
|           map_id: map_id, | ||||
|           user_characters: user_character_eve_ids, | ||||
|           initial_data: initial_data, | ||||
|           events: events | ||||
|         }}, 10) | ||||
|         Process.send_after( | ||||
|           self(), | ||||
|           {:map_start, | ||||
|            %{ | ||||
|              map_id: map_id, | ||||
|              user_characters: user_character_eve_ids, | ||||
|              initial_data: initial_data, | ||||
|              events: events | ||||
|            }}, | ||||
|           10 | ||||
|         ) | ||||
|  | ||||
|       _ -> | ||||
|         Process.send_after(self(), :no_access, 10) | ||||
|  | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -2079,14 +2121,13 @@ defmodule WandererAppWeb.MapLive do | ||||
|     :ok = WandererApp.Character.TrackerManager.start_tracking(character_id) | ||||
|   end | ||||
|  | ||||
|   defp _push_map_event(socket, type, event_body) | ||||
|     do | ||||
|   defp push_map_event(socket, type, body), | ||||
|     do: | ||||
|       socket | ||||
|       |> push_event("map_event", %{ | ||||
|         type: type, | ||||
|         body: event_body |> WandererApp.Utils.JSONUtil.compress() | ||||
|         body: body | ||||
|       }) | ||||
|     end | ||||
|  | ||||
|   defp map_id(%{assigns: %{map_id: map_id}} = _socket), do: map_id | ||||
| end | ||||
|   | ||||
| @@ -13,8 +13,6 @@ | ||||
|       <span class="Loader__Circle"></span> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|  | ||||
| </div> | ||||
|  | ||||
| <div class="w-full h-full" id="mapper" phx-hook="Mapper" phx-update="ignore"></div> | ||||
|   | ||||
| @@ -594,20 +594,12 @@ defmodule WandererAppWeb.MapsLive do | ||||
|  | ||||
|         added_acls | ||||
|         |> Enum.each(fn acl_id -> | ||||
|           :telemetry.execute([:wanderer_app, :map, :acl, :add], %{count: 1}, %{ | ||||
|             user_id: current_user.id, | ||||
|             map_id: map.id, | ||||
|             acl_id: acl_id | ||||
|           }) | ||||
|           :telemetry.execute([:wanderer_app, :map, :acl, :add], %{count: 1}) | ||||
|         end) | ||||
|  | ||||
|         removed_acls | ||||
|         |> Enum.each(fn acl_id -> | ||||
|           :telemetry.execute([:wanderer_app, :map, :acl, :remove], %{count: 1}, %{ | ||||
|             user_id: current_user.id, | ||||
|             map_id: map.id, | ||||
|             acl_id: acl_id | ||||
|           }) | ||||
|           :telemetry.execute([:wanderer_app, :map, :acl, :remove], %{count: 1}) | ||||
|         end) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
| @@ -898,6 +890,6 @@ defmodule WandererAppWeb.MapsLive do | ||||
|  | ||||
|   defp map_map(%{acls: acls} = map) do | ||||
|     map | ||||
|     |> Map.merge(%{acls: acls |> Enum.map(&map_acl/1)}) | ||||
|     |> Map.put(:acls, acls |> Enum.map(&map_acl/1)) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -200,8 +200,6 @@ defmodule WandererAppWeb.Router do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|  | ||||
|  | ||||
|   # Enable LiveDashboard and Swoosh mailbox preview in development | ||||
|   if Application.compile_env(:wanderer_app, :dev_routes) do | ||||
|     # If you want to use the LiveDashboard in production, you should put | ||||
|   | ||||
							
								
								
									
										5
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								mix.exs
									
									
									
									
									
								
							| @@ -100,7 +100,7 @@ defmodule WandererApp.MixProject do | ||||
|       {:makeup_elixir, ">= 0.0.0"}, | ||||
|       {:makeup_erlang, ">= 0.0.0"}, | ||||
|       {:better_number, "~> 1.0.0"}, | ||||
|       {:delta_crdt, "~> 0.6.5"}, | ||||
|       {:delta_crdt, "~> 0.6.5", override: true}, | ||||
|       {:qex, "~> 0.5"}, | ||||
|       {:site_encrypt, "~> 0.6.0"}, | ||||
|       {:bandit, "~> 1.0"}, | ||||
| @@ -111,7 +111,8 @@ defmodule WandererApp.MixProject do | ||||
|       {:mox, "~> 1.1", only: [:test, :integration]}, | ||||
|       {:git_ops, "~> 2.6.1"}, | ||||
|       {:version_tasks, "~> 0.12.0"}, | ||||
|       {:error_tracker, "~> 0.2"} | ||||
|       {:error_tracker, "~> 0.2"}, | ||||
|       {:ddrt, "~> 0.2.1"} | ||||
|     ] | ||||
|   end | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								mix.lock
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								mix.lock
									
									
									
									
									
								
							| @@ -18,6 +18,7 @@ | ||||
|   "crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"}, | ||||
|   "dart_sass": {:hex, :dart_sass, "0.5.1", "d45f20a8e324313689fb83287d4702352793ce8c9644bc254155d12656ade8b6", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "24f8a1c67e8b5267c51a33cbe6c0b5ebf12c2c83ace88b5ac04947d676b4ec81"}, | ||||
|   "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, | ||||
|   "ddrt": {:hex, :ddrt, "0.2.1", "c4e4bddcef36add5de6599ec72ec822699932413ece0ad310e4be4ab2b3ab6d3", [:mix], [{:delta_crdt, "~> 0.5.0", [hex: :delta_crdt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:merkle_map, "~> 0.2.0", [hex: :merkle_map, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "1efcd60cf4ca4a4352e752d7f41ed9d696560e5860ee07d5bf31c16950100365"}, | ||||
|   "debounce_and_throttle": {:hex, :debounce_and_throttle, "0.9.0", "fa86c982963e00365cc9808afa496e82ca2b48f8905c6c79f8edd304800d0892", [:mix], [], "hexpm", "573a7cff4032754023d8e6874f3eff5354864c90b39b692f1fc4a44b3eb7517b"}, | ||||
|   "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, | ||||
|   "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user