P6QOQ6LCNINOHPDWJESN7V43M6E5KY5CUECCQ74QW4HSPAU3J3QQC #!/usr/bin/env bashif [[ "$(uname -s)" == "Darwin" ]]; thenecho "Don't run this on your local computer!"exit 1fiecho "[remote] Updating processor"cd djinmusicpnpm install -rcd ..echo "[remote] Installed"
#!/usr/bin/env bash. $BIN_DIR/_lib.shrsync --progress -Pavuz --exclude-from="$WORKING_BIN_DIR/rsync-deploy.ignore" -e "ssh -i $HOME/.ssh/id_rsa_corda_digital_ocean" "${MONO_DIR}/." "tpcowan@processor.djinmusic.ca:/home/tpcowan/djinmusic"rsync --progress -Pavuz -e "ssh -i $HOME/.ssh/id_rsa_corda_digital_ocean" $WORKING_BIN_DIR/deploy-remote.sh "tpcowan@processor.djinmusic.ca:/home/tpcowan/deploy-remote.sh"ssh -F $HOME/.ssh/id_rsa_corda_digital_ocean tpcowan@processor.djinmusic.ca "sh /home/tpcowan/deploy-remote.sh"
{"state": {"events": {"75c6cfd0-139a-4a33-8826-9c284645f1ae": {"name": "Calgary Weekly List"},"62854dc2-7d97-45d3-be03-f0bac69119f8": {"name": "Toronto Weekly List"},"fe71a1ee-6e64-4d4f-8a03-7b091d93c823": {"name": "Belgium Weekly List"}}}}
{"state": {"events": {"75c6cfd0-139a-4a33-8826-9c284645f1ae": {"users": {"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {"items": {"spotify:track:7nDYw1nNAW4dAqgmW2W3tq": {"vote": 1599514588577},"spotify:track:6JqYhSdTE4WbQrMXxPH5cD": {"vote": 1599514590773},"spotify:track:3QpkbrYXtlU3LRJu3sTK6V": {"vote": 1599514593054},"spotify:track:1yTTMcUhL7rtz08Dsgb7Qb": {"vote": 1599514595941},"spotify:track:6S1IgeHxxOT9qVWnmsdGxe": {"vote": 1599514599534},"spotify:track:3VXvKTOQoY0kWvpjU67uq2": {"vote": 1599514649778},"spotify:track:2P0FH5jSRu8cctdYfTXtje": {"vote": 1599514650996},"spotify:track:6As34Fmjj7dtReKB51NOVc": {"vote": 1600015121994},"spotify:track:5t9KYe0Fhd5cW6UYT4qP8f": {"vote": 1600015123841},"spotify:track:17jEoYoOfRD6dvNCMmC9n4": {"vote": 1600015125507},"spotify:track:1tkg4EHVoqnhR6iFEXb60y": {"vote": 1602363580571},"spotify:track:5u1n1kITHCxxp8twBcZxWy": {"vote": 1602363581598},"spotify:track:5KCbr5ndeby4y4ggthdiAb": {"vote": 1602363582535},"spotify:track:0sNOPYInjylsM8ZnQozPjt": {"vote": 1608595195694},"spotify:track:4U1c58fpDgbjkb6sVQg26L": {"vote": 1608595196665},"spotify:track:5ryZK3msA04LNcnMaMtm6p": {"vote": 1608595197424},"spotify:track:00deiAYxr1qQx4km9ftnPK": {"vote": 1608595199560},"spotify:track:7lQ8MOhq6IN2w8EYcFNSUk": {"vote": 1608595299092}}},"f9666a8a-4df1-5d14-9b66-8d08d8d3df58": {"items": {"spotify:track:5t9KYe0Fhd5cW6UYT4qP8f": {"vote": 1600015220987},"spotify:track:6As34Fmjj7dtReKB51NOVc": {"vote": 1600015223023},"spotify:track:1zXpHPdBAUxnOCQqFMFLk3": {"vote": 1600015224884},"spotify:track:2kXeAEpGBN874ZKJPV24fr": {"vote": 1600016939990},"spotify:track:03ITeFvMvTRpTC92WsQWw5": {"vote": 1600016941050},"spotify:track:3eCwKRKjGT0EIJe3FKOjIo": {"vote": 1600016942825},"spotify:track:3HVRywtkhSjhpmkaeaYTgh": {"vote": 1600028103232},"spotify:track:0YedjUOqafibhe8htcD6Gz": {"vote": 1600028946840},"spotify:track:4G3DWijMhNkWZwLcxnDI0H": {"vote": 1600028954864},"spotify:track:54WIS7qug0Gnt65eD9gg8g": {"vote": 1600035577353},"spotify:track:4kK14radw0XfwxJDPt9tnP": {"vote": 1600036253132}}}},"playlist": [["spotify:track:5t9KYe0Fhd5cW6UYT4qP8f",2],["spotify:track:6As34Fmjj7dtReKB51NOVc",2],["spotify:track:4kK14radw0XfwxJDPt9tnP",1],["spotify:track:54WIS7qug0Gnt65eD9gg8g",1],["spotify:track:4G3DWijMhNkWZwLcxnDI0H",1],["spotify:track:0YedjUOqafibhe8htcD6Gz",1],["spotify:track:3HVRywtkhSjhpmkaeaYTgh",1],["spotify:track:3eCwKRKjGT0EIJe3FKOjIo",1],["spotify:track:03ITeFvMvTRpTC92WsQWw5",1],["spotify:track:2kXeAEpGBN874ZKJPV24fr",1],["spotify:track:1zXpHPdBAUxnOCQqFMFLk3",1],["spotify:track:7lQ8MOhq6IN2w8EYcFNSUk",1],["spotify:track:00deiAYxr1qQx4km9ftnPK",1],["spotify:track:5ryZK3msA04LNcnMaMtm6p",1],["spotify:track:4U1c58fpDgbjkb6sVQg26L",1],["spotify:track:0sNOPYInjylsM8ZnQozPjt",1],["spotify:track:5KCbr5ndeby4y4ggthdiAb",1],["spotify:track:5u1n1kITHCxxp8twBcZxWy",1],["spotify:track:1tkg4EHVoqnhR6iFEXb60y",1],["spotify:track:17jEoYoOfRD6dvNCMmC9n4",1],["spotify:track:2P0FH5jSRu8cctdYfTXtje",1],["spotify:track:3VXvKTOQoY0kWvpjU67uq2",1],["spotify:track:6S1IgeHxxOT9qVWnmsdGxe",1],["spotify:track:1yTTMcUhL7rtz08Dsgb7Qb",1],["spotify:track:3QpkbrYXtlU3LRJu3sTK6V",1],["spotify:track:6JqYhSdTE4WbQrMXxPH5cD",1],["spotify:track:7nDYw1nNAW4dAqgmW2W3tq",1]]},"62854dc2-7d97-45d3-be03-f0bac69119f8": {"users": {"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {"items": {"spotify:track:45bE4HXI0AwGZXfZtMp8JR": {"vote": 1600015005150},"spotify:track:4wosxLl0mAqhneDzya2MfY": {"vote": 1600015008029},"spotify:track:2J4P46vCFm1rPkNkp9pZWX": {"vote": 1600015009080},"spotify:track:5nLNuK7OoJt36gY9gWgnbo": {"vote": 1607307765906},"spotify:track:5vk6nP3fXbz9FoFmsu5coD": {"vote": 1607307767167},"spotify:track:7ytR5pFWmSjzHJIeQkgog4": {"vote": 1608594919265},"spotify:track:4CNzuSQoL5jgCxzYmuMvcz": {"vote": 1612729873750},"spotify:track:4VSyH8AkIt3kaR5xIPFVVi": {"vote": 1612730821011},"spotify:track:1XXimziG1uhM0eDNCZCrUl": {"vote": 1613256471284},"spotify:track:7lPN2DXiMsVn7XUKtOW1CS": {"vote": 1612733220849},"spotify:track:463CkQjx2Zk1yXoBuierM9": {"vote": 1612733222218},"spotify:track:5QO79kh1waicV47BqGRL3g": {"vote": 1613256469721},"spotify:track:3YJJjQPAbDT7mGpX3WtQ9A": {"vote": 1612733333737},"spotify:track:6Im9k8u9iIzKMrmV7BWtlF": {"vote": 1612733577976},"spotify:track:1FkIrCoa9Lkd8rgZ4VhNP9": {"vote": 1612757417811},"spotify:track:4WPaFfZYr290KKtbc0rEO7": {"vote": 1612757423526},"spotify:track:0izUjTuDrUy2FgQOSRALSU": {"vote": 1612757442106},"spotify:track:4fWK7zJp17fuhDfQ9YnAei": {"vote": 1612757443252},"spotify:track:3CeCwYWvdfXbZLXFhBrbnf": {"vote": 1613258089402},"spotify:track:5Kskr9LcNYa0tpt5f0ZEJx": {"vote": 1613258320712},"spotify:track:7Ei7kZxjEw9d76cEDxoxua": {"vote": 1613259636644},"spotify:track:2NeyJbL3ROKCjRkAjs77ya": {"vote": 1613259033247},"spotify:track:7L6G0wpIUiPXuvoo7qhb06": {"vote": 1613259037259},"spotify:track:5srKMwXoeyrRnyTnNbpgIW": {"vote": 1613259038382},"spotify:track:4Iedi94TIaB2GGb1nMB68v": {"vote": 1613259638535},"spotify:track:26UxwWl9xCb83OynXELJcL": {"vote": 1613259640344},"spotify:track:2oI1Avedp7KK4Wytv2Dx0O": {"vote": 1615669778570},"spotify:track:3hbi5zXAgQt0Z9V5JSOnCe": {"vote": 1625408125835},"USRW29600011": {"vote": 1625614042071},"USRW30900002": {"vote": 1625521507643},"USA2P2125949": {"vote": 1625710090088},"CAUM72100222": {"vote": 1625710098134},"QZES82074435": {"vote": 1625741218535},"USUM72021500": {"vote": 1625741219686},"GBAHS2100318": {"vote": 1626494269063},"FRX202125956": {"vote": 1626743462791}},"events": {"62854dc2-7d97-45d3-be03-f0bac69119f8": {"items": {"spotify:track:2NeyJbL3ROKCjRkAjs77ya": {"vote": 1613259033247},"spotify:track:1FkIrCoa9Lkd8rgZ4VhNP9": {"vote": 1612757417811},"spotify:track:4WPaFfZYr290KKtbc0rEO7": {"vote": 1612757423526},"spotify:track:0izUjTuDrUy2FgQOSRALSU": {"vote": 1612757442106},"spotify:track:4fWK7zJp17fuhDfQ9YnAei": {"vote": 1612757443252},"spotify:track:5QO79kh1waicV47BqGRL3g": {"vote": 1613256469721},"spotify:track:1XXimziG1uhM0eDNCZCrUl": {"vote": 1613256471284},"spotify:track:3CeCwYWvdfXbZLXFhBrbnf": {"vote": 1613258089402},"spotify:track:5Kskr9LcNYa0tpt5f0ZEJx": {"vote": 1613258320712},"spotify:track:7Ei7kZxjEw9d76cEDxoxua": {"vote": 1613259636644},"spotify:track:7L6G0wpIUiPXuvoo7qhb06": {"vote": 1613259037259},"spotify:track:5srKMwXoeyrRnyTnNbpgIW": {"vote": 1613259038382},"spotify:track:4Iedi94TIaB2GGb1nMB68v": {"vote": 1613259638535},"spotify:track:26UxwWl9xCb83OynXELJcL": {"vote": 1613259640344},"spotify:track:2oI1Avedp7KK4Wytv2Dx0O": {"vote": 1615669778570},"spotify:track:3hbi5zXAgQt0Z9V5JSOnCe": {"vote": 1625408125835},"USRW29600011": {"vote": 1625614042071},"USRW30900002": {"vote": 1625521507643},"USA2P2125949": {"vote": 1625710090088},"CAUM72100222": {"vote": 1625710098134},"QZES82074435": {"vote": 1625741218535},"USUM72021500": {"vote": 1625741219686},"GBAHS2100318": {"vote": 1626494269063},"FRX202125956": {"vote": 1626743462791}}}}}},"playlist": [["FRX202125956",1],["GBAHS2100318",1],["USUM72021500",1],["QZES82074435",1],["CAUM72100222",1],["USA2P2125949",1],["USRW30900002",1],["USRW29600011",1],["spotify:track:3hbi5zXAgQt0Z9V5JSOnCe",1],["spotify:track:2oI1Avedp7KK4Wytv2Dx0O",1],["spotify:track:26UxwWl9xCb83OynXELJcL",1],["spotify:track:4Iedi94TIaB2GGb1nMB68v",1],["spotify:track:5srKMwXoeyrRnyTnNbpgIW",1],["spotify:track:7L6G0wpIUiPXuvoo7qhb06",1],["spotify:track:2NeyJbL3ROKCjRkAjs77ya",1],["spotify:track:7Ei7kZxjEw9d76cEDxoxua",1],["spotify:track:5Kskr9LcNYa0tpt5f0ZEJx",1],["spotify:track:3CeCwYWvdfXbZLXFhBrbnf",1],["spotify:track:4fWK7zJp17fuhDfQ9YnAei",1],["spotify:track:0izUjTuDrUy2FgQOSRALSU",1],["spotify:track:4WPaFfZYr290KKtbc0rEO7",1],["spotify:track:1FkIrCoa9Lkd8rgZ4VhNP9",1],["spotify:track:6Im9k8u9iIzKMrmV7BWtlF",1],["spotify:track:3YJJjQPAbDT7mGpX3WtQ9A",1],["spotify:track:5QO79kh1waicV47BqGRL3g",1],["spotify:track:463CkQjx2Zk1yXoBuierM9",1],["spotify:track:7lPN2DXiMsVn7XUKtOW1CS",1],["spotify:track:1XXimziG1uhM0eDNCZCrUl",1],["spotify:track:4VSyH8AkIt3kaR5xIPFVVi",1],["spotify:track:4CNzuSQoL5jgCxzYmuMvcz",1],["spotify:track:7ytR5pFWmSjzHJIeQkgog4",1],["spotify:track:5vk6nP3fXbz9FoFmsu5coD",1],["spotify:track:5nLNuK7OoJt36gY9gWgnbo",1],["spotify:track:2J4P46vCFm1rPkNkp9pZWX",1],["spotify:track:4wosxLl0mAqhneDzya2MfY",1],["spotify:track:45bE4HXI0AwGZXfZtMp8JR",1]],"items": {"spotify:track:1FkIrCoa9Lkd8rgZ4VhNP9": {"name": "My People","artists": ["Erykah Badu"],"image_url": "https://i.scdn.co/image/ab67616d00001e0213066c8c5df466c8a3e57ca4"},"spotify:track:08zt4rqVjqahvXaWAuEBbP": {"name": "Cold Feet","artists": ["Loud Luxury"],"image_url": "https://i.scdn.co/image/ab67616d00001e0294805d9c6cd1f558d0b0a8ef"},"spotify:track:2plLJpUcYPFrl1sW2pMG63": {"name": "Lights Up","artists": ["Harry Styles"],"image_url": "https://i.scdn.co/image/ab67616d00001e02766ba00b287429d0b13e1e5f"},"spotify:track:0gmgCD6OoJMcoK5af0exA2": {"name": "The Lake (with Wrabel)","artists": ["Galantis","Wrabel"],"image_url": "https://i.scdn.co/image/ab67616d00001e02816cee08cb1ed2c881416a24"},"spotify:track:5QO79kh1waicV47BqGRL3g": {"name": "Save Your Tears","artists": ["The Weeknd"],"image_url": "https://i.scdn.co/image/ab67616d00001e028863bc11d2aa12b54f5aeb36"},"spotify:track:4pBhTGnL5N5KqsyqU58jee": {"name": "I'm not Pretty","artists": ["JESSIA"],"image_url": "https://i.scdn.co/image/ab67616d00001e02fb006ce1d7c3f263c521f01e"},"spotify:track:4wcOBczfEVjEgsF4aKhKbL": {"name": "When You're Home","artists": ["Tyler Shaw"],"image_url": "https://i.scdn.co/image/ab67616d00001e02b16d438b180bdec4ef771c19"},"spotify:track:6Im9k8u9iIzKMrmV7BWtlF": {"name": "34+35","artists": ["Ariana Grande"],"image_url": "https://i.scdn.co/image/ab67616d00001e025ef878a782c987d38d82b605"},"spotify:track:54bFM56PmE4YLRnqpW6Tha": {"name": "Therefore I Am","artists": ["Billie Eilish"],"image_url": "https://i.scdn.co/image/ab67616d00001e02fec5ef9f3133aff71c525acc"},"spotify:track:3USxtqRwSYz57Ewm6wWRMp": {"name": "Heat Waves","artists": ["Glass Animals"],"image_url": "https://i.scdn.co/image/ab67616d00001e02712701c5e263efc8726b1464"},"spotify:track:6IE47jpPeatF2Iay7GZtEc": {"name": "Feel It All Around","artists": ["Washed Out"],"image_url": "https://i.scdn.co/image/ab67616d00001e020f2e0c616a2eb4e00d4c51c3"},"spotify:track:2EH1ZVZx2wPGtQb5V2hNih": {"name": "Good To Sea","artists": ["Pinback"],"image_url": "https://i.scdn.co/image/ab67616d00001e02a3708b5baa5ec5f353324f81"},"spotify:track:24HPkbkXJsIFC4eyg63zgQ": {"name": "Wraith Pinned to the Mist and Other Games","artists": ["of Montreal"],"image_url": "https://i.scdn.co/image/ab67616d00001e0238a6cc0b38036949ed001f9a"},"spotify:track:3qUqucPIPqlSnzq5MacjxQ": {"name": "Goût cerise","artists": ["Ragers"],"image_url": "https://i.scdn.co/image/ab67616d00001e02be8727b798d326c3c164b359"},"spotify:track:6HyUeilH0GYtSWRYBtRm3l": {"name": "À qui j'dois ressembler ?","artists": ["Matthieu Lévesque"],"image_url": "https://i.scdn.co/image/ab67616d00001e0242ce05b61d23754a0204a4d2"},"spotify:track:4ycyOBm9iFoiNVkafhb1WW": {"name": "Nouveaux parrains","artists": ["Sofiane","Soolking"],"image_url": "https://i.scdn.co/image/ab67616d00001e028b6fb0dffb290ef4f27e56e7"},"spotify:track:6iUlUzSGZzKtlCvQ3wCVZD": {"name": "Breakdown (feat. Krayzie Bone & Wish Bone)","artists": ["Mariah Carey","Krayzie Bone","Wishbone"],"image_url": "https://i.scdn.co/image/ab67616d00001e0298ed501c6a838b2244ebaf75"},"spotify:track:58r4JuwHhXLAkttkaUZfLw": {"name": "Got to Be Real","artists": ["Cheryl Lynn"],"image_url": "https://i.scdn.co/image/ab67616d00001e02c0e98d7c48b548f1a3833368"},"spotify:track:4CNzuSQoL5jgCxzYmuMvcz": {"name": "Like It (with 6LACK)","artists": ["Summer Walker","6LACK"],"image_url": "https://i.scdn.co/image/ab67616d00001e02b5ed9187ac7f8aa281a547e3"},"spotify:track:7vxLj7MREliG5i5vSnqSVr": {"name": "Body","artists": ["Summer Walker"],"image_url": "https://i.scdn.co/image/ab67616d00001e02b5ed9187ac7f8aa281a547e3"},"spotify:track:4VSyH8AkIt3kaR5xIPFVVi": {"name": "Where My Girls At","artists": ["702"],"image_url": "https://i.scdn.co/image/ab67616d00001e020100a4e7e46b63b46e03b158"},"spotify:track:2NeyJbL3ROKCjRkAjs77ya": {"name": "Ashes","artists": ["Stellar"],"image_url": "https://i.scdn.co/image/ab67616d00001e023286f23a94f357cdbbb4d718"},"spotify:track:4WPaFfZYr290KKtbc0rEO7": {"name": "Life's Gone Down Low","artists": ["Lijadu Sisters"],"image_url": "https://i.scdn.co/image/ab67616d00001e02236de7aa0dab13d4c9b02eba"},"spotify:track:0izUjTuDrUy2FgQOSRALSU": {"name": "Lockdown","artists": ["Koffee"],"image_url": "https://i.scdn.co/image/ab67616d00001e02cdbcabc170ce557ae4919753"},"spotify:track:4fWK7zJp17fuhDfQ9YnAei": {"name": "Gettaway (feat. Space & Nicole)","artists": ["Missy Elliott","Nicole Wray","Space"],"image_url": "https://i.scdn.co/image/ab67616d00001e02f27571e59cac2e7a4624c9c4"},"spotify:track:1XXimziG1uhM0eDNCZCrUl": {"name": "Up","artists": ["Cardi B"],"image_url": "https://i.scdn.co/image/ab67616d00001e02d619b8baab0619516bb53804"},"spotify:track:3CeCwYWvdfXbZLXFhBrbnf": {"name": "Love Story (Taylor’s Version)","artists": ["Taylor Swift"],"image_url": "https://i.scdn.co/image/ab67616d00001e02877ea8fa223c26f19aaef92d"},"spotify:track:5Kskr9LcNYa0tpt5f0ZEJx": {"name": "Calling My Phone","artists": ["Lil Tjay","6LACK"],"image_url": "https://i.scdn.co/image/ab67616d00001e021b36f91abf80aedb7c88f460"},"spotify:track:7Ei7kZxjEw9d76cEDxoxua": {"name": "What It Feels Like","artists": ["Nipsey Hussle","JAY-Z"],"image_url": "https://i.scdn.co/image/ab67616d00001e02b02a3380a69bd2418a1f68f1"},"spotify:track:7L6G0wpIUiPXuvoo7qhb06": {"name": "oops!","artists": ["Yung Gravy"],"image_url": "https://i.scdn.co/image/ab67616d00001e02c5e844c860c9717785b7aaa2"},"spotify:track:5srKMwXoeyrRnyTnNbpgIW": {"name": "People I Don't Like","artists": ["UPSAHL"],"image_url": "https://i.scdn.co/image/ab67616d00001e02de3975b105bc6d216359ffe6"},"spotify:track:4Iedi94TIaB2GGb1nMB68v": {"name": "On Me","artists": ["Lil Baby"],"image_url": "https://i.scdn.co/image/ab67616d00001e028de3ce24866dcc8ffddbebac"},"spotify:track:26UxwWl9xCb83OynXELJcL": {"name": "Masterpiece","artists": ["DaBaby"],"image_url": "https://i.scdn.co/image/ab67616d00001e027b41da110df7023757e8f8fa"},"spotify:track:2oI1Avedp7KK4Wytv2Dx0O": {"name": "Love is a losing game","artists": ["THEHONESTGUY","Malaika Khadijaa"],"image_url": "https://i.scdn.co/image/ab67616d00001e025f27cac740de78fddbab7bfe"},"spotify:track:3hbi5zXAgQt0Z9V5JSOnCe": {"name": "Everybody Wants To Party","artists": ["Dubdogz","JØRD"],"image_url": "https://i.scdn.co/image/ab67616d00001e0286271a532aa6196b056f369f"}}},"fe71a1ee-6e64-4d4f-8a03-7b091d93c823": {"playlist": []},"null": {"users": {"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {"items": {"spotify:track:0k7wmahjkn389wAZdz19Cv": {"vote": 1607306677700}}}}},"": {"users": {"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {"spotify:track:1FkIrCoa9Lkd8rgZ4VhNP9": {"vote": 1612735281874},"spotify:track:08zt4rqVjqahvXaWAuEBbP": {"vote": 1612750262848},"spotify:track:2plLJpUcYPFrl1sW2pMG63": {"vote": 1612750264080},"spotify:track:0gmgCD6OoJMcoK5af0exA2": {"vote": 1612750266245}}}},"users": {"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {"spotify:track:5QO79kh1waicV47BqGRL3g": {"vote": 1612751939386},"spotify:track:4pBhTGnL5N5KqsyqU58jee": {"vote": 1612751940395},"spotify:track:4wcOBczfEVjEgsF4aKhKbL": {"vote": 1612751941208},"spotify:track:6Im9k8u9iIzKMrmV7BWtlF": {"vote": 1612752104792},"spotify:track:54bFM56PmE4YLRnqpW6Tha": {"vote": 1612752106934},"spotify:track:3USxtqRwSYz57Ewm6wWRMp": {"vote": 1612752110688},"spotify:track:6IE47jpPeatF2Iay7GZtEc": {"vote": 1612752137501},"spotify:track:2EH1ZVZx2wPGtQb5V2hNih": {"vote": 1612752146666},"spotify:track:24HPkbkXJsIFC4eyg63zgQ": {"vote": 1612752149757},"spotify:track:3qUqucPIPqlSnzq5MacjxQ": {"vote": 1612752423687},"spotify:track:6HyUeilH0GYtSWRYBtRm3l": {"vote": 1612752429315},"spotify:track:4ycyOBm9iFoiNVkafhb1WW": {"vote": 1612752433894},"spotify:track:6iUlUzSGZzKtlCvQ3wCVZD": {"vote": 1612753413416},"spotify:track:58r4JuwHhXLAkttkaUZfLw": {"vote": 1612753415873},"spotify:track:4CNzuSQoL5jgCxzYmuMvcz": {"vote": 1612753423847},"items": {"spotify:track:2NeyJbL3ROKCjRkAjs77ya": {"vote": 1612756744096}}}}},"users": {"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {"auth": {"salt": "27e869c626c79ebc8b0d4e59887f3bb66a8031a5e01a0e5faedbdcaa4a7748ab","verifier": "ab2acfc79f5bb1cad3f16276bed6089c626c6e8bb870486a687d5c41b1f0e9534e096e1657c4baee38b805a6ab8c2c38ec38d29048eb8f1f1a28f2a7668f0c2f8983ea6514a00f26f2bc42530946d11f9f304882d0bd4ce240272fe5fc46ada47c22917d630cb2c65f08f2a76438c9d08bfa458d5e1b67ebf43a829027365d1a2d8794b08132796c32d9d9492cc42ca5147eff60304a5b38496b0326f92dfbde167b722834657b45db7a2f51398128fb32753f6deba8a5f474bc58d6d34a1c5a5f75e84860cfba95ad4742017053b464e924bfd10aeceb0169b5c88ddb0bddaee59b198ecdae658a3211eeaf5590c2315459cc7ab78a2114d6e0a162845b386e","client_public_key": "8b119bd7b590235ed80827763e385af341e31e69743cab56500ea1dbe59a2703c38111c3a21751e018de0bf38237a313bc6ad52ac1ce816605e3c11b931822a1c0726715171576450bcd34c3b7a262cc0755fce9afd918e8e60f6e7b3ec8c55a12cd399a5940638113a00735971fbd4a19ed9b0821a6f625d9ac7a3c2d9590e89c4c2692ded40c8b442c477404b54a4aaae2ba0f226d800a1e4a6b5469ac50f5e48e466b82319728bc37839a88ad96c302f4eae80eb7d834cbf0e9b1554cec4b6c9a0749cb843257287f834785ce39d1ef06f71ecd2c19a5143e34f4ccf7d7ece7be83f8129ade15e678c02c9c7bbfafd69c68b26c4787de006c7fccb3f49dd5","server_secret_key": "357ea32f5d17bb517b882d150955d810eccf1dcc0cc3c9a99262897ea15e83ae","session_key": "34b1ae3709db4bcb9948ab8769b130a8a1c5954dc8d551432b0c93316bf13e2d"},"email": "thomas.p.cowan@gmail.com","name": "tpcowan","created_at": 1599514138874,"services": {"spotify": {"expiry": 1627916615,"token": "BQBW0F54C5aapo5-1Lq3B3nF3MUSM7j19a7CRrfKnjeg9xdN-y3Ix3zOP3ggRdarU5WtWZYyOYAAM-owFrEdXcT-cXkHYH_WI3p7Dtm4JRPnVGGLsZeVeA15N7bYzFRvPxFBooHoGehc2ILFlEYUyHUJ60odagrpOGyUsW3oCeZBO_7-3FrvEzgtDxkIrg"}},"drivers": {"spotify": {"token": "BQBW0F54C5aapo5-1Lq3B3nF3MUSM7j19a7CRrfKnjeg9xdN-y3Ix3zOP3ggRdarU5WtWZYyOYAAM-owFrEdXcT-cXkHYH_WI3p7Dtm4JRPnVGGLsZeVeA15N7bYzFRvPxFBooHoGehc2ILFlEYUyHUJ60odagrpOGyUsW3oCeZBO_7-3FrvEzgtDxkIrg"}},"event_id": "62854dc2-7d97-45d3-be03-f0bac69119f8","items": {"1tkg4EHVoqnhR6iFEXb60y": {"artists": ["Pop Smoke"],"image_url": "https://i.scdn.co/image/ab67616d00001e0277ada0863603903f57b34369"},"5u1n1kITHCxxp8twBcZxWy": {"artists": ["Justin Bieber","Chance the Rapper"],"image_url": "https://i.scdn.co/image/ab67616d00001e02572c68f79b356c21202e248c"},"5KCbr5ndeby4y4ggthdiAb": {"artists": ["Shawn Mendes"],"image_url": "https://i.scdn.co/image/ab67616d00001e023d9621bb2904dc57a60a6b36"},"spotify:track:0k7wmahjkn389wAZdz19Cv": {"artists": ["Future","Lil Uzi Vert"],"image_url": "https://i.scdn.co/image/ab67616d00001e0257928e0363878a71692e6a1f"},"spotify:track:5nLNuK7OoJt36gY9gWgnbo": {"artists": ["YSN Fab"],"image_url": "https://i.scdn.co/image/ab67616d00001e02dea56579b16b977c620ac267"},"spotify:track:5vk6nP3fXbz9FoFmsu5coD": {"artists": ["Tizzy Stackz"],"image_url": "https://i.scdn.co/image/ab67616d00001e020b20ee77457a3f4df0dcc63e"},"spotify:track:7ytR5pFWmSjzHJIeQkgog4": {"artists": ["DaBaby","Roddy Ricch"],"image_url": "https://i.scdn.co/image/ab67616d00001e0220e08c8cc23f404d723b5647"},"spotify:track:0sNOPYInjylsM8ZnQozPjt": {"artists": ["Ramin Djawadi"],"image_url": "https://i.scdn.co/image/ab67616d00001e02239a1395e4d595efc28af924"},"spotify:track:4U1c58fpDgbjkb6sVQg26L": {"artists": ["Ramin Djawadi"],"image_url": "https://i.scdn.co/image/ab67616d00001e02239a1395e4d595efc28af924"},"spotify:track:5ryZK3msA04LNcnMaMtm6p": {"artists": ["Ramin Djawadi"],"image_url": "https://i.scdn.co/image/ab67616d00001e02239a1395e4d595efc28af924"},"spotify:track:00deiAYxr1qQx4km9ftnPK": {"artists": ["Ramin Djawadi"],"image_url": "https://i.scdn.co/image/ab67616d00001e0262be9b16adb2e32aed9bda26"},"spotify:track:7lQ8MOhq6IN2w8EYcFNSUk": {"artists": ["Eminem"],"image_url": "https://i.scdn.co/image/ab67616d00001e026ca5c90113b30c3c43ffb8f4"},"spotify:track:4CNzuSQoL5jgCxzYmuMvcz": {"artists": ["Summer Walker","6LACK"],"image_url": "https://i.scdn.co/image/ab67616d00001e02b5ed9187ac7f8aa281a547e3"},"spotify:track:4VSyH8AkIt3kaR5xIPFVVi": {"artists": ["702"],"image_url": "https://i.scdn.co/image/ab67616d00001e020100a4e7e46b63b46e03b158"},"spotify:track:1XXimziG1uhM0eDNCZCrUl": {"artists": ["Cardi B"],"image_url": "https://i.scdn.co/image/ab67616d00001e02d619b8baab0619516bb53804"},"spotify:track:7lPN2DXiMsVn7XUKtOW1CS": {"artists": ["Olivia Rodrigo"],"image_url": "https://i.scdn.co/image/ab67616d00001e0259779689e1d9c15ca2f76b84"},"spotify:track:463CkQjx2Zk1yXoBuierM9": {"artists": ["Dua Lipa","DaBaby"],"image_url": "https://i.scdn.co/image/ab67616d00001e0249caa4fc6f962057ba65576a"},"spotify:track:5QO79kh1waicV47BqGRL3g": {"artists": ["The Weeknd"],"image_url": "https://i.scdn.co/image/ab67616d00001e028863bc11d2aa12b54f5aeb36"},"spotify:track:3YJJjQPAbDT7mGpX3WtQ9A": {"artists": ["SZA"],"image_url": "https://i.scdn.co/image/ab67616d00001e023097b1375ab17ae5bf302a0a"},"spotify:track:6Im9k8u9iIzKMrmV7BWtlF": {"artists": ["Ariana Grande"],"image_url": "https://i.scdn.co/image/ab67616d00001e025ef878a782c987d38d82b605"}}},"f9666a8a-4df1-5d14-9b66-8d08d8d3df58": {"auth": {"salt": "311ac5713fe41aa6ddb172325967c2ec73f93c5d52e4db1603ce5e209eb55a65","verifier": "39722ca76312f45013194c033279fdcac4470bcf8b001d64ad8d75896b372567b787775ea2d49b886e52cbde406f7977c1ac6201143801c00ddf2804262ab7dbe6b78f2d110d5addb5715ce485070f1736bae20e302ce809368b19b5bcb16e4ac785744a71824edc69847729d3d0e68559b640fa842ff1cf77520c83a2f67f84d72e1de89240c82c145c2b88452cbd5f7403998c554a20296fa017e0419698f289c8eb5a79dc9c8bb9184201c7d27a795658fba76a1114ccb2d0f3e71538af86d60ab2dbcf8b1ce0321ff84d6799352e9a0b11d26c2c4152a1c9d38d98988e6d87318f223f33f5c5e970a796186981338569fd97713b7f93825f30cb42311e2f","client_public_key": "365c94c8676042a17ceac7ff56d03be4c8565e086ccbe43eae362d5c2fe4eb9d22cc5d1ee460b96b30c91bb40d50cb6cf3b93dd6319bc2a9dc6a2a403da9d6f44a77f0478f33f6d09fbb8ccb43576889d437f07f189dc407781a6a65ca44738bffe20e51198a92b990143c4f4a3e5811d60c8202a87720176b564b66ed5ae5b58d833709a6c299517387da112f11363f9b429baf593bca9891adec15fa27685ad0040a8dc788c15e07fa1905360827e4f5ad59fbdeede252334d1b3213d5b74297412622f31065d9c9907eadede277e72d3ed4aa6fd5167947d56ac211bed8d6a1913119a7b876d8247b60966e4fb3c842f0a586757866e84c52e89cafa79fb4","server_secret_key": "53a123ed0d4fde5bb266ecf1e1d065ef0fa97c38d78069cfe16fb883f3ec12b3","session_key": "a5ad681112cf8a6c1e057b7f3697110dfbec110921861d0b9566a0238064ce76"},"email": "telcowan@gmail.com","name": "tec","created_at": 1600015188562,"drivers": {"spotify": {"token": "BQBYmk6K2LAFwuhn0zdk_6TgjMlc3N1Zz9-KZBHxvMI695au6QlMpSmPQvxqPcjeznJXSJbJiWT6gOmEy84XyZ0S9UCYNR6MK14Nth4D5tbHeLHushL2NJltos_Si2N_VDDKDV82m1vgj83_KvdaJhPF8_6jJOGk"}},"services": {"spotify": {"expiry": 1600825145,"token": "BQBYmk6K2LAFwuhn0zdk_6TgjMlc3N1Zz9-KZBHxvMI695au6QlMpSmPQvxqPcjeznJXSJbJiWT6gOmEy84XyZ0S9UCYNR6MK14Nth4D5tbHeLHushL2NJltos_Si2N_VDDKDV82m1vgj83_KvdaJhPF8_6jJOGk"}},"event_id": "75c6cfd0-139a-4a33-8826-9c284645f1ae","items": {"54WIS7qug0Gnt65eD9gg8g": {"artists": ["Morgan Delt"]},"4kK14radw0XfwxJDPt9tnP": {"artists": ["Lewis Del Mar"],"image_url": "https://i.scdn.co/image/ab67616d00001e0211555ed45c4377e101d7979d"}}},"59c40275-d764-5f53-907d-c9ccc7b097d7": {"auth": {"salt": "1f3f6715b48937796e62ea7725e02ec3be2edf681298a5d345b61bc75486739d","verifier": "5f2c9aaab598abfd6bc47ffc772061aa67c735d5ddc27d192f6a05b98c994ef151f27b67e9d059c46e05200eebed9cc905ec12a7f38fa30ab7778bebe0796572ef7f7d14eda52a938e65fc577c6e686e98f5f7ca4f2cc9b49747493bc266c6b0e62c90349dc3311bd310e0523d27fb404f3df4ad6fc7d50d2d48d5b9f23eb49456a8cef6642acbef2bf0c914f1d2f2801ae7de6d24d89b9698410dd2cfc91f592befcef89513ef8755ff11473ac8cdcb5ce3d99f970621403fd44f5b1aa177f9f33171de5f57b0a6eb16e24a96302acf7bf6443803b77efb35d8e956885c00836c142eb2fd4f4a3006311f513dc0554530b1691464b0a3c85304c8b96ec26cec","client_public_key": "a5f078c9d7c9e334a9d1c86328af91753e963f2f2633e04f8c6734e4ede048bdd4a4736c3f7309b0c7322fd1a9c23a8610ab34337b35cc773142cef80b0f7dbe16c745355981c9db7f578c263c64bf42c31eb9f34a9df9029790549bb0d0a4baa8e4005a47815ae29b97b902e887ed5d7019a3715ec456059b3465c8bdae10f7d14760e304ee61f94951b13e3589c1078f8f1853257ccc2b4b85da198462316e95cb8cb5d82df321de6955c0e406ae1964c403fd6bded40cbd1af7958a56af52ba0d609d2428788c3e75a85280f552890c608830c55ee58abcac686e0fccbacd46ceb4e227e5600c3091236ceddb5ee58ba16a3784c2b0e8b9c22ee209cf6bbf","server_secret_key": "edfeee05c987ab609e43f5b5ab6e8affd3a34249db192fdf9978cc664c591b99","session_key": "ce6589a816fb281814bca9fe6ae1d6e3d48c865d02673fe48982f6324a130bfc"},"created_at": 1607305069408}},"items": {"spotify:track:7nDYw1nNAW4dAqgmW2W3tq": {"name": "Almost (Sweet Music)"},"spotify:track:6JqYhSdTE4WbQrMXxPH5cD": {"name": "Honeypie"},"spotify:track:3QpkbrYXtlU3LRJu3sTK6V": {"name": "Joy"},"spotify:track:1yTTMcUhL7rtz08Dsgb7Qb": {"name": "The Bones - with Hozier"},"spotify:track:6S1IgeHxxOT9qVWnmsdGxe": {"name": "Treehouse (feat. Shotty Horroh)"},"spotify:track:3VXvKTOQoY0kWvpjU67uq2": {"name": "Daydreaming"},"spotify:track:2P0FH5jSRu8cctdYfTXtje": {"name": "Space and Time"},"spotify:track:45bE4HXI0AwGZXfZtMp8JR": {"name": "you broke me first"},"spotify:track:4wosxLl0mAqhneDzya2MfY": {"name": "Head & Heart (feat. MNEK)"},"spotify:track:2J4P46vCFm1rPkNkp9pZWX": {"name": "Ice Cream (with Selena Gomez)"},"spotify:track:6As34Fmjj7dtReKB51NOVc": {"name": "Super Natural"},"spotify:track:5t9KYe0Fhd5cW6UYT4qP8f": {"name": "Good Vibrations - Remastered"},"spotify:track:17jEoYoOfRD6dvNCMmC9n4": {"name": "City Club"},"spotify:track:1zXpHPdBAUxnOCQqFMFLk3": {"name": "Saltwater"},"spotify:track:2kXeAEpGBN874ZKJPV24fr": {"name": "Acaríñame"},"spotify:track:03ITeFvMvTRpTC92WsQWw5": {"name": "La Cumbia de los Monjes"},"spotify:track:3eCwKRKjGT0EIJe3FKOjIo": {"name": "La Sirenita"},"spotify:track:3HVRywtkhSjhpmkaeaYTgh": {"name": "Life Itself"},"spotify:track:0YedjUOqafibhe8htcD6Gz": {"name": "Bounce (feat. N.O.R.E.) - Radio Version"},"spotify:track:4G3DWijMhNkWZwLcxnDI0H": {"name": "Freeze Me"},"spotify:track:54WIS7qug0Gnt65eD9gg8g": {"name": "Some Sunsick Day"},"spotify:track:4kK14radw0XfwxJDPt9tnP": {"name": "Painting (Masterpiece)"},"spotify:track:1tkg4EHVoqnhR6iFEXb60y": {"name": "What You Know Bout Love"},"spotify:track:5u1n1kITHCxxp8twBcZxWy": {"name": "Holy (feat. Chance The Rapper)"},"spotify:track:5KCbr5ndeby4y4ggthdiAb": {"name": "Wonder"},"spotify:track:0k7wmahjkn389wAZdz19Cv": {"name": "Drankin N Smokin"},"spotify:track:5nLNuK7OoJt36gY9gWgnbo": {"name": "Get Rich or Die Tryin"},"spotify:track:5vk6nP3fXbz9FoFmsu5coD": {"name": "Boujee"},"spotify:track:7ytR5pFWmSjzHJIeQkgog4": {"name": "ROCKSTAR (feat. Roddy Ricch)"},"spotify:track:0sNOPYInjylsM8ZnQozPjt": {"name": "Winter Is Coming - From The \"Game Of Thrones\" Soundtrack"},"spotify:track:4U1c58fpDgbjkb6sVQg26L": {"name": "Jon's Honor - From The \"Game Of Thrones\" Soundtrack"},"spotify:track:5ryZK3msA04LNcnMaMtm6p": {"name": "The Night's Watch - From The \"Game Of Thrones\" Soundtrack"},"spotify:track:00deiAYxr1qQx4km9ftnPK": {"name": "White Walkers"},"spotify:track:7lQ8MOhq6IN2w8EYcFNSUk": {"name": "Without Me"},"spotify:track:4CNzuSQoL5jgCxzYmuMvcz": {"name": "Like It (with 6LACK)"},"spotify:track:4VSyH8AkIt3kaR5xIPFVVi": {"name": "Where My Girls At"},"spotify:track:2h9TDNEXRhcDIV3fsoEVq9": {"name": "What Other People Say"},"spotify:track:1A8990rtwHQ417l3yADe5t": {"name": "GNF (OKOKOK)"},"spotify:track:0tQmgwFKw9069z1BXniOiA": {"name": "Provide (feat. Chris Brown & Mark Morrison)"},"spotify:track:2u8NmvhYX6wiviyxJTOhEi": {"name": "Making A Fire"},"spotify:track:2Y0wPrPQBrGhoLn14xRYCG": {"name": "Come & Go (with Marshmello)"},"spotify:track:5SWnsxjhdcEDc7LJjq9UHk": {"name": "Runnin"},"spotify:track:3SYO8wU4bEgIYt7AeGRIwG": {"name": "Nightwhisper"},"spotify:track:73X9X7kDgsm4YeHpc8prf6": {"name": "Apricots"},"spotify:track:1XXimziG1uhM0eDNCZCrUl": {"name": "Up"},"spotify:track:7lPN2DXiMsVn7XUKtOW1CS": {"name": "drivers license"},"spotify:track:463CkQjx2Zk1yXoBuierM9": {"name": "Levitating (feat. DaBaby)"},"spotify:track:5QO79kh1waicV47BqGRL3g": {"name": "Save Your Tears"},"spotify:track:3YJJjQPAbDT7mGpX3WtQ9A": {"name": "Good Days"},"spotify:track:6Im9k8u9iIzKMrmV7BWtlF": {"name": "34+35"},"USRW29600011": {"name": "Everlong","spotfiy_id": "spotify:track:5UWwZ5lm5PKu6eKsHAGxOk","isrc": "USRW29600011"},"USRW30900002": {"name": "Everlong - Acoustic Version","spotfiy_id": "spotify:track:3QmesrvdbPjwf7i40nht1D","isrc": "USRW30900002"},"USA2P2125949": {"name": "All Eyes On Me - Song Only","spotfiy_id": "spotify:track:47emsK4Cj4dMqctYq18U03","isrc": "USA2P2125949"},"CAUM72100222": {"name": "Therapy","spotfiy_id": "spotify:track:3rsJVGczbI4PRb9YdyoZms","isrc": "CAUM72100222"},"QZES82074435": {"name": "Stunnin' (feat. Harm Franklin)","spotfiy_id": "spotify:track:2D0dj3hVkRQJCp63cxCPEx","isrc": "QZES82074435"},"USUM72021500": {"name": "Therefore I Am","spotfiy_id": "spotify:track:54bFM56PmE4YLRnqpW6Tha","isrc": "USUM72021500"},"GBAHS2100318": {"name": "Bad Habits","spotfiy_id": "spotify:track:6PQ88X9TkUIAUIZJHW2upE","isrc": "GBAHS2100318"},"FRX202125956": {"name": "Tonight","spotfiy_id": "spotify:track:3YoiHH3Myq1fo19pNCTmkW","isrc": "FRX202125956"}},"admin": {"auth": {"salt": "f0c7b75bc552729b75de85c221238275a665b2608204dc2def1bec3657c22946","verifier": "1a058c680c16bb0f09e2a69d92ca79ee4bdd24f28645fe5e5fed5b1ca8edd8d46760b5b469653923d1876695a4246fd603dd6045c81f10ed69d8b515ceb6e1784a4e0041ee7e9f716cc374519d258fbc08c14f9b82db7e2187a3693fa15e32f9bbb4c3a718ee7e8a30e9fec8bcf57ad592922206b84ca0af2480b4e98016600496a8873278745f2ec53306ea9e7dddfa3277faac8aa00f6091337fdaf66cb2e61fc45cca130b43dedeb3db2f3f3d9c1a8cdcdc17ec3c0892c621478afb22f6060e3e0195ea8527a07c6e0bc01b7304baad7a22a3e6162eec6c5adc8f78a1a4704430d7bba6d4f7c0a90960d98703a82d4e8069487cb35fe295fdb6b1b4e4eb51","client_public_key": "725fce92074dd0b06b3dea7858ddc2f12cd9a40f3091019c3ab743222783986d20924babef5904ae5acc976998b8186e846187feb1f98026060966619fba3c414546fdcca95b357eae4446b942fdfb79ad8c540f3e715a6a1162db8bd17edffc9596563f0757b70475b77017310861dfe1b504893fc50fb15e334a366f3a919eebb2c9996f27779f470a70b07f0a0f6db553de0f2d1f49fc778b44e897f13c96eac82b3004f428b8dbb966d01c36c85c50bcc262965f54af4e622d5ec8bbd9b4d2e4bc645d6b9784fa156ed081566f29cdcaad190bdca02d7026cb0d6b6c764ed5881d4a65ac9a5b0ae896beb9c1fb5b89811d90b596206a65ee9fded3889764","server_secret_key": "d5c862d34de2c18b3c162be538cfb59e5957224452b6c9733a37ff27f580480f","session_key": "bc444d93d7efa3010bdbe36e3762af63850147fd2f13df7c31ca3696348b591c"}}},"setup": {"admin": {"token": "BQAICrqhcCVqGJ5C73OVEuvJ-AHiyU9Fsa44yB6aBu1WC1q7Ig0-_QeGLkk7ZCCE_gX6_zU_ncwnL7Ot8JxeQFPBdsspZ3ZlK4hMyEmmOYHgnANFf48BOeuADuFjRogM-dXqpyTMWyQGVW6ShftOllU1ljsmWvBKPkbLuj4Afx4Fs3So15BOC4KQJsdyOsH_","code": "AQBkIHzrU8B3Kd5qmmcXdea-iyF60OaPk0zK6oS40zv4q2-RD2H9SQr1KHiinx8fx_IxO2VNgF1WOZp_ahnq6pPoQ42MgL7i8-ou1jxgMJyzD7Gc64cPc17EK64FWHW8__ugxPTN-Cl_BMp00zBzc80M3PxVb67oPCW3KALp1rNollD2Z2JPQhlikaHr3Zzj8otGbhVSvZfTbUY4eA0Sh6lrwWj8UtXwGlSBszBufNlgjh6--g3PtkrR5g","refresh_token": "AQDxl71M2uyxcKw-1j-oxKYpcnQvsKcRLra8i43aYJTqPgZkh5P4QgOiRZwIuiXKuUG3ebEomKtudJ_NDBw5_NXnwZk1seXqXSOIa5reZyZ3LTW5RG30WEA9KOOgi4DVQ8k","id": "12166793664","expiry": 1607203962},"events": {"75c6cfd0-139a-4a33-8826-9c284645f1ae": {"name": "Calgary Weekly List","playlist": [["spotify:track:5t9KYe0Fhd5cW6UYT4qP8f",2],["spotify:track:6As34Fmjj7dtReKB51NOVc",2],["spotify:track:4kK14radw0XfwxJDPt9tnP",1],["spotify:track:54WIS7qug0Gnt65eD9gg8g",1],["spotify:track:4G3DWijMhNkWZwLcxnDI0H",1],["spotify:track:0YedjUOqafibhe8htcD6Gz",1],["spotify:track:3HVRywtkhSjhpmkaeaYTgh",1],["spotify:track:3eCwKRKjGT0EIJe3FKOjIo",1],["spotify:track:03ITeFvMvTRpTC92WsQWw5",1],["spotify:track:2kXeAEpGBN874ZKJPV24fr",1],["spotify:track:1zXpHPdBAUxnOCQqFMFLk3",1],["spotify:track:5KCbr5ndeby4y4ggthdiAb",1],["spotify:track:5u1n1kITHCxxp8twBcZxWy",1],["spotify:track:1tkg4EHVoqnhR6iFEXb60y",1],["spotify:track:17jEoYoOfRD6dvNCMmC9n4",1],["spotify:track:2P0FH5jSRu8cctdYfTXtje",1],["spotify:track:3VXvKTOQoY0kWvpjU67uq2",1],["spotify:track:6S1IgeHxxOT9qVWnmsdGxe",1],["spotify:track:1yTTMcUhL7rtz08Dsgb7Qb",1],["spotify:track:3QpkbrYXtlU3LRJu3sTK6V",1],["spotify:track:6JqYhSdTE4WbQrMXxPH5cD",1],["spotify:track:7nDYw1nNAW4dAqgmW2W3tq",1]],"spotify_id": "1UxyXZ4YuKdbPHNkFYXpyZ"},"62854dc2-7d97-45d3-be03-f0bac69119f8": {"name": "Toronto Weekly List","playlist": [["spotify:track:2J4P46vCFm1rPkNkp9pZWX",1],["spotify:track:4wosxLl0mAqhneDzya2MfY",1],["spotify:track:45bE4HXI0AwGZXfZtMp8JR",1]],"spotify_id": "3Do3cXEkmdHao31jX0B1oo"},"fe71a1ee-6e64-4d4f-8a03-7b091d93c823": {"name": "Belgium Weekly List","playlist": [],"spotify_id": "6vXyy0FUbzWgrPuNB9xY88"}}}}
{"name": "@djinlist/content","version": "1.0.0","type": "module","license": "UNLICENSED","private": true,"main": "src/index.js","dependencies": {"ws": "^6.0.0","uuid": "^8.0.0"},"exports": {".": "./src/index.js"}}
import srp from "secure-remote-password/server"import crypto from "crypto"class serverAuthorization {createAuthorization(pointer, packet) {// 0const salt = packet.sconst verifier = packet.vconst salt_pointer = pointer.replace('/#', '/auth/salt')const verifier_pointer = pointer.replace('/#', '/auth/verifier')const response = {o: 'a',c: 0,t: pointer.topic,v: true}response.v = !datastore.has(salt_pointer)if (response.v) {datastore.write(salt_pointer, salt)datastore.write(verifier_pointer, verifier)}return response // reject}authorize(pointer, packet) {// 1const salt_pointer = pointer.replace('/#', '/auth/salt')const verifier_pointer = pointer.replace('/#', '/auth/verifier')const client_public_key_pointer = pointer.replace('/#', '/auth/client_public_key')const server_secret_key_pointer = pointer.replace('/#', '/auth/server_secret_key')datastore.write(client_public_key_pointer, packet.k)const response = {o: 'a',c: 2,t: pointer.topic,v: {u: packet.u}}let salt = datastore.read(salt_pointer)let verifier = datastore.read(verifier_pointer)let ephemeralif (salt && verifier) {console.log(`#authorize() ${packet.u} found`)ephemeral = srp.generateEphemeral(verifier)datastore.write(server_secret_key_pointer, ephemeral.secret)response.v.k = ephemeral.publicresponse.v.s = salt} else {console.log(`#authorize() ${packet.u} not found`)salt = crypto.randomBytes(32).toString('hex')ephemeral = crypto.randomBytes(256).toString('hex')response.v.k = ephemeralresponse.v.s = salt}return response}prove(pointer, packet) {// 3const salt_pointer = pointer.replace('/#', '/auth/salt')const verifier_pointer = pointer.replace('/#', '/auth/verifier')const client_public_key_pointer = pointer.replace('/#', '/auth/client_public_key')const server_secret_key_pointer = pointer.replace('/#', '/auth/server_secret_key')const session_key_pointer = pointer.replace('/#', '/auth/session_key')const server_secret_key = datastore.read(server_secret_key_pointer)const client_public_key = datastore.read(client_public_key_pointer)const salt = datastore.read(salt_pointer)const username = packet.uconst verifier = datastore.read(verifier_pointer)const proof = packet.pvar session, responsetry {session = srp.deriveSession(server_secret_key,client_public_key,salt,username,verifier,proof)datastore.write(session_key_pointer, session.key)response = {o: 'a',c: 4,t: pointer.topic,v: {u: packet.u,p: session.proof}}return response} catch (error) {response = {o: 'a',c: 4,t: pointer.topic,v: {u: packet.u,p: false}}return response}}}const authorization = new serverAuthorization()export { authorization }
import { Base } from './base.js'class Admin extends Base {constructor() {super()this.blacklist('state/admin/auth/#')this.blacklist('state/admin/auth/*')}toString() {return 'Channel-Admin'}}const admin = new Admin()export { admin }
import { TopicTree, coppice } from '@djinlist/datastore'import { authorization } from '../authorization.js'class Base {constructor() {Object.defineProperties(this, {_permissions: {value: new TopicTree},_subscribers: {value: new TopicTree}})}toString() {return 'Channel-Base'}whitelist(topic) {console.log(`${this.toString()} #whitelist ${topic}`)this.list(topic, 'whitelist')}blacklist(topic) {console.log(`${this.toString()} #blacklist ${topic}`)this.list(topic, 'blacklist')}dewhitelist(topic) {console.log(`${this.toString()} #dewhitelist ${topic}`)this.delist(topic, 'whitelist')}deblacklist(topic) {console.log(`${this.toString()} #deblacklist ${topic}`)this.delist(topic, 'blacklist')}list(topic, type) {const permissions = this._permissions.getWithDefault(topic, [])permissions._topic = topicpermissions._value.push(type)}initialize(prefix, postfix, callback) {console.log(`${this.toString()} #initialize ${prefix}/+/${postfix}`)const keys = datastore.keys(prefix)const callback_topic = `${prefix}/+${postfix}`_.forEach(keys, key => {const pointer = Pointer.create(`${prefix}/${key}${postfix}`)const value = datastore.read(pointer)callback(callback_topic, pointer, value)})}delist(topic, type) {const permissions = this._permissions.get(topic)if (permissions == null || Object.keys(permissions).length === 0) { return }permissions.delete(type)}syndicate(topic, pointer, value) {const syndicated_path = pointer.steps.slice(0, -1).concat('#').join('/')const private_path = '/' + pointer.steps.slice(0, -1).concat('private').join('/')console.log(`${this.toString()} #syndicate ${syndicated_path} ${value}`)if (!value) {this.whitelist(syndicated_path)datastore.destroy(private_path)} else {this.dewhitelist(syndicated_path)datastore.write(private_path, true)}}publish(topic, pointer, value) {console.log(`${this.toString()} #publish ${topic} @ ${pointer.path}`)const permissions = this._permissions.entries(pointer.topic)let whitelisted = falsefor (var idx in permissions) {const _permitted = permissions[idx][1]if (_permitted.includes('blacklist')) { return }if (_permitted.includes('whitelist')) {whitelisted = true}}const permitted = _.concat(...Object.values(_.fromPairs(permissions)))_.remove(permitted,subscriber => ['whitelist', 'blacklist'].includes(subscriber))const subscribers = this._subscribers.entries(pointer.path.slice(1))if (subscribers.length == 0) { return }let parseif (whitelisted) {parse = ([_topic, subscribed]) => {console.log(_topic, subscribed)_.forEach(subscribed, callbacks => {callbacks.forEach(callback => callback(_topic, pointer, value))})}} else {parse = ([_topic, subscribed]) => {_.forEach(subscribed, (callbacks, subscriber) => {if (permitted.includes(subscriber)) {callbacks.forEach(callback => callback(_topic, pointer, value))}})}}_.forEach(subscribers, parse)}createAuthorization(pointer, packet, subscriber) {const response = authorization.createAuthorization(pointer, packet)subscriber.send(JSON.stringify(response))}authorize(pointer, packet, subscriber) {const response = authorization.authorize(pointer, packet)subscriber.send(JSON.stringify(response))}prove(pointer, packet, subscriber) {const response = authorization.prove(pointer, packet)if (response.v.p === false) {console.log(`#prove FAIL ${pointer.topic} ${subscriber}`)return false}pointer.root = '+'console.log('test', pointer.topic)const permissions = this._permissions.getWithDefault(pointer.topic, [])permissions._value.push(subscriber.toString())console.log(`${this.toString()} #prove SUCCEED ${pointer.topic} ${subscriber}`)subscriber.send(JSON.stringify(response))return true}resume(pointer, subscriber) {console.log(`${this.toString()} #resume ${pointer.topic} ${subscriber}`)const permissions = this._permissions.getWithDefault(pointer.topic, [])permissions._value.push(subscriber.toString())}isAuthorized(pointer, subscriber) {const entries = this._permissions.entries(pointer.dequeue().topic)if (!entries) {console.log(`${this.toString()} #isAuthorized FAIL ${pointer.path}`)return false}let authorized = falsefor (var idx in entries) {const callbacks = entries[idx][1]if (callbacks.includes('blacklist')) {console.log(`${this.toString()} #isAuthorized BLACKLIST ${entries[idx][0]} ${pointer.path}`)return false}if (callbacks.includes('whitelist')) {console.log(`${this.toString()} #isAuthorized WHITELIST ${entries[idx][0]}`)authorized = true} else if (callbacks.includes(subscriber.toString())) {console.log(`${this.toString()} #isAuthorized APPROVED ${entries[idx][0]}`)authorized = true}}console.log(`${this.toString()} #isAuthorized SUCCEED ${pointer.path}`)return authorized}read(pointer, subscriber) {if (!this.isAuthorized(pointer, subscriber)) { return }console.log(`#read ${pointer.path}`)console.dir(datastore.read(pointer.path))const authorizeCoppice = (path, subscriber) => {return _.reduce(datastore.read(pointer.path), (result, value, path) => {if (this.isAuthorized(Pointer.create(path), subscriber)) {result[path] = value}return result}, {})}const data_coppice = {}if (pointer.is_wildcard) {Object.assign(data_coppice,authorizeCoppice(pointer.path, subscriber))} else {const found = datastore.read(pointer.path)if (_.isPlainObject(found)) {coppice(found, pointer.path, data_coppice)} else {data_coppice[path] = found}}return data_coppice}write(pointer, value, subscriber) {if (!this.isAuthorized(pointer, subscriber)) { return }console.log(`${this.toString()} #write`, pointer.path, value)return datastore.write(pointer.path, value)}merge(pointer, value, subscriber) {if (!this.isAuthorized(pointer, subscriber)) { return }console.log(`${this.toString()} #merge`, pointer.path, value)return datastore.merge(pointer.path, value)}delete(pointer, subscriber) {if (!this.isAuthorized(pointer, subscriber)) { return }return datastore.delete(pointer.path)}subscribe(pointer, subscriber, callback) {const subscribers = this._subscribers.getWithDefault(pointer.topic, {})._valueif (_.isArray(subscribers[subscriber])) {subscribers[subscriber].push(callback)} else {subscribers[subscriber] = [callback]}}unsubscribe(subscriber) {console.log(`#unsubscribe ${subscriber}`)const removeSubscriber = ({ _value }) => {if (!_value) returndelete _value[subscriber]}this._subscribers.apply(removeSubscriber)const removePermission = ({ _value }) => {if (!_value) return_.remove(_value, subscriber.toString())}this._permissions.apply(removePermission)}}export { Base }
import { Base } from './base.js'class Event extends Base {constructor() {super()this.whitelist('setup/events/+/name')this.whitelist('setup/events/+/items/#')this.whitelist('setup/events/+/private')this.blacklist('state/events/+/pin')datastore.subscribe('state/events/+/pin', this, this.syndicate.bind(this))datastore.subscribe('state/events/+/#', this, this.publish.bind(this))this.initialize('/state/events', '/pin', this.syndicate.bind(this))}toString() {return 'Channel-Events'}}const events = new Event()export { events }
import { Base } from './base.js'class User extends Base {constructor() {super()datastore.subscribe('state/users/+/#', this, this.publish.bind(this))this.blacklist('state/users/+/auth/#')this.blacklist('state/users/+/auth/*')}toString() {return 'Channel-Users'}}const users = new User()export { users }
import _ from 'lodash'if (!global._) global._ = _import { Datastore, Pointer} from '@djinlist/datastore'if (!global.datastore) global.datastore = new Datastore()if (!global.Pointer) global.Pointer = Pointerimport fs from 'fs'const loadConfig = async () => {// configuration information goes heredatastore.set('/session/node_id', '952ede89-4c91-4df7-bdab-c6dda4257abb')let filePathfs.access('.stash.json', fs.constants.F_OK, (err) => {filePath = `.${err ? 'preconfig' : 'stash'}.json`})const onFileFound = (file, data) => {if (!data) { return false }const _root = JSON.parse(data)if (Object.keys(_root).length === 0) { return false }console.log(`index.js Parsing root found at ${file}`)datastore.set('/state', _root.state)datastore.set('/setup', _root.setup)main()return true}['.stash.json', '.preconfig.json'].find(filePath => {try {return onFileFound(filePath, fs.readFileSync(filePath))} catch (e) {console.log(`Error reading config at ${filePath}`)console.log(e)return false}})}const main = () => {import('./server')}loadConfig()
class Admin {constructor() {console.log(`${this.toString()} #constructor`)datastore.subscribe('q/setup/admin/#', this, this.onSetupQueued.bind(this))}toString() {return 'Model-Admin'}onSetupQueued(topic, pointer, value) {console.log(`${this.toString()} #onSetupQueued: ${pointer.path}, ${JSON.stringify(value)}`)this.accept(pointer, value)}accept(pointer, value, { force = false } = {}) {datastore.set(pointer, null, { silent: true })const dequeued_pointer = pointer.dequeue()datastore.set(dequeued_pointer, value, { force })}destroy() {}}const admin = new Admin()const cleanup = () => {admin.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { admin }
class Event {constructor() {console.log(`${this.toString()} #constructor`)datastore.subscribe('q/state/events/+/#', this, this.onStateQueued.bind(this))datastore.subscribe('q/setup/events/+/#', this, this.onSetupQueued.bind(this))this.ticker = setInterval(this.publishPlaylists.bind(this), 60 * 1000)}toString() {return 'Model-Events'}onStateQueued(topic, pointer, value) {console.log(`${this.toString()} #onStateQueued: ${pointer.path}, ${JSON.stringify(value)}`)switch (pointer.branch_path.length) {case 4:if (pointer.branch_steps[2] == 'users') {this.onUserQueued(topic, pointer, value)}breakdefault:this.accept(pointer, value)break}}onSetupQueued(topic, pointer, value) {console.log(`${this.toString()} #onSetupQueued: ${pointer.path}, ${JSON.stringify(value)}`)this.accept(pointer, value)}onUserQueued(topic, pointer, value) {switch (pointer.leaf) {case 'vote':this.accept(pointer, value)}}publishPlaylist(name, pointer) {const issueAt = datastore.read(pointer.path + '/issue_at')const currentTime = + new Date()console.log(`#publishPlaylist ${name} ${issueAt && (issueAt < currentTime)}`)if (!issueAt // &&// currentTime.getDay() == 5 ||// currentTime.getHours() == 20 ||// currentTime.getMinutes() == 0) {this.issuePlaylist(name, pointer)} else if (issueAt < currentTime) {this.issuePlaylist(name, pointer)// Determine the next time we need to update the playlistconst intervalDays = datastore.read(pointer.path + '/interval')const intervalSeconds = intervalDays * 24 * 60 * 60datastore.write(pointer.path + '/issue_at', issueAt + intervalSeconds)}}publishPlaylists() {const playlists = datastore.read('setup/events/+/name')_.forEach(playlists, (playlist, path) => {const pointer = Pointer.create(path)this.publishPlaylist(playlist, pointer.slice(0, -1))})}tally(pointer) {const items = datastore.read(`/state${pointer.trunk_path}/users/+/items/+/vote`)const length = datastore.read(`/state${pointer.trunk_path}/users/+/items/+/vote`)const tally = {}_.forEach(items, (value, path) => {const id = path.split('/').slice(-2, -1)[0]if (tally[id]) {tally[id] += 1} else {tally[id] = 1}})const sorted_list = _.reverse(_.sortBy(_.entries(tally), entry => entry[1]))const current_list = datastore.read(`/state${pointer.trunk_path}/playlist`)let match = trueif (current_list) {for (var idx in current_list) {if (current_list[idx] != sorted_list[idx]) { match = false }}} else {match = false}if (match) returndatastore.write(`/q/state${pointer.trunk_path}/playlist`, sorted_list)}issuePlaylist(name, pointer) {console.log(`#issuePlaylist ${name} at ${pointer.path}`)const exists = datastore.read(`/setup${pointer.trunk_path}/spotify_id`)if (exists) {this.tally(pointer)} else {datastore.write(`/action${pointer.trunk_path}/start`, +new Date())}}accept(pointer, value, { force = false } = {}) {datastore.set(pointer, null, { silent: true })const dequeued_pointer = pointer.dequeue()datastore.set(dequeued_pointer, value, { force })}destroy() {clearInterval(this.ticker)}}const events = new Event()const cleanup = () => {events.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { events }
class Item {constructor() {console.log(`${this.toString()} #constructor`)datastore.subscribe('q/state/items/+/#', this, this.onStateQueued.bind(this))}toString() {return 'Model-Items'}onStateQueued(topic, pointer, value) {console.log(`${this.toString()} #onStateQueued: ${pointer.path}, ${JSON.stringify(value)}`)this.accept(pointer, value)}accept(pointer, value, { force = false } = {}) {datastore.set(pointer, null, { silent: true })const dequeued_pointer = pointer.dequeue()datastore.set(dequeued_pointer, value, { force })}destroy() {}}const items = new Item()const cleanup = () => {items.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { items }
import https from 'https'class Driver {constructor() {this.chain = datastore.chain()this.chain.link('start', 'action/events/+/start', this.initializePlaylist.bind(this))this.debouncePushPlaylistDetails = _.debounce(this.pushPlaylistDetails, 1000)this.chain.link('name', 'action/events/+/name', this.debouncePushPlaylistDetails.bind(this))this.chain.link('description', 'action/events/+/description', this.debouncePushPlaylistDetails.bind(this))this.chain.link('queue_playlist', 'q/state/events/+/playlist', this.onPlaylistItemsQueued.bind(this))}toString() {return 'Driver-Spotify-Database'}token() {return datastore.read('/setup/admin/token')}initializePlaylist(value, pointer) {console.log(`#initialize_playlist ${pointer.path}`)const eventId = pointer.branch_path.split('/').slice(-1)[0]const name = eventIdconst admin_id = datastore.read("/setup/admin/id")const body = {name,public: true,collaborative: false}const request = https.request({method: 'POST',hostname: 'api.spotify.com',path: `/v1/users/${admin_id}/playlists`,headers: {'Authorization': `Bearer ${this.token()}`,'Content-Type': 'application/json'}},response => {console.log(response.statusCode)if (![200, 201].includes(response.statusCode)) { return }let body = ''response.on('data', chunk => {console.log(body)body += chunk.toString()})response.on('end', () => {this.decodeInitializePlaylist(JSON.parse(body))})})request.on('error', e => {console.error(`problem with request ${e.message}`)})request.write(JSON.stringify(body))request.end()}decodeInitializePlaylist(response) {console.log(`#decodeInitializePlaylist`)console.log(response)const control_id = response['id']const djin_id = response['name']const externalUrl = response['external_urls']['spotify']const branch_path = `/events/${djin_id}`datastore.write(`/setup${branch_path}/spotify_id`, control_id)this.updateInitializedPlaylist(branch_path)}updateInitializedPlaylist(branch_path) {const name = datastore.read(`/setup${branch_path}/name`)const description = datastore.read(`/setup${branch_path}/description`)const options = {name,description: description + `\nCreated with Djinlist (www.djinlist.ca).`}this.changePlaylistDetails(branch_path, options, branch_path => {datastore.write(`/setup${branch_path}/initialized`, true)})}onPushPlaylistDetails(topic, pointer, value) {datastore.write(`/setup${pointer.branch_path}/details_synced`, false)this.debouncePushPlaylistDetails(value, pointer)}pushPlaylistDetails(value, pointer) {const name = datastore.read(`/setup${pointer.branch_path}/name`)const description = datastore.read(`/setup${pointer.branch_path}/description`)const options = {}if (name) { Object.assign(options, { name }) }if (description) { Object.assign(options, { description }) }this.changePlaylistDetails(pointer.branch_path, options, branch_path => {datastore.write(`/setup${branch_path}/details_synced`, true)})}changePlaylistDetails(branch_path, options, callback) {const spotify_id = datastore.read(`/setup${branch_path}/spotify_id`)console.log(`#changePlaylistDetails ${branch_path}, ${spotify_id}`)console.log('options:', options)const request = https.request({method: 'PUT',hostname: 'api.spotify.com',path: `/v1/playlists/${spotify_id}`,headers: {'Authorization': `Bearer ${this.token()}`,'Content-Type': 'application/json'}},response => {response.on('end', () => callback(branch_path))})request.on('error', e => {console.error(`problem with request ${e.message}`)})request.write(JSON.stringify(options))request.end()}onTokenExpiry(topic, pointer, value) {const time = value - (+ new Date())console.log('#onTokenExpiry #{time}')if (this.refreshTokenInterval) {clearInterval(this.refreshTokenInterval)}if (time > 0) {this.refreshTokenInterval = setInterval(this.refreshToken.bind(this), time * 1000)} else {this.refreshToken()}}refreshToken() {const refreshToken = datastore.read(`/setup/admin/refresh_id`)console.log(`#refreshToken ${refresh_id}`)if (!refreshToken) { return }const body = {grant_type: 'refresh_token',refresh_token: refreshToken}request = https.request({method: 'POST',hostname: 'accounts.spotify.com',path: '/api/token',headers: {'Authorization': `Basic ZmUwMDk5M2ZmOTNlNDgyNzgwNGFmMTZlMWRlMzEyZGU6ODQ1NzQzNzhkMDg2NDQwZGI2MDczNmRiN2MxNzc1Mzg=`,'Content-Type': 'application/json'}},response => {if (![200, 201].includes(response.statusCode)) { return }let body = ''response.on('data', chunk => {body += chunk.toString()})response.on('end', () => {this.decodeTokenRefresh(JSON.parse(body))})})request.on('error', e => {console.log(`problem with request ${e.message}`)})request.write(JSON.stringify(body))request.end()}decodeRefreshToken(message) {console.log(`#decodeRefreshToken ${JSON.stringify(message)}`)const new_expiry = (+ new Date()) + message.expires_indatastore.write('/setup/admin/token', message.access_token)datastore.write('/setup/admin/expiry', new_expiry)}onPlaylistItemsQueued(value, pointer) {if (!value) returnconsole.log(`#onPlaylistItemsQueued ${value.length}`)const spotify_id = datastore.read(`/setup${pointer.branch_path}/spotify_id`)// const length = datastore.read(`/setup${pointer.branch_path}/spotify_id`)const request = https.request({method: 'PUT',hostname: 'api.spotify.com',path: `/v1/playlists/${spotify_id}/tracks`,headers: {'Authorization': `Bearer ${this.token()}`,'Content-Type': 'application/json'}})request.on('error', e => {console.log(`problem with request ${e.message}`)})const tracks = _.reduce(value || [], (acc, track) => {return _.concat(acc, [track[0]])}, [])request.write(JSON.stringify({uris: tracks}))request.end()}destroy() {}}const spotify = new Driverconst cleanup = () => {spotify.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { spotify }
import fs from 'fs'class Stash {constructor() {console.log(`${this.toString()} #constructor`)setInterval(this.stash.bind(this), 5000)}toString() {return 'Model-Stash'}stash() {console.log(`${this.toString()} #stash`)const tree = {}if (datastore.has('/state')) Object.assign(tree, { state: datastore.read('/state') })if (datastore.has('/setup')) Object.assign(tree, { setup: datastore.read('/setup') })fs.writeFile('./.stash.json',JSON.stringify(tree, null, 2),(error) => {if (!error) returnconsole.log("Error Writing to Stash")console.log(error)})}destroy() {clearInterval(this.interval)}}const stash = new Stash()const cleanup = () => {stash.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { stash }
class User {constructor() {console.log(`${this.toString()} #constructor`)datastore.subscribe('q/state/users/+/#', this, this.onStateQueued.bind(this))}toString() {return 'Model-Users'}onStateQueued(topic, pointer, value) {console.log(`${this.toString()} #onStateQueued: ${pointer.path}, ${JSON.stringify(value)}`)if (!value) { return }if (pointer.leaf == 'vote') {const event_id = datastore.read(`/state${pointer.trunk_path}/event_id`)console.log('event_id', pointer.branch_path, `/state${pointer.trunk_path}/event_id`, event_id)if (!event_id) { return }datastore.set(`/q/state/events/${event_id}${pointer.branch_path}`, value)datastore.set(pointer, null, { silent: true })console.log(`Transfer vote to /q/state/${pointer.branch_steps.slice(2,4).join('/')}${pointer.trunk_path}/${pointer.branch_steps.slice(-3).join('/')}`)datastore.set(`/q/state/${pointer.branch_steps.slice(2,4).join('/')}${pointer.trunk_path}/${pointer.branch_steps.slice(-3).join('/')}`, value)datastore.set(pointer, null, { silent: true })} else if (pointer.leaf == 'pin') {this.accept(pointer, value)pointer.leaf = 'srp'datastore.set(pointer.path, 'authenticated')} else {this.accept(pointer, value)}}accept(pointer, value, { force = false } = {}) {datastore.set(pointer, null, { silent: true })const dequeued_pointer = pointer.dequeue()console.log(dequeued_pointer.path)datastore.set(dequeued_pointer, value, { force })}destroy() {}}const users = new User()const cleanup = () => {users.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { users }
import WebSocket from 'ws'import { v4 as uuid } from 'uuid'import './models/item'import './models/event'import './models/user'import './models/admin'import './models/stash'import './models/spotify'import { events } from './channels/event.js'import { users } from './channels/user.js'import { items } from './channels/item.js'import { admin } from './channels/admin.js'import { session } from './session.js'let main = () => {/* Models */const channels = {events: events,users: users,items: items,admin: admin}console.log(channels)/* IPC Sockets */const port = 25706const wss = new WebSocket.Server({ port })console.log(`Listenning on ${port}`)wss.on('connection', function connection(ws, request) {const ip = request.socket.remoteAddressws.uuid = uuid()ws.toString = () => ws.uuidconsole.log(`Connection from ${ip}`)datastore.push('/session/connections', ip)const node_id = datastore.read('/session/node_id')const hello = {}hello.o = 'w'hello.v = node_idhello.p = '/session/node_id'ws.send(JSON.stringify(hello))const publish = (topic, pointer, value) => {const message = {}message.o = 'p'message.p = pointer.pathmessage.v = value//console.log(`sending: ${JSON.stringify(message)}`)ws.send(JSON.stringify(message))}ws.on('message', (message) => {console.log(message)message = JSON.parse(message)if (!message.o) {return}let pointer, channelswitch (message.o) {case 'a':console.dir(message)switch (message.c) {case 0:// createAccountpointer = Pointer.create(message.t)channel = channels[pointer.trunk_steps[0]]channel.createAuthorization(pointer, message.v, ws)breakcase 1:// startSessionpointer = Pointer.create(message.t)channel = channels[pointer.trunk_steps[0]]channel.authorize(pointer, message.v, ws)breakcase 4:// verifySessionpointer = Pointer.create(message.t)channel = channels[pointer.trunk_steps[0]]if (channel.prove(pointer, message.v, ws)) {session.addSubscription(message.v.u, pointer)}breakcase 5:// post sessionsession.fetchSession(message.v, ws, channels)breakcase 6:// fetch sessionsession.requestSession(message.v, ws, users)default:break}breakcase 'm':pointer = Pointer.create(message.p)channel = channels[pointer.trunk_steps[0]]channel.merge(pointer, message.v, ws)breakcase 'w':pointer = Pointer.create(message.p)channel = channels[pointer.trunk_steps[0]]channel.write(pointer, message.v, ws)breakcase 'd':pointer = Pointer.create(message.p)channel = channels[pointer.trunk_steps[0]]channel.delete(pointer, ws)breakcase 's':pointer = Pointer.create(message.p)channel = channels[pointer.trunk_steps[0]]channel.subscribe(pointer, ws, publish)if (!message.i) breakcase 'r':pointer = Pointer.create(message.p)channel = channels[pointer.trunk_steps[0]]_.forEach(channel.read(pointer, ws), (value, path) => {const response = { o: 'r' }response.p = pathresponse.v = valuews.send(JSON.stringify(response))})break}})ws.on('close', () => {datastore.pull('/session/connections', ip)_.forEach(channels, channel => {channel.unsubscribe(ws)})console.log('disconnected')})})const cleanup = () => {console.log('\rShutting down server') // eslint-disable-line no-consoleprocess.removeListener('SIGINT', cleanup)process.removeListener('SIGTERM', cleanup)wss.close(() => {datastore.destroy('')process.exit()})}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)}// TODO: Figure out a better way to prevent jsdoctest from executing this.if (!process.env.TEST) {main()}
import crypto from "crypto"class Session {constructor() {this.cleaner = setInterval(this.clean.bind(this), 1000 * 60 * 60)this.key = '2390fbad9157e1f8de9ecb5a494feeb988dfe2ba31c39ca1ba91ba2fa9d30d20'}clean() {const timeouts = datastore.read('/session/connections/+/tokens/+')_.forEach(timeouts, (timeout, path) => {if (+ new Date < timeout) { return }const pointer = Pointer.create(path)pointer.replace('/timeout', '')datastore.destroy(pointer)})}fetchSession(cookie, ws, channels) {console.log(`fetchSession\n\ntoken:\n${cookie}`)const response = {o: 'a',c: 5,v: false}// {// tokens: ,// topics: []// }const [user, token, mac] = cookie.split(':')const verify = crypto.createHmac('sha256',user + ':' + token,this.key).digest('hex')const session = datastore.read(`/session/connections/${user}`)if (!session) returnlet topics = []if (crypto.timingSafeEqual(Buffer.from(verify, 'utf8'), Buffer.from(mac, 'utf8')) &&session && session.tokens) {_.find(session.tokens, (timestamp, stored_token) => {if (crypto.timingSafeEqual(Buffer.from(stored_token, 'utf8'), Buffer.from(token, 'utf8')) &×tamp > + new Date) {response.v = { u: user, s: session.topics || [] }return true}})}_.forEach((session.topics || []), topic => {console.log(topic)const pointer = Pointer.create(topic)const channel = channels[pointer.trunk_steps[0]]channel.resume(pointer, ws)})ws.send(JSON.stringify(response))}requestSession(user, ws, users) {console.log("FFFFFFFFFFFFFFFFF")if (!users.isAuthorized(Pointer.create(`state/users/${user}/#`), ws)) {return}const token = crypto.randomBytes(256).toString('hex')const cookie = user + ':' + tokenconsole.log(`postSession for ${user} \n\ntoken:\n${token}`)const mac = crypto.createHmac('sha256',user + ':' + token,this.key).digest('hex')console.log('L')console.log(mac)datastore.write(`/session/connections/${user}/tokens/${token}`, + new Date + 60 * 60 * 24 * 7 * 1000)const response = {o: 'a',c: 6,v: cookie + ':' + mac}ws.send(JSON.stringify(response))}addSubscription(user, pointer) {console.log(`#addSubscription ${user}, ${pointer.topic}`)const topics = new Set(datastore.read(`/session/connections/${user}/topics`) || [])topics.add(pointer.topic)datastore.write(`/session/connections/${user}/topics`, Array.from(topics))}toString() {return 'Channel-Connections'}}const session = new Session()export { session }
#!/usr/bin/env bashif [[ "$(uname -s)" == "Darwin" ]]; thenecho "Don't run this on your local computer!"exit 1fiecho "[remote] Updating processor"cd djinmusicpnpm install -rcd ..echo "[remote] Installed"
#!/usr/bin/env bash. $BIN_DIR/_lib.shrsync --progress -Pavuz --exclude-from="$WORKING_BIN_DIR/rsync-deploy.ignore" -e "ssh -i $HOME/.ssh/id_rsa_corda_digital_ocean" "${MONO_DIR}/." "tpcowan@processor.djinmusic.ca:/home/tpcowan/djinmusic"rsync --progress -Pavuz -e "ssh -i $HOME/.ssh/id_rsa_corda_digital_ocean" $WORKING_BIN_DIR/deploy-remote.sh "tpcowan@processor.djinmusic.ca:/home/tpcowan/deploy-remote.sh"ssh -F $HOME/.ssh/id_rsa_corda_digital_ocean tpcowan@processor.djinmusic.ca "sh /home/tpcowan/deploy-remote.sh"
{"setup": {"events": {"75c6cfd0-139a-4a33-8826-9c284645f1ae": {"name": "Calgary Weekly List"},"62854dc2-7d97-45d3-be03-f0bac69119f8": {"name": "Toronto Weekly List"},"fe71a1ee-6e64-4d4f-8a03-7b091d93c823": {"name": "Belgium Weekly List"}}}}
{"state": {"users": {"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {"auth": {"salt": "e92d1a36e8056d3efd60d9e27f18f813a087d1b4327ecaad076b1ca198b9cc37","verifier": "94de3d2f9006dc28450425e7bd6bb07c92bebd1722139c9906b4e71ca325bfff5890bcb9319dc703f5e51cf7b6fe0b49660a6b7a6240eafbf2d30c60e8c8ec862b5607580e9690fcf1cae12f903a22c03da5c6a4b86889c1832a7f15354872b6ee5077b1aa956607b0455eaf28e700a2455345fa0d76777a4190efa5e6983043c14bb70aec211a266111ae80b06e68be62d2e7882c7cdf44369a19e9dc0dcd9ffbb116bd63382b47c61e03d2a4a616e3c83c85815b76cd3b8d9c0032bee03a1737f7491b83e08cff566bd2be9811e0c34edc5cb93ebb16d0b314e0bac21e2daedaa9e79a5dc61e02e096f4b9a8c24cc5ca1b2df21b09f5182c0e0f1aa0a4069b","client_public_key": "7aaa63ff31225a38343089e9e2e04e90aa3a3c5eb11f17ff66af9999079c98c16b9eb64d319023c686702e0c990b92d9f69656b28fe78c04a7a0d1e3c08589a5778f10c75a49c28a9045a3aa7d9f47e8807d75b76ee1bf717528f4ebbb5f1904b8438bddfd96a81a9beb7f2f811a732302a787918461154ac6a94dfdead46f0fcb072249b19549031c1b2c087cab122161312c3fa359355f5d0b6b4d63de3a6f5eecdb386bec7c55efc97107c35ad4318171c3a1ea73b6251449834a6605fc8567d58d8f3f8fca7678b1a78b5b8ff1077f43a815ad612089928a355aa94d89b305531354725dc940ad4e6c6133363164085884fec070f74b800f9f620d0dc539","server_secret_key": "7397ff2fcea13bc79b0f1025db63814eb1a71a83da423a20048cee764cedbc29","session_key": "b49f0604bb3a7f59c693c06b1ad63dc75c0272c07ccfd386de329de80deaf01e"},"email": "thomas.p.cowan@gmail.com","name": "tpcowan","created_at": 1629466616852,"services": {"spotify": {"expiry": 1633222298,"token": "BQB-QnozbTKvP0Ki7tdQdsSg_1RdFkXHtiDQV1xVdxWE2bOZXlzAmHZ3FLTqZyJWAA8Jh65269T4chOdgEWLxj_lnScyofxR_lp_M229cyVrx3ZC3maJkXwQIhLy8wvZUbYAgB4MzAlUx5qpLto19mBHiGJA2Qe_kPAGPwHqWhE-1VTDagpih5w-_aCLtw","client": {"expiry": 1633323505,"token": "BQChLRiYH97h389FuIhZ3teQGSTI1N1OLYTHd20Bk56w_Fg7PN3eTCoW5MKvBNGYBBEGToo5Qor1byQsblBITsljwBwzo-0dDS6XtkrCLWLizIvol6U3UVHnC_FfN2h9dWOrmQlokbmHrn2f2obVs2GbXLhPCf6FQS3idpHVW02aCO4hcCex_Q87FCJZ4g"}}},"event_id": "fe71a1ee-6e64-4d4f-8a03-7b091d93c823","drivers": {"spotify": {"token": "BQB-QnozbTKvP0Ki7tdQdsSg_1RdFkXHtiDQV1xVdxWE2bOZXlzAmHZ3FLTqZyJWAA8Jh65269T4chOdgEWLxj_lnScyofxR_lp_M229cyVrx3ZC3maJkXwQIhLy8wvZUbYAgB4MzAlUx5qpLto19mBHiGJA2Qe_kPAGPwHqWhE-1VTDagpih5w-_aCLtw","client": {"token": "BQB-QnozbTKvP0Ki7tdQdsSg_1RdFkXHtiDQV1xVdxWE2bOZXlzAmHZ3FLTqZyJWAA8Jh65269T4chOdgEWLxj_lnScyofxR_lp_M229cyVrx3ZC3maJkXwQIhLy8wvZUbYAgB4MzAlUx5qpLto19mBHiGJA2Qe_kPAGPwHqWhE-1VTDagpih5w-_aCLtw"}}}},"59c40275-d764-5f53-907d-c9ccc7b097d7": {"auth": {"client_public_key": "77c45a87ee811e78ac8bfa91dc1e309af7ca653d446afc3747e08037201f629b80edbb9453aae7c3f21ad3c5e2d55e400c191c1e682854af0010a7446911c31dfc8febbd1d91ad7bdba6530047d6c0103c7ffad0740772feae5c74902804f6028b0a5cff3e9a92c672598d780e6e439966a4c57649920abfc58acf0aff21dee13a095b3936eada69ee3b5bfea35482249b953174c5d792c4f12c3a877f6eb25ad0f01f8648b06de9e2466b734b1fe637640e1eb367a845949808b16c1c3fd4ffedd4fd6e44a493c5f7280c01658884370e0792b29b546b80e8ecf317d563cee3051fb51813fc91ce86a212420235170f51b53edfb494902cb37e97844a08e5ae"}}},"items": {"USRW29600011": {"name": "Everlong","spotfiy_id": "spotify:track:5UWwZ5lm5PKu6eKsHAGxOk","isrc": "USRW29600011"}},"events": {"fe71a1ee-6e64-4d4f-8a03-7b091d93c823": {"users": {"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {"items": {"USRW29600011": 1632019111472}}}}}},"setup": {"events": {"75c6cfd0-139a-4a33-8826-9c284645f1ae": {"name": "Calgary Weekly List"},"62854dc2-7d97-45d3-be03-f0bac69119f8": {"name": "Toronto Weekly List"},"fe71a1ee-6e64-4d4f-8a03-7b091d93c823": {"name": "Belgium Weekly List"}}}}
{"name": "@djinlist/content","version": "1.0.0","type": "module","license": "UNLICENSED","private": true,"main": "src/index.js","dependencies": {"ws": "^6.0.0","uuid": "^8.0.0"},"exports": {".": "./src/index.js"}}
import srp from "secure-remote-password/server"import crypto from "crypto"class serverAuthorization {createAuthorization(topic, packet) {// 0const salt = packet.sconst verifier = packet.vconst salt_topic = topic.replace('/#', '/auth/salt')const verifier_topic = topic.replace('/#', '/auth/verifier')const response = {o: 'a',c: 0,t: topic.pattern,v: true}console.log({salt_topic})response.v = !datastore.read('/' + salt_topic.pattern)if (response.v) {datastore.write('/' + salt_topic.pattern, salt)datastore.write('/' + verifier_topic.pattern, verifier)}return response // reject}authorize(topic, packet) {// 1const salt_topic = topic.replace('/#', '/auth/salt')const verifier_topic = topic.replace('/#', '/auth/verifier')const client_public_key_topic = topic.replace('/#', '/auth/client_public_key')const server_secret_key_topic = topic.replace('/#', '/auth/server_secret_key')console.log({topic, salt_topic, verifier_topic, client_public_key_topic, server_secret_key_topic})datastore.write('/' + client_public_key_topic.pattern, packet.k)const response = {o: 'a',c: 2,t: topic.pattern,v: {u: packet.u}}let salt = datastore.read('/' + salt_topic.pattern)let verifier = datastore.read('/' + verifier_topic.pattern)let ephemeralif (salt && verifier) {console.log(`#authorize() ${packet.u} found`)ephemeral = srp.generateEphemeral(verifier)datastore.write('/' + server_secret_key_topic.pattern, ephemeral.secret)response.v.k = ephemeral.publicresponse.v.s = salt} else {console.log(`#authorize() ${packet.u} not found`)salt = crypto.randomBytes(32).toString('hex')ephemeral = crypto.randomBytes(256).toString('hex')response.v.k = ephemeralresponse.v.s = salt}return response}prove(topic, packet) {// 3const salt_topic = topic.replace('/#', '/auth/salt')const verifier_topic = topic.replace('/#', '/auth/verifier')const client_public_key_topic = topic.replace('/#', '/auth/client_public_key')const server_secret_key_topic = topic.replace('/#', '/auth/server_secret_key')const session_key_topic = topic.replace('/#', '/auth/session_key')const server_secret_key = datastore.read('/' + server_secret_key_topic.pattern)const client_public_key = datastore.read('/' + client_public_key_topic.pattern)const salt = datastore.read('/' + salt_topic.pattern)const username = packet.uconst verifier = datastore.read('/' + verifier_topic.pattern)const proof = packet.pvar session, responsetry {session = srp.deriveSession(server_secret_key,client_public_key,salt,username,verifier,proof)datastore.write('/' + session_key_topic.pattern, session.key)response = {o: 'a',c: 4,t: topic.pattern,v: {u: packet.u,p: session.proof}}return response} catch (error) {response = {o: 'a',c: 4,t: topic.pattern,v: {u: packet.u,p: false}}return response}}}const authorization = new serverAuthorization()export { authorization }
import { Base } from './base.js'class Admin extends Base {constructor() {super()this.blacklist(new Topic('state/admin/auth/#'))this.blacklist(new Topic('state/admin/auth/*'))}toString() {return 'Channel-Admin'}}const admin = new Admin()export { admin }
import { coppice } from '@controlenvy/datastore'import { TopicTree } from './topic_tree.js'import { authorization } from '../authorization.js'class Base {constructor() {Object.defineProperties(this, {_permissions: {value: new TopicTree},_subscribers: {value: new TopicTree}})}toString() {return 'Channel-Base'}whitelist(topic) {console.log(`${this.toString()} #whitelist ${topic.toString()}`)this.list(topic, 'whitelist')}blacklist(topic) {console.log(`${this.toString()} #blacklist ${topic.toString()}`)this.list(topic, 'blacklist')}dewhitelist(topic) {console.log(`${this.toString()} #dewhitelist ${topic.toString()}`)this.delist(topic, 'whitelist')}deblacklist(topic) {console.log(`${this.toString()} #deblacklist ${topic.toString()}`)this.delist(topic, 'blacklist')}list(topic, type) {const permissions = this._permissions.getWithDefault(topic.pattern, [])permissions._topic = topicpermissions._value.push(type)}initialize(prefix, postfix, callback) {console.log(`${this.toString()} #initialize ${prefix}/+${postfix}`)const keys = datastore.get(prefix)?.keys() || []const callback_topic = `${prefix}/+${postfix}`_.forEach(keys, key => {const pointer = new Pointer(`${prefix}/${key}${postfix}`)const value = datastore.read(pointer)callback(callback_topic, pointer, value)})}delist(topic, type) {const permissions = this._permissions.get(topic.pattern)if (permissions == null || Object.keys(permissions).length === 0) { return }permissions.delete(type)}syndicate(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueconst syndicated_path = pointer.steps.slice(0, -1).concat('#').join('/')const private_path = '/' + pointer.steps.slice(0, -1).concat('private').join('/')console.log(`${this.toString()} #syndicate ${syndicated_path} ${value}`)if (!value) {this.whitelist(syndicated_path)datastore.delete(private_path)} else {this.dewhitelist(syndicated_path)datastore.write(private_path, true)}}publish(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconsole.log(`${this.toString()} #publish ${topic} @ ${pointer.path}`)const permissions = this._permissions.entries(pointer.path.slice(1))let whitelisted = falsefor (var idx in permissions) {const _permitted = permissions[idx][1]if (_permitted.includes('blacklist')) { return }if (_permitted.includes('whitelist')) {whitelisted = true}}const permitted = _.concat(...Object.values(_.fromPairs(permissions)))console.log({permitted})_.remove(permitted,subscriber => ['whitelist', 'blacklist'].includes(subscriber))const subscribers = this._subscribers.entries(pointer.path.slice(1))if (subscribers.length == 0) { return }let parseif (whitelisted) {parse = ([_topic, subscribed]) => {_.forEach(subscribed, callbacks => {callbacks.forEach(callback => callback(_topic, pointer))})}} else {parse = ([_topic, subscribed]) => {_.forEach(subscribed, (callbacks, subscriber) => {if (permitted.includes(subscriber)) {callbacks.forEach(callback => callback(_topic, pointer))}})}}_.forEach(subscribers, parse)}createAuthorization(pointer, packet, subscriber) {const response = authorization.createAuthorization(pointer, packet)subscriber.send(JSON.stringify(response))}authorize(topic, packet, subscriber) {const response = authorization.authorize(topic, packet)subscriber.send(JSON.stringify(response))}prove(topic, packet, subscriber) {const response = authorization.prove(topic, packet)if (response.v.p === false) {console.log(`#prove FAIL ${topic.pattern} ${subscriber}`)return false}topic = topic.changeRoot('+')const permissions = this._permissions.getWithDefault(topic.pattern, [])permissions._value.push(subscriber.toString())console.log(`${this.toString()} #prove SUCCEED ${topic.pattern} ${subscriber}`)subscriber.send(JSON.stringify(response))return true}resume(topic, subscriber) {console.log(`${this.toString()} #topic ${topic.pattern} ${subscriber}`)const permissions = this._permissions.getWithDefault(topic.pattern, [])permissions._value.push(subscriber.toString())}isAuthorized(topic, subscriber) {const entries = this._permissions.entries(topic.dequeue().pattern)console.log({p: this._permissions._root.state, kk: topic.dequeue().pattern, entries})if (!entries) {console.log(`${this.toString()} #isAuthorized FAIL ${topic.pattern}`)return false}let authorized = falsefor (var idx in entries) {const callbacks = entries[idx][1]if (callbacks.includes('blacklist')) {console.log(`${this.toString()} #isAuthorized BLACKLIST ${entries[idx][0]} ${topic.pattern}`)return false}if (callbacks.includes('whitelist')) {console.log(`${this.toString()} #isAuthorized WHITELIST ${entries[idx][0]}`)authorized = true} else if (callbacks.includes(subscriber.toString())) {console.log(`${this.toString()} #isAuthorized APPROVED ${entries[idx][0]}`)authorized = true}}console.log(`${this.toString()} #isAuthorized ${authorized ? 'SUCCEED' : 'FAIL'} ${topic.pattern}`)return authorized}read(pointer, subscriber) {const data_coppice = {}if (pointer.isWildcard()) {console.log(`${this.toString()} #search ${pointer.path}`)return _.reduce(datastore.search(pointer.path), (result, value, path) => {if (this.isAuthorized(new Topic(path.slice(1)), subscriber)) {result[path] = value}return result}, {})} else {console.log(`${this.toString()} #read ${pointer.path}`)if (!this.isAuthorized(new Topic(pointer.steps), subscriber)) { return }const found = datastore.read(pointer.path)if (_.isPlainObject(found)) {coppice(found, pointer.path, data_coppice)} else {data_coppice[pointer.path] = found}}return data_coppice}write(pointer, value, subscriber) {if (!this.isAuthorized(new Topic(pointer.steps), subscriber)) { return }console.log(`${this.toString()} #write`, pointer.path, value)return datastore.write(pointer.path, value)}merge(pointer, value, subscriber) {if (!this.isAuthorized(new Topic(pointer.steps), subscriber)) { return }console.log(`${this.toString()} #merge`, pointer.path, value)return datastore.merge(pointer.path, value)}delete(pointer, subscriber) {if (!this.isAuthorized(new Topic(pointer.steps), subscriber)) { return }return datastore.delete(pointer.path)}subscribe(topic, subscriber, callback) {const subscribers = this._subscribers.getWithDefault(topic.pattern, {})._valueif (_.isArray(subscribers[subscriber])) {subscribers[subscriber].push(callback)} else {subscribers[subscriber] = [callback]}}unsubscribe(subscriber) {console.log(`#unsubscribe ${subscriber}`)const removeSubscriber = ({ _value }) => {if (!_value) returndelete _value[subscriber]}this._subscribers.apply(removeSubscriber)const removePermission = ({ _value }) => {if (!_value) return_.remove(_value, subscriber.toString())}this._permissions.apply(removePermission)}}export { Base }
import { Base } from './base.js'class Event extends Base {constructor() {super()this.whitelist(new Topic('setup/events/+/name'))this.whitelist(new Topic('setup/events/+/items/#'))this.whitelist(new Topic('setup/events/+/private'))this.blacklist(new Topic('state/events/+/pin'))datastore.subscribe('setup/events/+/pin', this.syndicate.bind(this))datastore.subscribe('setup/events/+/#', this.publish.bind(this))datastore.subscribe('state/events/+/#', this.publish.bind(this))this.initialize('/setup/events', '/pin', this.syndicate.bind(this))}toString() {return 'Channel-Events'}}const events = new Event()export { events }
import { Base } from './base.js'class Item extends Base {constructor() {super()this.whitelist(new Topic('state/items/+/#'))}toString() {return 'Channel-Items'}}const items = new Item()export { items }
class TopicTree {constructor() {Object.defineProperties(this, {_root: {value: this.createTreeNode()}})}createTreeNode() {const node = Object.create(null)Object.defineProperties(node, {_value: {writable: true}})return node}// expensive, call rarelyall(func = null, output = [], node = this._root) {if (node._topic && node._value && (!func || func(node))) {output.push([node._topic, node._value])}_.forEach(node, (child, key) => {if (!['_value', '_topic'].includes(key)) {this.all(func, output, child)}})return output}apply(func, node = this._root) {func(node)return _.forEach(node, (child, key) => {if (!['_value', '_topic'].includes(key)) {this.apply(func, child)}})}get(topic) {const steps = topic.split('/')let left = this._rootfor (const step of steps) {left = left[step]if (left == null) {left = this.createTreeNode()break}}return left}getWithDefault(topic, value) {const steps = topic.split('/')let node = this._rootfor (const step of steps) {if (node[step] == null) {node[step] = this.createTreeNode()}node = node[step]}if (node._value == null) {node._topic = topicnode._value = value}return node}add(topic, value) {const node = this.getWithDefault(topic)node._topic = topicnode._value = value}values(topic) {const steps = topic.split('/')return this._values(this._root, steps, 0, []).reverse()}_values(node, steps, pivot, values) {if (steps.length == pivot) {if (node._value != null) {values.push(node._value)}return values}const step = steps[pivot]if (node['#'] != null) {values.push(node['#']._value)}if (node['+'] != null) {values = this._values(node['+'], steps, pivot + 1, values)}if (node[step] != null) {values = this._values(node[step], steps, pivot + 1, values)}return values}entries(topic) {const steps = topic.split('/')return this._entries(this._root, steps, 0, []).reverse()}_entries(node, steps, pivot, entries) {if (steps.length == pivot) {if (node._value != null) {entries.push([node._topic, node._value])}if (node['*'] != null) {entries.push([node['*']._topic, node['*']._value])}return entries}const step = steps[pivot]if (node['#'] != null) {entries.push([node['#']._topic, node['#']._value])}if (node['+'] != null) {entries = this._entries(node['+'], steps, pivot + 1, entries)}if (node[step] != null) {entries = this._entries(node[step], steps, pivot + 1, entries)}return entries}}export { TopicTree }
import { Base } from './base.js'class User extends Base {constructor() {super()datastore.subscribe('state/users/+/#', this.publish.bind(this))this.blacklist(new Topic('state/users/+/auth/#'))this.blacklist(new Topic('state/users/+/auth/*'))}toString() {return 'Channel-Users'}}const users = new User()export { users }
import _ from 'lodash'if (!global._) global._ = _import { Datastore, Pointer, Topic } from '@controlenvy/datastore'if (!global.datastore) global.datastore = new Datastore()if (!global.Pointer) global.Pointer = Pointerif (!global.Topic) global.Topic = Topicimport fs from 'fs'const loadConfig = async () => {// configuration information goes heredatastore.set('/session/node_id', '952ede89-4c91-4df7-bdab-c6dda4257abb')let filePathfs.access('.stash.json', fs.constants.F_OK, (err) => {filePath = `.${err ? 'preconfig' : 'stash'}.json`})const onFileFound = (file, data) => {if (!data) { return false }const _root = JSON.parse(data)if (Object.keys(_root).length === 0) { return false }console.log(`index.js Parsing root found at ${file}`)datastore.merge('/state', _root.state)datastore.merge('/setup', _root.setup)main()return true}['.stash.json', '.preconfig.json'].find(filePath => {try {return onFileFound(filePath, fs.readFileSync(filePath))} catch (e) {console.log(`Error reading config at ${filePath}`)console.log(e)return false}})}const main = () => {import('./server')}loadConfig()
class Admin {constructor() {console.log(`${this.toString()} #constructor`)datastore.subscribe('q/setup/admin/#', this.onSetupQueued.bind(this))}toString() {return 'Model-Admin'}onSetupQueued(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueconsole.log(`${this.toString()} #onSetupQueued: ${pointer.path}, ${JSON.stringify(value)}`)this.accept(pointer, value)}accept(pointer, value, { force = false } = {}) {datastore.set(pointer, null, { silent: true })const dequeued_pointer = pointer.dequeue()datastore.set(dequeued_pointer.path, value, { force })}destroy() {}}const admin = new Admin()const cleanup = () => {admin.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { admin }
class Event {constructor() {console.log(`${this.toString()} #constructor`)datastore.subscribe('q/state/events/+/#', this.onStateQueued.bind(this))datastore.subscribe('q/setup/events/+/#', this.onSetupQueued.bind(this))this.ticker = setInterval(this.publishPlaylists.bind(this), 60 * 1000)}toString() {return 'Model-Events'}onStateQueued(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueconsole.log(`${this.toString()} #onStateQueued: ${pointer.path}, ${JSON.stringify(value)}`)switch (pointer.branch_path.length) {case 4:if (pointer.branch_steps[2] == 'users') {this.onUserQueued(topic, pointer, value)}breakdefault:this.accept(pointer, value)break}}onSetupQueued(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueconsole.log(`${this.toString()} #onSetupQueued: ${pointer.path}, ${JSON.stringify(value)}`)this.accept(pointer, value)}onUserQueued(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueswitch (pointer.leaf) {case 'vote':this.accept(pointer, value)}}publishPlaylist(name, pointer) {const issueAt = datastore.read(pointer.path + '/issue_at')const currentTime = + new Date()console.log(`#publishPlaylist ${name} ${issueAt && (issueAt < currentTime)}`)if (!issueAt // &&// currentTime.getDay() == 5 ||// currentTime.getHours() == 20 ||// currentTime.getMinutes() == 0) {this.issuePlaylist(name, pointer)} else if (issueAt < currentTime) {this.issuePlaylist(name, pointer)// Determine the next time we need to update the playlistconst intervalDays = datastore.read(pointer.path + '/interval')const intervalSeconds = intervalDays * 24 * 60 * 60datastore.write(pointer.path + '/issue_at', issueAt + intervalSeconds)}}publishPlaylists() {const playlists = datastore.read('/setup/events/+/name')console.log(`#publishPlaylists`, playlists)_.forEach(playlists, (playlist, path) => {const pointer = new Pointer(path)this.publishPlaylist(playlist, pointer.slice(0, -1))})}tally(pointer) {const items = datastore.read(`/state${pointer.trunk_path}/users/+/items/+/vote`)const length = datastore.read(`/state${pointer.trunk_path}/users/+/items/+/vote`)const tally = {}_.forEach(items, (value, path) => {const id = path.split('/').slice(-2, -1)[0]if (tally[id]) {tally[id] += 1} else {tally[id] = 1}})const sorted_list = _.reverse(_.sortBy(_.entries(tally), entry => entry[1]))const current_list = datastore.read(`/state${pointer.trunk_path}/playlist`)let match = trueif (current_list) {for (var idx in current_list) {if (current_list[idx] != sorted_list[idx]) { match = false }}} else {match = false}if (match) returndatastore.write(`/q/state${pointer.trunk_path}/playlist`, sorted_list)}issuePlaylist(name, pointer) {console.log(`#issuePlaylist ${name} at ${pointer.path}`)const exists = datastore.read(`/setup${pointer.trunk_path}/spotify_id`)if (exists) {this.tally(pointer)} else {datastore.write(`/action${pointer.trunk_path}/start`, +new Date())}}accept(pointer, value, { force = false } = {}) {datastore.set(pointer, null, { silent: true })const dequeued_pointer = pointer.dequeue()datastore.set(dequeued_pointer.path, value, { force })}destroy() {console.log(`${this.toString()} #destroy`)clearInterval(this.ticker)}}const events = new Event()const cleanup = () => {events.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { events }
class Item {constructor() {console.log(`${this.toString()} #constructor`)datastore.subscribe('q/state/items/+/#', this.onStateQueued.bind(this))}toString() {return 'Model-Items'}onStateQueued(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueconsole.log(`${this.toString()} #onStateQueued: ${pointer.path}, ${JSON.stringify(value)}`)this.accept(pointer, value)}accept(pointer, value, { force = false } = {}) {datastore.set(pointer, null, { silent: true })const dequeued_pointer = pointer.dequeue()datastore.set(dequeued_pointer.path, value, { force })}destroy() {}}const items = new Item()const cleanup = () => {items.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { items }
import https from 'https'class Driver {constructor() {datastore.subscribe('action/events/+/start', this.initializePlaylist.bind(this))this.debouncePushPlaylistDetails = _.debounce(this.pushPlaylistDetails, 1000)datastore.subscribe('action/events/+/name', this.debouncePushPlaylistDetails.bind(this))datastore.subscribe('action/events/+/description', this.debouncePushPlaylistDetails.bind(this))datastore.subscribe('q/state/events/+/playlist', this.onPlaylistItemsQueued.bind(this))}toString() {return 'Driver-Spotify-Database'}token() {return datastore.read('/setup/admin/token')}initializePlaylist(topic, event) {const { pointer } = eventconsole.log(`#initialize_playlist ${pointer.path}`)const eventId = pointer.branch.slice(-1)[0]const name = eventIdconst admin_id = datastore.read("/setup/admin/id")const body = {name,public: true,collaborative: false}const request = https.request({method: 'POST',hostname: 'api.spotify.com',path: `/v1/users/${admin_id}/playlists`,headers: {'Authorization': `Bearer ${this.token()}`,'Content-Type': 'application/json'}},response => {console.log(response.statusCode)if (![200, 201].includes(response.statusCode)) { return }let body = ''response.on('data', chunk => {console.log(body)body += chunk.toString()})response.on('end', () => {this.decodeInitializePlaylist(JSON.parse(body))})})request.on('error', e => {console.error(`problem with request ${e.message}`)})request.write(JSON.stringify(body))request.end()}decodeInitializePlaylist(response) {console.log(`#decodeInitializePlaylist`)console.log(response)const control_id = response['id']const djin_id = response['name']const externalUrl = response['external_urls']['spotify']const branch_path = `/events/${djin_id}`datastore.write(`/setup${branch_path}/spotify_id`, control_id)this.updateInitializedPlaylist(branch_path)}updateInitializedPlaylist(branch_path) {const name = datastore.read(`/setup${branch_path}/name`)const description = datastore.read(`/setup${branch_path}/description`)const options = {name,description: description + `\nCreated with Djinlist (www.djinlist.ca).`}this.changePlaylistDetails(branch_path, options, branch_path => {datastore.write(`/setup${branch_path}/initialized`, true)})}onPushPlaylistDetails(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valuedatastore.write(`/setup${pointer.branch_path}/details_synced`, false)this.debouncePushPlaylistDetails(value, pointer)}pushPlaylistDetails(value, pointer) {if (event.type !== '=') returnconst name = datastore.read(`/setup${pointer.branch_path}/name`)const description = datastore.read(`/setup${pointer.branch_path}/description`)const options = {}if (name) { Object.assign(options, { name }) }if (description) { Object.assign(options, { description }) }this.changePlaylistDetails(pointer.branch_path, options, branch_path => {datastore.write(`/setup${branch_path}/details_synced`, true)})}changePlaylistDetails(branch_path, options, callback) {const spotify_id = datastore.read(`/setup${branch_path}/spotify_id`)console.log(`#changePlaylistDetails ${branch_path}, ${spotify_id}`)console.log('options:', options)const request = https.request({method: 'PUT',hostname: 'api.spotify.com',path: `/v1/playlists/${spotify_id}`,headers: {'Authorization': `Bearer ${this.token()}`,'Content-Type': 'application/json'}},response => {response.on('end', () => callback(branch_path))})request.on('error', e => {console.error(`problem with request ${e.message}`)})request.write(JSON.stringify(options))request.end()}onTokenExpiry(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueconst time = value - (+ new Date())console.log('#onTokenExpiry #{time}')if (this.refreshTokenInterval) {clearInterval(this.refreshTokenInterval)}if (time > 0) {this.refreshTokenInterval = setInterval(this.refreshToken.bind(this), time * 1000)} else {this.refreshToken()}}refreshToken() {const refreshToken = datastore.read(`/setup/admin/refresh_id`)console.log(`#refreshToken ${refresh_id}`)if (!refreshToken) { return }const body = {grant_type: 'refresh_token',refresh_token: refreshToken}request = https.request({method: 'POST',hostname: 'accounts.spotify.com',path: '/api/token',headers: {'Authorization': `Basic ZmUwMDk5M2ZmOTNlNDgyNzgwNGFmMTZlMWRlMzEyZGU6ODQ1NzQzNzhkMDg2NDQwZGI2MDczNmRiN2MxNzc1Mzg=`,'Content-Type': 'application/json'}},response => {if (![200, 201].includes(response.statusCode)) { return }let body = ''response.on('data', chunk => {body += chunk.toString()})response.on('end', () => {this.decodeTokenRefresh(JSON.parse(body))})})request.on('error', e => {console.log(`problem with request ${e.message}`)})request.write(JSON.stringify(body))request.end()}decodeRefreshToken(message) {console.log(`#decodeRefreshToken ${JSON.stringify(message)}`)const new_expiry = (+ new Date()) + message.expires_indatastore.write('/setup/admin/token', message.access_token)datastore.write('/setup/admin/expiry', new_expiry)}onPlaylistItemsQueued(value, pointer) {if (!value) returnconsole.log(`#onPlaylistItemsQueued ${value.length}`)const spotify_id = datastore.read(`/setup${pointer.branch_path}/spotify_id`)// const length = datastore.read(`/setup${pointer.branch_path}/spotify_id`)const request = https.request({method: 'PUT',hostname: 'api.spotify.com',path: `/v1/playlists/${spotify_id}/tracks`,headers: {'Authorization': `Bearer ${this.token()}`,'Content-Type': 'application/json'}})request.on('error', e => {console.log(`problem with request ${e.message}`)})const tracks = _.reduce(value || [], (acc, track) => {return _.concat(acc, [track[0]])}, [])request.write(JSON.stringify({uris: tracks}))request.end()}destroy() {}}const spotify = new Driverconst cleanup = () => {spotify.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { spotify }
import fs from 'fs'class Stash {constructor() {console.log(`${this.toString()} #constructor`)setInterval(this.stash.bind(this), 5000)}toString() {return 'Model-Stash'}stash() {console.log(`${this.toString()} #stash`)const tree = {}const state = datastore.read('/state')const setup = datastore.read('/setup')if (state) Object.assign(tree, { state })if (setup) Object.assign(tree, { setup })fs.writeFile('./.stash.json',JSON.stringify(tree, null, 2),(error) => {if (!error) returnconsole.log("Error Writing to Stash")console.log(error)})}destroy() {clearInterval(this.interval)}}const stash = new Stash()const cleanup = () => {stash.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { stash }
import { v4 as uuid } from 'uuid'class User {constructor() {console.log(`${this.toString()} #constructor`)datastore.subscribe('q/state/users/+/#', this.onStateQueued.bind(this))datastore.subscribe('q/action/users/+/events/new', this.onEventCreate.bind(this))datastore.subscribe('q/setup/users/+/events/+/+', this.onEventSetup.bind(this))}toString() {return 'Model-Users'}onStateQueued(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueconsole.log(`${this.toString()} #onStateQueued: ${pointer.path}, ${JSON.stringify(value)}`)if (!value || event.type !== '=') { return }if (pointer.leaf == 'pin') {// FIXME: @thomascowan what purpose does this serve? 09/20/2021this.accept(pointer, value)pointer.leaf = 'srp'datastore.set(pointer.path, 'authenticated')} else if (pointer.steps[4] === 'events') {switch (pointer.leaf) {case 'vote': {// NOTE: if the leaf is 'vote', then transfer the vote to the eventconst event_id = datastore.read(`/state${pointer.trunk_path}/event_id`)console.log('event_id', pointer.branch_path, `/state${pointer.trunk_path}/event_id`, event_id)if (!event_id) { return }datastore.set(pointer, null, { silent: true })console.log(`Transfer vote to /q/state/${pointer.branch.slice(2,4).join('/')}${pointer.trunk_path}/${pointer.branch.slice(-2).join('/')}`)datastore.set(`/q/state/${pointer.branch.slice(2,4).join('/')}${pointer.trunk_path}/${pointer.branch.slice(-2).join('/')}`, value)datastore.set(pointer, null, { silent: true })} breakcase 'pin': {datastore.read(`/setup/${pointer.branch.slice(2,4).join('/')}/owner`)} break}} else {this.accept(pointer, value)}}// What are the basic actions that can be taken on an event? Assuming there are three levels// of permissions that are consistent across all event types.// (These event types can be elaborated and parse out by reading a 'type' leaf later)// 1. Owner// 2. Admin// - add admin (action) v// - change name (setup) v// - splice// - insert// - blacklist (setup) v// - whitelist (setup) v// - ban user (setup) v// - unban user (setup) v// - change date (setup)// - description (setup) v// 3. Participant// - vote//// 'q/setup/users/+/events/+/+'onEventSetup(topic, event) {if (event.type !== '=') returnconst { pointer } = eventswitch(pointer.leaf) {// leafs that don't require validationcase 'name':case 'description':case 'private': {if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {this.accept(event.pointer.sliceBranch(2), event.pointer.value)}} break// leafs that require some validationcase 'pin': {this.accept(pointer, pointer.value)} breakcase 'time': {this.accept(pointer, pointer.value)} breakcase 'type': {if (!['recurring', 'one_time'].includes(pointer.value)) returnthis.accept(pointer, pointer.value)} break// special casescase 'blacklist': {if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {datastore.push(`/setup/events/${pointer.setps[5]}/blacklist`, pointer.value)}} breakcase 'whitelist': {if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {datastore.pull(`/setup/events/${pointer.setps[5]}/blacklist`, pointer.value)}} breakcase 'ban': {if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {const admins = datastore.read(`/setup/events/${pointer.steps[5]}/admin`)if (admins.includes(event.pointer.value)) {if (this.permissionFor('user', pointer.steps[3], pointer.steps[5])) {datastore.pull(`/setup/events/${pointer.setps[5]}/admin`, pointer.value)datastore.push(`/setup/events/${pointer.setps[5]}/banned`, pointer.value)}} else {datastore.pull(`/setup/events/${pointer.setps[5]}/banned`, pointer.value)}}} breakcase 'unban': {if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {datastore.pull(`/setup/events/${pointer.setps[5]}/banned`, pointer.value)}} break}}permissionFor(level, user, event) {if (level === 'owner') {return datastore.read(`/setup/events/${event}/owner`) === user} else if (level === 'admin') {(datastore.read(`/setup/events/${event}/admin`) || []).includes(user)} else {(datastore.read(`/setup/events/${event}/users`) || []).includes(user)}}onEventCreate(topic, event) {if (event.type !== '=') returnconst { pointer } = eventconst value = pointer?.tree?.valueif (!value) return// NOTE: check the account type and the number of owned events// TODO: @thomascowan add support for accoun typesconst account_type = datastore.read(`/setup${pointer.trunk_path}/type`) || 'free'const owned = datastore.read(`/setup${pointer.trunk_path}/events/owned`) || []// NOTE: check that the user meets the criteria required to create a new eventswitch (account_type) {case 'business': {// NOTE: Placeholder do nothing for now} breakcase 'basic': {if (owned.length >= 5) return} breakcase 'free': {if (owned.length >= 3) return}}// NOTE: create an event with the user_idconst user_id = pointer.steps[3]const new_event_id = uuid()datastore.write(`/setup/events/${new_event_id}/private`, true)datastore.write(`/setup/events/${new_event_id}/owner`, user_id)datastore.write(`/setup/events/${new_event_id}/admin`, [user_id])datastore.write(`/setup/events/${new_event_id}/users`, [user_id])}onEventPin(topic, event) {if (event.type !== '=') {return}const { pointer } = eventconst value = pointer?.tree?.valueif (!this.validatePin(value)) {return}const owners = datastore.read(`/state/${pointer.branch.slice(2,4).join('/')}/owners`)const user_id = pointer.steps[3]if (!owners.includes(user_id)) {return}datastore.write(`/state${pointer.branch.slice(2,4).join('/')}/pin`, value)}validatePin(pin) {/\d{4}/.test(pin) // FIXME: @thomascowan Improve available logic for pins}accept(pointer, value, { force = false } = {}) {datastore.set(pointer.path, null, { silent: true })const dequeued_pointer = pointer.dequeue()datastore.set(dequeued_pointer.path, value, { force })}destroy() {}}const users = new User()const cleanup = () => {users.destroy()}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)export { users }
import WebSocket from 'ws'import { v4 as uuid } from 'uuid'import './models/item'import './models/event'import './models/user'import './models/admin'import './models/stash'import './models/spotify'import { events } from './channels/event.js'import { users } from './channels/user.js'import { items } from './channels/item.js'import { admin } from './channels/admin.js'import { session } from './session.js'let main = () => {/* Models */const channels = {events: events,users: users,items: items,admin: admin}console.log(channels)/* IPC Sockets */const port = 25706const wss = new WebSocket.Server({ port })console.log(`Listenning on ${port}`)wss.on('connection', function connection(ws, request) {const ip = request.socket.remoteAddressws.uuid = uuid()ws.toString = () => ws.uuidconsole.log(`Connection from ${ip}`)datastore.push('/session/connections', ip)const node_id = datastore.read('/session/node_id')const hello = {}hello.o = 'w'hello.v = node_idhello.p = '/session/node_id'ws.send(JSON.stringify(hello))const publish = (topic, pointer) => {const message = {}const { tree, path } = pointerconst { value } = treemessage.o = 'p'message.t = topicmessage.p = pathmessage.v = valueconsole.log(`sending: ${JSON.stringify(message)}`)ws.send(JSON.stringify(message))}ws.on('message', (message) => {console.log(message)message = JSON.parse(message)if (!message.o) {return}let pointer, topic, channelswitch (message.o) {case 'a': {switch (message.c) {case 0: {// createAccounttopic = new Topic(message.t)channel = channels[topic.trunk[0]]channel.createAuthorization(topic, message.v, ws)} breakcase 1: {// startSessiontopic = new Topic(message.t)channel = channels[topic.trunk[0]]channel.authorize(topic, message.v, ws)} breakcase 4: {// verifySessiontopic = new Topic(message.t)channel = channels[topic.trunk[0]]if (channel.prove(topic, message.v, ws)) {session.addSubscription(message.v.u, topic)}} breakcase 5: {// post sessionsession.fetchSession(message.v, ws, channels)} breakcase 6: {// fetch sessionsession.requestSession(message.v, ws, users)} breakdefault:break}} breakcase 'm': {pointer = new Pointer(message.p)channel = channels[pointer.trunk[0]]channel.merge(pointer, message.v, ws)} breakcase 'w': {pointer = new Pointer(message.p)channel = channels[pointer.trunk[0]]channel.write(pointer, message.v, ws)} breakcase 'd': {pointer = new Pointer(message.p)channel = channels[pointer.trunk[0]]channel.delete(pointer, ws)} breakcase 's': {topic = new Topic(message.t)console.log({topic})channel = channels[topic.trunk[0]]channel.subscribe(topic, ws, publish)if (!message.i) { break }console.log('IMMEDIATE')message.p = `/${message.t}`}case 'r': {pointer = new Pointer(message.p)channel = channels[pointer.trunk[0]]_.forEach(channel.read(pointer, ws), (value, path) => {const response = { o: 'r' }response.p = pathresponse.v = valuews.send(JSON.stringify(response))})} breakcase 'u': {topic = new Topic(message.t)channel = channels[topic.trunk[0]]channel.unsubscribe(topic, ws, publish)if (!message.i) break}}})ws.on('close', () => {datastore.pull('/session/connections', ip)_.forEach(channels, channel => {channel.unsubscribe(ws)})console.log('disconnected')})})const cleanup = () => {console.log('\rShutting down server') // eslint-disable-line no-consoleprocess.removeListener('SIGINT', cleanup)process.removeListener('SIGTERM', cleanup)wss.close(() => {datastore.destroy('')process.exit()})}process.on('SIGINT', cleanup)process.on('SIGTERM', cleanup)}// TODO: Figure out a better way to prevent jsdoctest from executing this.if (!process.env.TEST) {main()}
import crypto from "crypto"class Session {constructor() {this.cleaner = setInterval(this.clean.bind(this), 1000 * 60 * 60)this.key = '2390fbad9157e1f8de9ecb5a494feeb988dfe2ba31c39ca1ba91ba2fa9d30d20'}clean() {const timeouts = datastore.read('/session/connections/+/tokens/+')_.forEach(timeouts, (timeout, path) => {if (+ new Date < timeout) { return }const pointer = new Pointer(path.replace('/timeout', ''))datastore.destroy(pointer.path)})}fetchSession(cookie, ws, channels) {console.log(`fetchSession\n\ntoken:\n${cookie}`)const response = {o: 'a',c: 5,v: false}// {// tokens: ,// topics: []// }const [user, token, mac] = cookie.split(':')const verify = crypto.createHmac('sha256',user + ':' + token,this.key).digest('hex')const session = datastore.read(`/session/connections/${user}`)if (!session) returnif (crypto.timingSafeEqual(Buffer.from(verify, 'utf8'), Buffer.from(mac, 'utf8')) &&session && session.tokens) {_.find(session.tokens, (timestamp, stored_token) => {if (crypto.timingSafeEqual(Buffer.from(stored_token, 'utf8'), Buffer.from(token, 'utf8')) &×tamp > + new Date) {response.v = { u: user, s: session.topics || [] }return true}})}_.forEach((session.topics || []), pattern => {console.log(pattern)const topic = new Topic(pattern)const channel = channels[topic.trunk[0]]channel.resume(topic, ws)})ws.send(JSON.stringify(response))}requestSession(user, ws, users) {if (!users.isAuthorized(new Topic(`state/users/${user}/#`), ws)) {return}const token = crypto.randomBytes(256).toString('hex')const cookie = user + ':' + tokenconsole.log(`postSession for ${user} \n\ntoken:\n${token}`)const mac = crypto.createHmac('sha256',user + ':' + token,this.key).digest('hex')console.log('L')console.log(mac)datastore.write(`/session/connections/${user}/tokens/${token}`, + new Date + 60 * 60 * 24 * 7 * 1000)const response = {o: 'a',c: 6,v: cookie + ':' + mac}ws.send(JSON.stringify(response))}addSubscription(user, topic) {console.log(`#addSubscription ${user}, ${topic.pattern}`)const topics = new Set(datastore.read(`/session/connections/${user}/topics`) || [])topics.add(topic.pattern)datastore.write(`/session/connections/${user}/topics`, Array.from(topics))}toString() {return 'Channel-Connections'}}const session = new Session()export { session }
- datastore
hasBin: trueresolution:integrity: sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==
/varint/6.0.0:resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}dev: false
svelte: 3.42.1svelte-preprocess: 3.9.12_svelte@3.42.1svelte-preprocess-sass: 0.2.0app/djiny:specifiers:'@fortawesome/free-brands-svg-icons': ^5.12.0'@fortawesome/free-solid-svg-icons': ^5.12.0'@sveltejs/adapter-static': next'@sveltejs/kit': next'@taylorzane/sveltejs-adapter-node': ^1.0.0-next.35eslint: ^7.22.0eslint-config-prettier: ^8.1.0eslint-plugin-svelte3: ^3.2.0lodash: ^4.17.21node-sass: ^6.0.1prettier: ~2.2.1prettier-plugin-svelte: ^2.2.0query-string: 4.3.2sanitize.css: ^12.0.1sass: ^1.35.2secure-remote-password: ^0.3.1svelte: ^3.34.0svelte-preprocess: ^4.7.3uuid: ^8.0.0dependencies:'@fortawesome/free-brands-svg-icons': 5.15.4'@fortawesome/free-solid-svg-icons': 5.15.4sanitize.css: 12.0.1sass: 1.37.5devDependencies:'@sveltejs/adapter-static': 1.0.0-next.16'@sveltejs/kit': 1.0.0-next.146_svelte@3.42.1'@taylorzane/sveltejs-adapter-node': 1.0.0-next.35eslint: 7.32.0eslint-config-prettier: 8.3.0_eslint@7.32.0eslint-plugin-svelte3: 3.2.0_eslint@7.32.0+svelte@3.42.1lodash: 4.17.21node-sass: 6.0.1prettier: 2.2.1prettier-plugin-svelte: 2.3.1_prettier@2.2.1+svelte@3.42.1query-string: 4.3.2secure-remote-password: 0.3.1svelte: 3.42.1svelte-preprocess: 4.7.4_6197623e5ed34153d1bcd9290e2954d7uuid: 8.3.2
lib/datastore_old:
lib/ipc:specifiers:uuid: ^8.0.0varint: ^6.0.0dependencies:uuid: 8.3.2varint: 6.0.0services/channel_ws_server:specifiers:'@djinlist/env': 1.0.0'@djinlist/ipc': 1.0.0uuid: ^8.0.0ws: ^6.0.0dependencies:'@djinlist/env': link:../../lib/env'@djinlist/ipc': link:../../lib/ipcuuid: 8.3.2ws: 6.2.2services/datastore_ipc_server:
/@eslint/eslintrc/0.4.3:resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==}engines: {node: ^10.12.0 || >=12.0.0}dependencies:ajv: 6.12.6debug: 4.3.2espree: 7.3.1globals: 13.10.0ignore: 4.0.6import-fresh: 3.3.0js-yaml: 3.14.1minimatch: 3.0.4strip-json-comments: 3.1.1transitivePeerDependencies:- supports-colordev: true/@fortawesome/fontawesome-common-types/0.2.36:resolution: {integrity: sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==, tarball: '@fortawesome/fontawesome-common-types/-/0.2.36/fontawesome-common-types-0.2.36.tgz'}engines: {node: '>=6'}requiresBuild: truedev: false/@fortawesome/free-brands-svg-icons/5.15.4:resolution: {integrity: sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==, tarball: '@fortawesome/free-brands-svg-icons/-/5.15.4/free-brands-svg-icons-5.15.4.tgz'}engines: {node: '>=6'}requiresBuild: truedependencies:'@fortawesome/fontawesome-common-types': 0.2.36dev: false/@fortawesome/free-solid-svg-icons/5.15.4:resolution: {integrity: sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==, tarball: '@fortawesome/free-solid-svg-icons/-/5.15.4/free-solid-svg-icons-5.15.4.tgz'}engines: {node: '>=6'}requiresBuild: truedependencies:'@fortawesome/fontawesome-common-types': 0.2.36dev: false/@humanwhocodes/config-array/0.5.0:resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==}engines: {node: '>=10.10.0'}dependencies:'@humanwhocodes/object-schema': 1.2.0debug: 4.3.2minimatch: 3.0.4transitivePeerDependencies:- supports-colordev: true/@humanwhocodes/object-schema/1.2.0:resolution: {integrity: sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==}dev: true
dev: true/@sveltejs/adapter-static/1.0.0-next.16:resolution: {integrity: sha512-xGFcg+GHF0BL1fyWx2vCzlYj4S4R+Od9cF00soo1TVp/scGOi1G9grSYYW4x5H+iDn1sscoJ65OGBGWIcOgrXg==}dev: true/@sveltejs/kit/1.0.0-next.146_svelte@3.42.1:resolution: {integrity: sha512-MSatcaCRfjl88Prd5mW4pNOJ3Gsr525+Vjr24MoKtyTt6PZQmTfQsDVwyP93exn/6w2xl9uMCW6cFpDVBu7jSg==}engines: {node: ^12.20 || >=14.13}hasBin: truepeerDependencies:svelte: ^3.34.0dependencies:'@sveltejs/vite-plugin-svelte': 1.0.0-next.15_svelte@3.42.1+vite@2.4.4cheap-watch: 1.0.3sade: 1.7.4svelte: 3.42.1vite: 2.4.4transitivePeerDependencies:- diff-match-patch- supports-color
/@sveltejs/vite-plugin-svelte/1.0.0-next.15_svelte@3.42.1+vite@2.4.4:resolution: {integrity: sha512-8yGX7PxaqtvWw+GHiO2DV7lZ4M7DwIrFq+PgZGZ9X09PuoSeaWszm76GWQXJMKHoPPhdA9084662en9qbv4aRw==}engines: {node: ^12.20 || ^14.13.1 || >= 16}peerDependencies:diff-match-patch: ^1.0.5svelte: ^3.34.0vite: ^2.3.7peerDependenciesMeta:diff-match-patch:optional: truedependencies:'@rollup/pluginutils': 4.1.1debug: 4.3.2kleur: 4.1.4magic-string: 0.25.7require-relative: 0.8.7svelte: 3.42.1svelte-hmr: 0.14.7_svelte@3.42.1vite: 2.4.4transitivePeerDependencies:- supports-colordev: true/@taylorzane/sveltejs-adapter-node/1.0.0-next.35:resolution: {integrity: sha512-5DVAmeCgcKtU+DZ36HoglNHjPZbx52/cP3V9s/RcwAzEa9VKh+MVwxW/fYm8M7x3bD4YUhaAy5ECdKjCPxUwcw==}dependencies:esbuild: 0.12.19tiny-glob: 0.2.9dev: true
dev: true/astral-regex/2.0.0:resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}engines: {node: '>=8'}dev: true/async-foreach/0.1.3:resolution: {integrity: sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=}
optionalDependencies:fsevents: 2.3.2dev: true/chokidar/3.5.2:resolution: {integrity: sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==}engines: {node: '>= 8.10.0'}dependencies:anymatch: 3.1.2braces: 3.0.2glob-parent: 5.1.2is-binary-path: 2.1.0is-glob: 4.0.1normalize-path: 3.0.0readdirp: 3.6.0
/eslint-plugin-svelte3/3.2.0_eslint@7.32.0+svelte@3.42.1:resolution: {integrity: sha512-qdWB1QN21dEozsJFdR8XlEhMnsS6aKHjsXWuNmchYwxoet5I6QdCr1Xcq62++IzRBMCNCeH4waXqSOAdqrZzgA==}engines: {node: '>=10'}peerDependencies:eslint: '>=6.0.0'svelte: ^3.2.0dependencies:eslint: 7.32.0svelte: 3.42.1dev: true
/eslint/7.32.0:resolution: {integrity: sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==}engines: {node: ^10.12.0 || >=12.0.0}hasBin: truedependencies:'@babel/code-frame': 7.12.11'@eslint/eslintrc': 0.4.3'@humanwhocodes/config-array': 0.5.0ajv: 6.12.6chalk: 4.1.2cross-spawn: 7.0.3debug: 4.3.2doctrine: 3.0.0enquirer: 2.3.6escape-string-regexp: 4.0.0eslint-scope: 5.1.1eslint-utils: 2.1.0eslint-visitor-keys: 2.1.0espree: 7.3.1esquery: 1.4.0esutils: 2.0.3fast-deep-equal: 3.1.3file-entry-cache: 6.0.1functional-red-black-tree: 1.0.1glob-parent: 5.1.2globals: 13.10.0ignore: 4.0.6import-fresh: 3.3.0imurmurhash: 0.1.4is-glob: 4.0.1js-yaml: 3.14.1json-stable-stringify-without-jsonify: 1.0.1levn: 0.4.1lodash.merge: 4.6.2minimatch: 3.0.4natural-compare: 1.4.0optionator: 0.9.1progress: 2.0.3regexpp: 3.2.0semver: 7.3.5strip-ansi: 6.0.0strip-json-comments: 3.1.1table: 6.7.1text-table: 0.2.0v8-compile-cache: 2.3.0transitivePeerDependencies:- supports-colordev: true
/espree/7.3.1:resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==}engines: {node: ^10.12.0 || >=12.0.0}dependencies:acorn: 7.4.1acorn-jsx: 5.3.2_acorn@7.4.1eslint-visitor-keys: 1.3.0dev: true
dev: true/forever-agent/0.6.1:resolution: {integrity: sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=}dev: true/form-data/2.3.3:resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}engines: {node: '>= 0.12'}dependencies:asynckit: 0.4.0combined-stream: 1.0.8mime-types: 2.1.32
/gauge/2.7.4:resolution: {integrity: sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=}dependencies:aproba: 1.2.0console-control-strings: 1.1.0has-unicode: 2.0.1object-assign: 4.1.1signal-exit: 3.0.3string-width: 1.0.2strip-ansi: 3.0.1wide-align: 1.1.3dev: true/gaze/1.1.3:resolution: {integrity: sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==}engines: {node: '>= 4.0.0'}dependencies:globule: 1.3.2dev: true
/globrex/0.1.2:resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}dev: true/globule/1.3.2:resolution: {integrity: sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==}engines: {node: '>= 0.10'}dependencies:glob: 7.1.7lodash: 4.17.21minimatch: 3.0.4dev: true
/har-validator/5.1.5:resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==}engines: {node: '>=6'}deprecated: this library is no longer supporteddependencies:ajv: 6.12.6har-schema: 2.0.0dev: true/hard-rejection/2.1.0:resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}engines: {node: '>=6'}dev: true
dev: true/meow/9.0.0:resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==}engines: {node: '>=10'}dependencies:'@types/minimist': 1.2.2camelcase-keys: 6.2.2decamelize: 1.2.0decamelize-keys: 1.1.0hard-rejection: 2.1.0minimist-options: 4.1.0normalize-package-data: 3.0.2read-pkg-up: 7.0.1redent: 3.0.0trim-newlines: 3.0.1type-fest: 0.18.1yargs-parser: 20.2.9
/minipass/3.1.3:resolution: {integrity: sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==}engines: {node: '>=8'}dependencies:yallist: 4.0.0dev: true/minizlib/2.1.2:resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}engines: {node: '>= 8'}dependencies:minipass: 3.1.3yallist: 4.0.0dev: true
/node-gyp/7.1.2:resolution: {integrity: sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==}engines: {node: '>= 10.12.0'}hasBin: truedependencies:env-paths: 2.2.1glob: 7.1.7graceful-fs: 4.2.8nopt: 5.0.0npmlog: 4.1.2request: 2.88.2rimraf: 3.0.2semver: 7.3.5tar: 6.1.7which: 2.0.2dev: true
/node-sass/6.0.1:resolution: {integrity: sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ==}engines: {node: '>=12'}hasBin: truerequiresBuild: truedependencies:async-foreach: 0.1.3chalk: 1.1.3cross-spawn: 7.0.3gaze: 1.1.3get-stdin: 4.0.1glob: 7.1.7lodash: 4.17.21meow: 9.0.0nan: 2.15.0node-gyp: 7.1.2npmlog: 4.1.2request: 2.88.2sass-graph: 2.2.5stdout-stream: 1.4.1true-case-path: 1.0.3dev: true
/normalize-package-data/3.0.2:resolution: {integrity: sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==}engines: {node: '>=10'}dependencies:hosted-git-info: 4.0.2resolve: 1.20.0semver: 7.3.5validate-npm-package-license: 3.0.4dev: true
word-wrap: 1.2.3dev: true/optionator/0.9.1:resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}engines: {node: '>= 0.8.0'}dependencies:deep-is: 0.1.3fast-levenshtein: 2.0.6levn: 0.4.1prelude-ls: 1.2.1type-check: 0.4.0
dev: true/prettier-plugin-svelte/2.3.1_prettier@2.2.1+svelte@3.42.1:resolution: {integrity: sha512-F1/r6OYoBq8Zgurhs1MN25tdrhPw0JW5JjioPRqpxbYdmrZ3gY/DzHGs0B6zwd4DLyRsfGB2gqhxUCbHt/D1fw==}peerDependencies:prettier: ^1.16.4 || ^2.0.0svelte: ^3.2.0dependencies:prettier: 2.2.1svelte: 3.42.1
dev: true/prettier/2.2.1:resolution: {integrity: sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==}engines: {node: '>=10.13.0'}hasBin: truedev: true/process-nextick-args/2.0.1:resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
/qs/6.5.2:resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==}engines: {node: '>=0.6'}dev: true/query-string/4.3.2:resolution: {integrity: sha1-7A/XZfWKUAMaOWjCQxOG+JR6XN0=}engines: {node: '>=0.10.0'}dependencies:object-assign: 4.1.1strict-uri-encode: 1.1.0dev: true
dev: true/readable-stream/2.3.7:resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}dependencies:core-util-is: 1.0.2inherits: 2.0.4isarray: 1.0.0process-nextick-args: 2.0.1safe-buffer: 5.1.2string_decoder: 1.1.1util-deprecate: 1.0.2
/request/2.88.2:resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}engines: {node: '>= 6'}deprecated: request has been deprecated, see https://github.com/request/request/issues/3142dependencies:aws-sign2: 0.7.0aws4: 1.11.0caseless: 0.12.0combined-stream: 1.0.8extend: 3.0.2forever-agent: 0.6.1form-data: 2.3.3har-validator: 5.1.5http-signature: 1.2.0is-typedarray: 1.0.0isstream: 0.1.2json-stringify-safe: 5.0.1mime-types: 2.1.32oauth-sign: 0.9.0performance-now: 2.1.0qs: 6.5.2safe-buffer: 5.2.1tough-cookie: 2.5.0tunnel-agent: 0.6.0uuid: 3.4.0dev: true
/sanitize.css/12.0.1:resolution: {integrity: sha512-QbusSBnWHaRBZeTxsJyknwI0q+q6m1NtLBmB76JfW/rdVN7Ws6Zz70w65+430/ouVcdNVT3qwrDgrM6PaYyRtw==}dev: false/sass-graph/2.2.5:resolution: {integrity: sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==}hasBin: truedependencies:glob: 7.1.7lodash: 4.17.21scss-tokenizer: 0.2.3yargs: 13.3.2dev: true/sass/1.37.5:resolution: {integrity: sha512-Cx3ewxz9QB/ErnVIiWg2cH0kiYZ0FPvheDTVC6BsiEGBTZKKZJ1Gq5Kq6jy3PKtL6+EJ8NIoaBW/RSd2R6cZOA==}engines: {node: '>=8.9.0'}hasBin: truedependencies:chokidar: 3.5.2dev: false/scss-tokenizer/0.2.3:resolution: {integrity: sha1-jrBtualyMzOCTT9VMGQRSYR85dE=}dependencies:js-base64: 2.6.4source-map: 0.4.4dev: true
/sshpk/1.16.1:resolution: {integrity: sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==}engines: {node: '>=0.10.0'}hasBin: truedependencies:asn1: 0.2.4assert-plus: 1.0.0bcrypt-pbkdf: 1.0.2dashdash: 1.14.1ecc-jsbn: 0.1.2getpass: 0.1.7jsbn: 0.1.1safer-buffer: 2.1.2tweetnacl: 0.14.5dev: true
/svelte-hmr/0.14.7_svelte@3.42.1:resolution: {integrity: sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==}peerDependencies:svelte: '>=3.19.0'dependencies:svelte: 3.42.1dev: true/svelte-preprocess-filter/1.0.0:resolution: {integrity: sha512-92innv59nyEx24xbfcSurB5ocwC8qFdDtGli/JVMHzJsxyvV2yjQKIcbUqU9VIV5mKUWO2PoY93nncS2yF4ULQ==}dev: true/svelte-preprocess-sass/0.2.0:resolution: {integrity: sha512-xcjwihO9hhd5W9hCSFKv1iBc8XhMif50IPP9Qu2G8IaxVaOoBUWeZu21Qu26tOw2gtv44/3p00eLrxGzLWyCLg==}dependencies:svelte-preprocess-filter: 1.0.0dev: true/svelte-preprocess/3.9.12_svelte@3.42.1:resolution: {integrity: sha512-OX8a7drmlYcX/bLKbtRTvcc0lYu5Ub78D4B/GVxac2zeyrj1e5vEJU6BsxFbc/8kFDqI6BgsCLZAqsFDr/KrDQ==}engines: {node: '>= 7.6.0'}requiresBuild: truepeerDependencies:'@babel/core': ^7.10.2coffeescript: ^2.5.1less: ^3.11.3node-sass: '*'postcss: ^7.0.32postcss-load-config: ^2.1.0pug: ^3.0.0sass: ^1.26.8stylus: ^0.54.7svelte: ^3.23.0typescript: ^3.9.5peerDependenciesMeta:'@babel/core':optional: truecoffeescript:optional: trueless:optional: truenode-sass:optional: truepostcss:optional: truepostcss-load-config:optional: truepug:optional: truesass:optional: truestylus:optional: truesvelte:optional: truetypescript:optional: truedependencies:'@types/pug': 2.0.5'@types/sass': 1.16.1detect-indent: 6.1.0strip-indent: 3.0.0svelte: 3.42.1dev: true/svelte-preprocess/4.7.4_6197623e5ed34153d1bcd9290e2954d7:resolution: {integrity: sha512-mDAmaltQl6e5zU2VEtoWEf7eLTfuOTGr9zt+BpA3AGHo8MIhKiNSPE9OLTCTOMgj0vj/uL9QBbaNmpG4G1CgIA==}engines: {node: '>= 9.11.2'}requiresBuild: truepeerDependencies:'@babel/core': ^7.10.2coffeescript: ^2.5.1less: ^3.11.3node-sass: '*'postcss: ^7 || ^8postcss-load-config: ^2.1.0 || ^3.0.0pug: ^3.0.0sass: ^1.26.8stylus: ^0.54.7sugarss: ^2.0.0svelte: ^3.23.0typescript: ^3.9.5 || ^4.0.0peerDependenciesMeta:'@babel/core':optional: truecoffeescript:optional: trueless:optional: truenode-sass:optional: truepostcss:optional: truepostcss-load-config:optional: truepug:optional: truesass:optional: truestylus:optional: truesugarss:optional: truetypescript:optional: truedependencies:'@types/pug': 2.0.5'@types/sass': 1.16.1detect-indent: 6.1.0node-sass: 6.0.1sass: 1.37.5strip-indent: 3.0.0svelte: 3.42.1dev: true/svelte/3.42.1:resolution: {integrity: sha512-XtExLd2JAU3T7M2g/DkO3UNj/3n1WdTXrfL63OZ5nZq7nAqd9wQw+lR4Pv/wkVbrWbAIPfLDX47UjFdmnY+YtQ==}engines: {node: '>= 8'}dev: true
dev: true/table/6.7.1:resolution: {integrity: sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==}engines: {node: '>=10.0.0'}dependencies:ajv: 8.6.2lodash.clonedeep: 4.5.0lodash.truncate: 4.4.2slice-ansi: 4.0.0string-width: 4.2.2strip-ansi: 6.0.0
/tar/6.1.7:resolution: {integrity: sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA==}engines: {node: '>= 10'}dependencies:chownr: 2.0.0fs-minipass: 2.1.0minipass: 3.1.3minizlib: 2.1.2mkdirp: 1.0.4yallist: 4.0.0dev: true
/uuid/3.4.0:resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.hasBin: truedev: true
/verror/1.10.0:resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=}engines: {'0': node >=0.6.0}dependencies:assert-plus: 1.0.0core-util-is: 1.0.2extsprintf: 1.3.0dev: true
/varint/6.0.0:resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}dev: false
dev: true/vite/2.4.4:resolution: {integrity: sha512-m1wK6pFJKmaYA6AeZIUXyiAgUAAJzVXhIMYCdZUpCaFMGps0v0IlNJtbmPvkUhVEyautalajmnW5X6NboUPsnw==}engines: {node: '>=12.0.0'}hasBin: truedependencies:esbuild: 0.12.19postcss: 8.3.6resolve: 1.20.0rollup: 2.56.1optionalDependencies:fsevents: 2.3.2
import Benchmark from 'benchmark'const suite = new Benchmark.Suite()function isEmpty1(obj) {for (const prop in obj) {if (Object.prototype.hasOwnProperty.call(obj, prop)) {return false}}return true}function isEmpty2(obj) {return Object.getOwnPropertyNames(obj).length === 0}suite.add('for ... in', function() {isEmpty1({ a: 1, b: 2, c: 3 })})suite.add('keys', function() {isEmpty2({ a: 1, b: 2, c: 3 })})suite.on('abort', function() {console.log('Aborted')})suite.on('error', function(event) {console.log(String(JSON.stringify(event)))})suite.on('cycle', function(event) {console.log(String(event.target))})suite.on('complete', function() {console.log('Fastest is ' + this.filter('fastest').map('name'))})suite.run()
{"name": "@djinlist/datastore","version": "0.0.0","type": "module","license": "Apache-2.0","private": true,"main": "src/index.js","svelte": "src/index.js","dependencies": {"lodash": "^4.17.15"}}
class Base {constructor(group, name, ...callbacks) {this.group = groupthis.name = namethis.callbacks = callbacksthis.datastore = this.group.datastore}addDependent(dependent, position) {if (!(this.dependents instanceof Map)) {this.dependents = new Map()}this.dependents.set(dependent, position)}notify() {if (!(this.dependents instanceof Map)) {return}this.dependents.forEach((position, dependent) => {if (position != null) {// Linkable, Preferable, Pathabledependent.steps[position] = this.valuedependent.update()} else {// Pipabledependent.update(this.value)}})}destroy() {}}export { Base }
import { Base } from './base.js'class Injectable extends Base {constructor(group, name, value) {super(group, name)this.value = value}toString() {return `[Injectable ${JSON.stringify(this.name)}]`}update(value) {this.value = valuethis.notify()}}export { Injectable }
import { Pointer } from '../pointer.js'import { Base } from './base.js'class Linkable extends Base {constructor(group, name, topic_or_path, ...callbacks) {super(group, name, ...callbacks)this.relink(topic_or_path, callbacks)}toString() {return `[Linkable ${JSON.stringify(this.name)}]`}relink(topic_or_path, callbacks) {if (callbacks.length > 1) {if (callbacks.slice(-1)[0] === true) {callbacks = callbacks.slice(0, -1)this.topic_or_path = null // force relink}}this.callbacks = callbacksif (this.topic_or_path === topic_or_path) {return false}this.topic_or_path = topic_or_paththis.pointer = Pointer.create(topic_or_path)this.steps = []this.is_static = truethis.pointer.steps.forEach((step, i) => {if (step.startsWith(':')) {step = step.slice(1)this.is_static = falsethis.group.addDependent(step, this, i)this.steps.push(null)} else {this.steps.push(step)}})this.update()return true}update() {if (this.topic != null) {this.datastore.unsubscribe(this.topic, this)}this.topic = ''for (const step of this.steps) {if (typeof step !== 'string') {delete this.topicreturn}if (this.topic != '') {this.topic = `${this.topic}/`}if (step.startsWith('/')) {this.topic = `${this.topic}${step.slice(1)}`} else {this.topic = `${this.topic}${step}`}}this.datastore.subscribe(this.topic, this, (topic, pointer, value) => {this.value = valueif (this.callbacks.length > 0) {this.callbacks.forEach(callback => {callback(this.value, pointer)})}this.notify()})}destroy() {if (typeof this.topic !== 'string') {return}this.datastore.unsubscribe(this.topic, this)}}export { Linkable }
import { Pointer } from '../pointer.js'import { Base } from './base.js'class Pathable extends Base {constructor(group, name, topic_or_path, ...callbacks) {super(group, name, ...callbacks)this.repath(topic_or_path, callbacks)}toString() {return `[Pathable ${JSON.stringify(this.name)}]`}repath(topic_or_path, callbacks) {this.callbacks = callbacksif (this.topic_or_path === topic_or_path) {return false}this.topic_or_path = topic_or_paththis.pointer = Pointer.create(topic_or_path)this.steps = []this.is_static = truethis.pointer.steps.forEach((step, i) => {if (step.startsWith(':')) {step = step.slice(1)this.is_static = falsethis.group.addDependent(step, this, i)this.steps.push(null)} else {this.steps.push(step)}})this.update()return true}update() {let path = ''for (const step of this.steps) {if (step) {if (step.startsWith('/')) {path = `${path}${step}`} else {path = `${path}/${step}`}}}this.value = pathif (!this.value) {return}if (this.callbacks.length > 0) {this.callbacks.forEach(callback => {callback(this.value)})}this.notify()}destroy() {if (typeof this.topic !== 'string') {return}this.datastore.unsubscribe(this.topic, this)}}export { Pathable }
import { Base } from './base.js'class Pipable extends Base {constructor(group, name, from_name, replacement, ...callbacks) {super(group, name, ...callbacks)this.from_name = from_namethis.replacement = replacementthis.group.addDependent(this.from_name, this, null)}toString() {return `[Pipable ${JSON.stringify(this.name)}]`}update(value) {if (typeof this.replacement === 'string') {if (value == null) {return}this.value = value[this.replacement]if (this.callbacks.length > 0) {this.callbacks.forEach(callback => {callback(this.value)})}this.notify()return}if (typeof this.replacement === 'function') {if (value === undefined) {return}const next = this.replacement.call(this, value)if (next === this.value) returnthis.value = nextconsole.log(`${this}.update()`, this.value)if (this.callbacks.length > 0) {this.callbacks.forEach(callback => {callback(this.value)})}this.notify()}}}export { Pipable }
import { Base } from './base.js'class Preferable extends Base {constructor(group, name, preferences, default_value, ...callbacks) {super(group, name, ...callbacks)this.steps = preferencesthis.default_value = default_valuethis.steps.forEach((step, i) => {this.group.addDependent(step, this, i)})}toString() {return `[Preferable ${JSON.stringify(this.name)}]`}update() {this.value = this.default_valuefor (const step of this.steps) {if (step != null) {this.value = stepbreak}}if (this.callbacks.length > 0) {this.callbacks.forEach(callback => {callback(this.value)})}this.notify()}}export { Preferable }
import { Injectable } from './chainable/injectable.js'import { Linkable } from './chainable/linkable.js'import { Pathable } from './chainable/pathable.js'import { Pipable } from './chainable/pipable.js'import { Preferable } from './chainable/preferable.js'import { Stubbable } from './chainable/stubbable.js'class ChainableGroup {constructor(datastore) {this.datastore = datastoreObject.defineProperties(this, {links: {value: {}},paths: {value: {}},pipes: {value: {}},prefs: {value: {}},props: {value: {}},stubs: {value: {}}})this.get = this.get.bind(this)this.set = this.set.bind(this)this.keys = this.keys.bind(this)}link(name, topic_or_path, ...callbacks) {if (this.links[name] instanceof Linkable) {const notify = this.links[name].relink(topic_or_path, callbacks)if (notify) {this.notify('relink', name)}return this}this.links[name] = new Linkable(this, name, topic_or_path, ...callbacks)this.notify('link', name)return this}path(name, topic_or_path, ...callbacks) {if (this.paths[name] instanceof Pathable) {const notify = this.paths[name].repath(topic_or_path, callbacks)if (notify) {this.notify('repath', name)}return this}this.paths[name] = new Pathable(this, name, topic_or_path, ...callbacks)this.notify('path', name)return this}pipe(to_name, from_name, replacement, ...callbacks) {const pipe = this.pipes[to_name]if (pipe instanceof Pipable) {pipe.callbacks = callbackspipe.update(pipe.value)return this}this.pipes[to_name] = new Pipable(this, to_name, from_name, replacement, ...callbacks)this.notify('pipe', 'name')return this}prefer(name, prefs, default_value, ...callbacks) {if (this.prefs[name] instanceof Preferable) {throw new Error(`prefer("${name}", ...) has already been called.`)}this.prefs[name] = new Preferable(this, name, prefs, default_value, ...callbacks)this.notify('prefer', name)return this}prop(name, value) {if (this.props[name] instanceof Injectable) {this.props[name].update(value)return this}const prop = (this.props[name] = new Injectable(this, name, value))const stub = this.stubs[name]if (stub instanceof Stubbable) {prop.dependents = stub.dependentsdelete this.stubs[name]}this.notify('prop', name)return this}addDependent(name, dependent, position) {let chainable = this.links[name] || this.pipes[name] || this.prefs[name] || this.props[name]if (chainable == null) {chainable = this.stubs[name] = new Stubbable(this, name)}chainable.addDependent(dependent, position)}notify(method, group_name) {for (const name in this.props) {const chainable = this.props[name]chainable.notify()}for (const name in this.links) {const chainable = this.links[name]chainable.notify()}for (const name in this.pipes) {const chainable = this.pipes[name]chainable.notify()}}destroy() {for (const name in this.links) {const chainable = this.links[name]chainable.destroy()}for (const name in this.pipes) {const chainable = this.pipes[name]chainable.destroy()}}get(name) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))if (typeof path !== 'string') {return}return this.datastore.get(path)}set(name, value) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))if (typeof path !== 'string') {return}this.datastore.set(path, value)}keys(name) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))if (typeof path !== 'string') {return []}return this.datastore.keys(path)}queue(name, value) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))if (typeof path !== 'string') {return}this.datastore.queue(path, value)}mark(name) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))if (typeof path !== 'string') {return}this.datastore.mark(path)}push(name, value, queue = true) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))console.log('pushing', name, path, value, queue)if (typeof path !== 'string') {return}console.log('pushing', path, value, queue)this.datastore.push(path, value, queue)}pull(name, value) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))if (typeof path !== 'string') {return}this.datastore.pull(path, value)}add(name, value) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))if (typeof path !== 'string') {return}return this.datastore.add(path, value)}read(name, value) {const path = _.get(this.links, [name, 'topic'], _.get(this.props, [name, 'value']))if (typeof path !== 'string') {return}return this.datastore.read(path, value)}}export { ChainableGroup, Injectable, Linkable, Pathable, Pipable, Preferable, Stubbable }
import { Pointer } from '../pointer.js'import { isEmpty, isBareObject } from '../utils.js'class Datastore {constructor() {Object.defineProperties(this, {_root: {value: Object.create(null)}})}get(pointer) {pointer = Pointer.create(pointer)if (pointer == null) {return}let left = this._rootfor (const step of pointer.steps) {left = left[step]if (left == null) {break}}return left}set(pointer, value, { force = false, silent = false } = {}) {pointer = Pointer.create(pointer)if (pointer == null) {return}if (value == null) {this.delete(pointer, { force, silent })return}if (pointer.length === 0) {return}if (!force && _.isEqual(value, this.get(pointer))) {return}const last = pointer.steps.slice(-1)[0]let left = this._rootpointer.steps.slice(0, -1).forEach((step, i) => {if (!isBareObject(left[step])) {const subpointer = Pointer.create(pointer.steps.slice(0, i + 1))const object = Object.create(null)left[step] = object// if (!silent) {// this.publish(subpointer, object)// }}left = left[step]})left[last] = valueif (!silent) {this.publish(pointer, value)}}delete(pointer, { force = false, silent = false } = {}) {pointer = Pointer.create(pointer)if (pointer == null) {return}if (pointer.length === 0) {this.clear()return}if (!force && this.get(pointer) == null) {return}for (let i = pointer.length - 1; i > -1; i--) {const subpointer = pointer.slice(0, i)const right = this.get(subpointer)if (isBareObject(right)) {const key = pointer.steps[i]delete right[key]if (!isEmpty(right)) {break}}}if (!silent) {this.publish(pointer, null)}}clear() {for (const key in this._root) {this.delete([key])}}// publish(pointer, value, phase) {}publish() {}}export { Datastore }
import { Pointer } from '../pointer.js'import { isBareObject, isEmpty, coppice } from '../utils.js'const Convenience = superclass =>class extends superclass {has(pointer) {pointer = Pointer.create(pointer)if (pointer == null) {return}return this.get(pointer) != null}each(pointer, callback) {pointer = Pointer.create(pointer)if (pointer == null) {return}const node = this.get(pointer)if (!Array.isArray(node)) {return}node.forEach(item => {callback(item)})}includes(pointer, value) {pointer = Pointer.create(pointer)if (pointer == null) {return}const node = this.get(pointer)if (!Array.isArray(node)) {return false}return node.includes(value)}any(pointer) {pointer = Pointer.create(pointer)if (pointer == null) {return}const node = this.get(pointer)if (isBareObject(node)) {return !isEmpty(node)}return false}keys(pointer) {pointer = Pointer.create(pointer)if (pointer == null) {return []}const node = this.get(pointer)if (isBareObject(node)) {return Object.keys(node) || []}return []}branchPaths(pointer) {pointer = Pointer.create(pointer)const object = this.get(pointer)if (!isBareObject(object)) {return []}const copse = coppice(object, pointer.path)const paths = new Set()for (const path in copse) {const subpointer = Pointer.create(path)paths.add(subpointer.branch_path)}return Array.from(paths)}mark(pointer) {pointer = Pointer.create(pointer)if (pointer == null) {return}pointer.root = ['action']this.queue(pointer, Date.now())}queue(pointer, value) {pointer = Pointer.create(pointer)if (pointer == null) {return}pointer.flag = 'q'if (!isBareObject(value)) {this.set(pointer, value, { force: true })this.delete(pointer, { silent: true })return}const copse = coppice(value, pointer.path)for (const path in copse) {const value = copse[path]this.set(path, value, { force: true })this.delete(path, { silent: true })}}push(pointer, value, queue = true) {pointer = Pointer.create(pointer)if (pointer == null) {return}const existing = this.get(pointer) || []if (!Array.isArray(existing)) {return}const modified = _.uniq(_.concat(existing, value))if (queue) {this.queue(pointer, modified)} else {this.merge(pointer, modified)}}pull(pointer, value, queue = true) {pointer = Pointer.create(pointer)if (pointer == null) {return}const existing = this.get(pointer) || []if (!Array.isArray(existing)) {return}const modified = _.without(existing, value)if (queue) {this.queue(pointer, modified)} else {this.merge(pointer, modified)}}add(pointer, value, queue = true) {pointer = Pointer.create(pointer)if (pointer == null) {return}const collection = this.get(pointer) || {}if (typeof collection !== 'object') {return}let id = 1while (collection[id.toString()] != null) {id++}const added = Pointer.create(`${pointer.path}/${id}`)if (queue) {this.queue(added, value)} else {this.merge(added, value)}return id}}export { Convenience }
import { Pointer } from '../pointer.js'import { coppice, isBareObject, isTraversable } from '../utils.js'const External = superclass =>class extends superclass {read(pointer, { coppiced = false } = {}) {pointer = Pointer.create(pointer)if (pointer == null) {return}if (pointer.is_wildcard && pointer.path !== '') {return this._search(this._root, pointer.steps, 0, {})}const value = this.get(pointer)if (coppiced) {return coppice(value, pointer.path)}return value}_search(node, steps, pivot, results) {if (steps.length === pivot) {return results}const step = steps[pivot]if (step === '+') {if (isTraversable(node)) {for (const key in node) {steps[pivot] = keyconst next = node[key]if (steps.length - 1 === pivot) {const path = `/${steps.join('/')}`results[path] = nextcontinue}if (isBareObject(next)) {results = this._search(next, steps, pivot + 1, results)}}}steps[pivot] = '+'return results}if (step === '#') {const path = `/${steps.slice(0, pivot).join('/')}`const subtree = this.get(path)if (isBareObject(subtree)) {results = coppice(subtree, path, results)}return results}const next = node[step]if (next === undefined) {return results}if (steps.length - 1 === pivot) {const path = `/${steps.join('/')}`results[path] = nextreturn results}return this._search(next, steps, pivot + 1, results)}write(pointer, value, { force = false } = {}) {pointer = Pointer.create(pointer)if (pointer == null) {return}if (pointer.flag === 'q' && typeof this.queue === 'function') {this.queue(pointer, value)return}if (!isTraversable(value)) {this.set(pointer, value, { force: force })return}this.delete(pointer, { force: force })const copse = coppice(value, pointer.path)for (const path in copse) {const value = copse[path]this.set(path, value, { force: force })}}merge(pointer, value, { force = false } = {}) {pointer = Pointer.create(pointer)if (pointer == null) {return}if (pointer.flag === 'q' && typeof this.queue === 'function') {this.queue(pointer, value)return}if (!isTraversable(value)) {this.set(pointer, value, { force: force })return}const copse = coppice(value, pointer.path)for (const path in copse) {const value = copse[path]this.set(path, value, { force: force })}}destroy(pointer, { force = false } = {}) {pointer = Pointer.create(pointer)if (pointer == null) {return}this.delete(pointer, { force: force })}}export { External }
import { Pointer } from '../pointer.js'import { isEmpty, isBareObject } from '../utils.js'const Hooks = superclass =>class extends superclass {set(pointer, value, { force = false, silent = false } = {}) {pointer = Pointer.create(pointer)if (pointer == null) {return}if (value == null) {this.delete(pointer, { force, silent })return}if (pointer.length === 0) {return}if (!force && _.isEqual(value, this.get(pointer))) {return}if (!silent) {this.publish(pointer, value, 'before')this.publish(pointer, value, 'async')}const last = pointer.steps.slice(-1)[0]let left = this._rootpointer.steps.slice(0, -1).forEach((step, i) => {if (!isBareObject(left[step])) {const subpointer = Pointer.create(pointer.steps.slice(0, i + 1))const object = Object.create(null)left[step] = objectif (!silent) {this.publish(subpointer, object, 'when')}}left = left[step]})left[last] = valueif (!silent) {this.publish(pointer, value, 'when')this.publish(pointer, value, 'after')}}delete(pointer, { force = false, silent = false } = {}) {pointer = Pointer.create(pointer)if (pointer == null) {return}if (pointer.length === 0) {this.clear()return}if (!force && this.get(pointer) == null) {return}if (!silent) {this.publish(pointer, null, 'async')this.publish(pointer, null, 'before')}for (let i = pointer.length - 1; i > -1; i--) {const subpointer = pointer.slice(0, i)const right = this.get(subpointer)if (isBareObject(right)) {const key = pointer.steps[i]delete right[key]if (!isEmpty(right)) {break}}}if (!silent) {this.publish(pointer, null, 'when')this.publish(pointer, null, 'after')}}publish(pointer, value, phase) {const topic = `${phase}/${pointer.topic}`const subscription_maps = this._topic_tree.entries(topic)if (subscription_maps == null) {return}subscription_maps.forEach(entry => {const [topic, subscription_map] = entrysubscription_map.forEach((callback, subscriber) => {callback.call(subscriber, topic, pointer, value)})})const steps = pointer.steps.slice()while (steps.pop()) {const path = `/${steps.join('/')}`const subtopic = `${phase}${path}/*`const subscription_maps = this._topic_tree.entries(subtopic)const node_value = this.get(path)subscription_maps.filter(([topic]) => topic.endsWith('*')).forEach(entry => {const [topic, subscription_map] = entrysubscription_map.forEach((callback, subscriber) => {callback.call(subscriber, topic, pointer, node_value)})})}}subscribe(topic, subscriber, callback, options = { immediate: true }) {if (typeof topic !== 'string') {throw new TypeError('topic must be a string')}if (topic.startsWith('/')) {throw new TypeError(`topic ${topic} must be an MQTT-style topic`)}// Convert JSON Pointer to MQTT topicsconst subscription_map = this._topic_tree.getWithDefault(topic, new Map())._valuesubscription_map.set(subscriber, callback)const subscriber_set = (() => {if (this._subscribers_map.has(subscriber)) {return this._subscribers_map.get(subscriber)} else {return new Set()}})()subscriber_set.add(topic)this._subscribers_map.set(subscriber, subscriber_set)if (!options.immediate) {return}if (topic.endsWith('/*')) {topic = topic.slice(0, -2)}let pointer = Pointer.create(topic).slice(1)if (!pointer.is_wildcard) {const results = this.read(pointer)callback(topic, pointer, results)return}const results = this.read(pointer, { coppiced: true })for (const path in results) {callback(topic, Pointer.create(path), results[path])}}}export { Hooks }
import { Pointer } from '../pointer.js'import { TopicTree } from '../topic_tree.js'const PubSub = superclass =>class extends superclass {constructor() {super()Object.defineProperties(this, {_topic_tree: {value: new TopicTree()},_subscribers_map: {value: new Map()}})}publish(pointer, value) {// console.log(`Datastore #publish ` + pointer.path)const topic = pointer.topicconst subscription_maps = this._topic_tree.entries(topic)if (subscription_maps == null) {return}subscription_maps.forEach(entry => {const [topic, subscription_map] = entryif (subscription_map) {subscription_map.forEach((callback, subscriber) => {callback.call(subscriber, topic, pointer, value)})}})const steps = pointer.steps.slice()while (steps.pop()) {const path = `/${steps.join('/')}`const subtopic = `${steps.join('/')}/*`const subscription_maps = this._topic_tree.entries(subtopic)const node_value = this.get(path)subscription_maps.filter(([topic]) => topic && topic.endsWith('*')).forEach(entry => {const [topic, subscription_map] = entrysubscription_map.forEach((callback, subscriber) => {callback.call(subscriber, topic, pointer, node_value)})})}}subscribe(topic, subscriber, callback, options = { immediate: true }) {// console.log(`Datastore #subscribe ` + topic)if (typeof topic !== 'string') {throw new TypeError('topic must be a string')}if (topic.startsWith('/')) {throw new TypeError(`topic ${topic} must be an MQTT-style topic`)}// Convert JSON Pointer to MQTT topicsconst subscription_map = this._topic_tree.getWithDefault(topic, new Map())._valuesubscription_map.set(subscriber, callback)const subscriber_set = (() => {if (this._subscribers_map.has(subscriber)) {return this._subscribers_map.get(subscriber)} else {return new Set()}})()subscriber_set.add(topic)this._subscribers_map.set(subscriber, subscriber_set)if (!options.immediate) {return}if (topic.endsWith('/*')) {topic = topic.slice(0, -2)}let pointer = Pointer.create(topic)if (!pointer.is_wildcard) {const results = this.read(pointer)callback(topic, pointer, results)return}const results = this.read(pointer, { coppiced: true })console.log(results)for (const path in results) {callback(topic, Pointer.create(path), results[path])}}once(topic, callback) {const nonce = Object.create(null)const wrapper = (topic, pointer, value) => {if (typeof value === 'undefined') returncallback(topic, pointer, value)this.unsubscribe(topic, nonce)}this.subscribe(topic, nonce, wrapper)}unsubscribe(topic, subscriber) {if (topic == null) {const subscriber_set = this._subscribers_map.get(subscriber)if (subscriber_set == null) returnsubscriber_set.forEach(topic => {this.unsubscribe(topic, subscriber)})return}if (typeof topic !== 'string') {throw new TypeError('topic must be a string')}if (topic.startsWith('/')) {throw new TypeError(`topic ${topic} must be an MQTT-style topic`)}const subscription_map = this._topic_tree.get(topic)._valueif (subscription_map == null) {return}subscription_map.delete(subscriber)}}export { PubSub }
import { Datastore as Base } from './datastore/base.js'import { External } from './datastore/external.js'import { Convenience } from './datastore/convenience.js'import { PubSub } from './datastore/pubsub.js'import { Chain } from './datastore/chain.js'import { Hooks } from './datastore/hooks.js'class Datastore extends Chain(PubSub(Convenience(External(Base)))) {}class DatastoreWithHooks extends Hooks(Chain(PubSub(Convenience(External(Base))))) {}export { Datastore, DatastoreWithHooks}
export { Datastore as Base } from './datastore/base.js'export { External } from './datastore/external.js'export { Convenience } from './datastore/convenience.js'export { PubSub } from './datastore/pubsub.js'export { Chain } from './datastore/chain.js'export { Hooks } from './datastore/hooks.js'export { Datastore, DatastoreWithHooks } from './datastore.js'export { Pointer, toPointer, toPath } from './pointer.js'export { coppice, isCoppice } from './utils.js'export { TopicTree } from './topic_tree.js'
class Pointer {constructor(path_steps_topic) {this.isEqual = this.isEqual.bind(this)this.clear()if (Array.isArray(path_steps_topic)) {this.steps = path_steps_topicreturn}if (typeof path_steps_topic === 'string') {if (path_steps_topic.startsWith('/') || path_steps_topic === '') {this.path = path_steps_topic} else {this.topic = path_steps_topic}return}throw new TypeError(`Pointer path_steps_topic must be an array or string not a ${typeof path_steps_topic}`)}get path() {if (typeof this._path === 'string') {return this._path}if (Array.isArray(this._steps)) {if (this.steps.length === 0) {this._path = ''} else {this._path = `/${this._steps.join('/')}`}return this._path}if (typeof this._topic === 'string') {if (this._topic === '#') {this._path = ''} else {this._path = `/${this._topic}`}return this._path}throw new TypeError('Pointer must have path or steps or topic.')}set path(value) {if (typeof value === 'string') {this.clear()this._path = valuereturn}throw new TypeError('Pointer path must be a string.')}get steps() {if (Array.isArray(this._steps)) {return this._steps}if (typeof this._path === 'string') {if (this._path === '') {this._steps = []} else {this._steps = this._path.slice(1).split('/')}return this._steps}if (typeof this._topic === 'string') {if (this._topic === '#') {this._steps = []} else {this._steps = this._topic.split('/')}return this._steps}throw new TypeError('Pointer must have path or steps or topic.')}set steps(value) {if (Array.isArray(value)) {this.clear()if (value[0] === '#') {this._steps = []} else {this._steps = value}return}throw new TypeError('Pointer steps must be an array.')}get topic() {if (typeof this._topic === 'string') {return this._topic}if (typeof this._path === 'string') {if (this._path === '') {this._topic = '#'} else {this._topic = this._path.slice(1)}return this._topic}if (Array.isArray(this._steps)) {if (this.steps.length === 0) {this._topic = '#'} else {this._topic = this._steps.join('/')}return this._topic}throw new TypeError('Pointer must have path or steps or topic.')}set topic(value) {if (typeof value === 'string') {this.clear()this._topic = valuereturn}throw new TypeError('Pointer topic must be a string.')}get is_wildcard() {if (typeof this._is_wildcard !== 'boolean') {this._is_wildcard = this.topic.endsWith('#') || this.topic.endsWith('/*') || this.topic.indexOf('/+') > -1}return this._is_wildcard}toString() {return this.path}isEqual(other) {return this.path === other.path}get length() {return this.steps.length}clone() {return Pointer.create(this.path)}slice(index) {return Pointer.create(this.steps.slice(index))}clear() {Object.defineProperties(this, {_path: {enumerable: false,writable: true,value: undefined},_steps: {enumerable: false,writable: true,value: undefined},_topic: {enumerable: false,writable: true,value: undefined},_is_wildcard: {enumerable: false,writable: true,value: undefined}})}}export { Pointer }
const DjinList = superclass =>class extends superclass {get flag() {if (!this._parsed) {this.parse()}if (this._flag_start == null) {return null}return this.steps[this._flag_start]}set flag(value) {if (!this._parsed) {this.parse()}if (this._flag_start == null) {let start = 0if (this._hook_start != null) {start += 1}if (this._grove_start != null) {start += 2}this.steps.splice(start, 0, value)this._parsed = false} else {if (value != null) {this.steps.splice(this._flag_start, 1, value)} else {this.steps.splice(this._flag_start, 1)this._parsed = false}}this.steps = this.steps // eslint-disable-line}get root() {if (!this._parsed) {this.parse()}if (this._root_start == null) {return null}return this.steps[this._root_start]}set root(value) {if (!this._parsed) {this.parse()}if (typeof value === 'string') {switch (true) {case value.startsWith('/'): {value = value.slice(1)}case value.startsWith('q'): {value = value.split('/')}}}if (this._root_start == null) {let start = 0if (this._flag_start != null) {start += 1}if (typeof value === 'string') {this.steps.splice(start, 0, value)} else {this.steps.splice(start, 0, ...value)}this._parsed = false} else {if (typeof value === 'string') {this.steps.splice(this._root_start, 1, value)} else {this.steps.splice(this._root_start, 1, ...value)}this._parsed = value.length > 0}this.steps = this.steps // eslint-disable-line}get trunk_path() {if (!this._parsed) {this.parse()}if (this._trunk_start == null) {return null}return `/${this.steps.slice(this._trunk_start, this._trunk_end).join('/')}`}set trunk_path(value) {const steps = value == null ? [] : value.slice(1).split('/')this.trunk_steps = steps}get trunk_steps() {if (!this._parsed) {this.parse()}if (this._trunk_start == null) {return null}return this.steps.slice(this._trunk_start, this._trunk_end)}set trunk_steps(value) {if (!this._parsed) {this.parse()}if (this._trunk_start == null) {let start = 0if (this._flag_start != null) {start += 1}if (this._root_start != null) {start += 1}this.steps.splice(start, 0, ...value)this._parsed = false} else {this.steps.splice(this._trunk_start, 2, ...value)this._parsed = value.length > 0}this.steps = this.steps // eslint-disable-line}get branch_path() {if (!this._parsed) {this.parse()}if (this._branch_start == null) {return null}return `/${this.steps.slice(this._branch_start, this._branch_end).join('/')}`}set branch_path(value) {const steps = value == null ? [] : value.slice(1).split('/')this.branch_steps = steps}get branch_steps() {if (!this._parsed) {this.parse()}if (this._branch_start == null) {return null}return this.steps.slice(this._branch_start, this._branch_end)}set branch_steps(value) {if (!this._parsed) {this.parse()}if (this._branch_start == null) {let start = 0if (this._hook_start != null) {start += 1}if (this._grove_start != null) {start += 2}if (this._flag_start != null) {start += 1}if (this._root_start != null) {start += 1}this.steps.splice(start, 0, ...value)this._parsed = false} else {const removed = this._branch_end - this._branch_start // FIXME: Remove damned build processes. This ';' shouldn't be needed!this.steps.splice(this._branch_start, removed, ...value)this._parsed = value.length === removed}this.steps = this.steps // eslint-disable-line}get twig_path() {if (!this._parsed) {this.parse()}if (this._twig_start == null) {return null}return `/${this.steps.slice(this._twig_start, this._twig_end).join('/')}`}get twig_steps() {if (!this._parsed) {this.parse()}if (this._twig_start == null) {return null}return this.steps.slice(this._twig_start, this._twig_end)}get leaf() {if (!this._parsed) {this.parse()}if (this._twig_start == null) {return null}return this.steps[this.steps.length - 1]}set leaf(value) {if (!this._parsed) {this.parse()}if (value == null) {if (this.leaf != null) {this.steps = this.steps.slice(0, -1)this._twig_start = nullthis._twig_end = null}return}if (this.leaf == null) {this._twig_start = this.steps.lengththis._twig_end = this._twig_start + 1}this.steps[this._twig_end - 1] = valuethis.steps = this.steps // eslint-disable-line}parse() {let cursor = 0Object.defineProperties(this, {_flag_start: {writable: true,value: undefined},_root_start: {writable: true,value: undefined},_trunk_start: {writable: true,value: undefined},_trunk_end: {writable: true,value: undefined},_branch_start: {writable: true,value: undefined},_branch_end: {writable: true,value: undefined},_twig_start: {writable: true,value: undefined},_twig_end: {writable: true,value: undefined}})switch (this.steps[cursor]) {case 'q':this._flag_start = cursorcursor += 1breakcase 'session':this._root_start = cursorcursor += 1this.parseSession(cursor)return}switch (this.steps[cursor]) {case 'action':case 'state':case 'setup':case '+':this._root_start = cursorcursor += 1break}switch (this.steps[cursor]) {case 'users':case 'events':case 'items':this._trunk_start = cursorthis._branch_start = cursorthis._trunk_end = cursor + 2breakcase 'admin':this._trunk_start = cursorthis._branch_start = cursorthis._trunk_end = cursor + 1breakcase 'services':this._trunk_start = cursorthis._trunk_end = cursor + 1cursor += 1this.parseService(cursor)return}const limit = this.steps.length - 1while (cursor < limit) {const collection = this.steps[cursor]const id = this.steps[cursor + 1]if (!(/^(?:\w+|\+)$/.test(collection) && /^(?:\d+|\+|[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}|\w{22})$/i.test(id))) {break}cursor += 2this._branch_end = cursor}if (cursor < this.steps.length) {this._twig_start = cursorthis._twig_end = this.steps.length}this._parsed = true}parseSession(cursor) {switch (this.steps[cursor]) {case 'path':case 'redirect':case 'user':this.twig_start = cursorcursor += 1this.twig_end = this.steps.lengthreturncase 'services':this._trunk_start = cursorthis._trunk_end = cursor + 1cursor += 1this.parseService(cursor)return}this._parsed = true}parseService(cursor) {switch (this.steps[cursor]) {case 'spotify':case 'apple-music':this._branch_start = cursorthis._branch_end = cursor + 1cursor += 1break}const limit = this.steps.length - 1while (cursor < limit) {const collection = this.steps[cursor]const id = this.steps[cursor + 1]if (!(/^(?:\w+|\+)$/.test(collection) && /^(?:\d+|\+|[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12})$/i.test(id))) {break}cursor += 2this._branch_end = cursor}if (cursor < this.steps.length) {this._twig_start = cursorthis._twig_end = this.steps.length}this._parsed = true}}export { DjinList }
import { Pointer as Base } from './pointer/base.js'import { DjinList } from './pointer/djinlist.js'class Pointer extends DjinList(Base) {static create(args) {if (args instanceof Pointer) {return args}if (Array.isArray(args) || typeof args === 'string' || args instanceof String) {return new Pointer(args)}if (typeof args === 'object' && Object.prototype.hasOwnProperty.call(args, 'c') && (Object.prototype.hasOwnProperty.call(args, 'p') || Object.prototype.hasOwnProperty.call(args, 't')) && Object.prototype.hasOwnProperty.call(args, 'o')) {return this.createFromMessage(args)}}static createWithDefaults(args, { flag = '', root = '', trunk_path = '' } = {}) {const pointer = Pointer.create(args)if ((flag != '' && pointer.flag == '') || (root != '' && pointer.root == '') || (trunk_path != '' && pointer.trunk_path == '')) {return new Pointer((pointer.trunk_path != '' ? pointer.trunk_path : trunk_path) +(pointer.flag != '' ? `/${pointer.flag}` : flag != '' ? `/${flag}` : '') +(pointer.root != '' ? `/${pointer.root}` : root != '' ? `/${root}` : '') +pointer.branch_path +pointer.twig_path)}return pointer}static createFromMessage(message) {if (message.t != null && message.p == null) {message.p = message.t}if (message.o == 's' || message.o == 'u') {if (message.p == '') {return new Pointer(`${message.c}/#`)}}if (message.p == '') {return new Pointer(message.c)} else if (message.p.startsWith('/')) {return new Pointer(`${message.c}${message.p}`)} else {return new Pointer(`${message.c}/${message.p}`)}}replace(match_string, replace_string) {return new Pointer(this.topic.replace(match_string, replace_string))}queue() {if (this.flag == 'q') return thisreturn this.replace(`/${this.root}/`, `/q/${this.root}/`)}dequeue() {if (this.flag != 'q') return thisreturn this.replace('q/', '/')}concat(...steps) {steps = _.concat(this.steps, steps)return Pointer.create(steps)}changeTrunk(trunk_path) {return this.replace(this.trunk_path, trunk_path)}changeBranch(branch_path) {return this.replace(this.branch_path, branch_path)}slicePath(start, end) {return `/${this.steps.slice(start, end).join('/')}`}sliceTrunk(start, end) {return `/${this.trunk_steps.slice(start, end).join('/')}`}sliceBranch(start, end) {return `/${this.branch_steps.slice(start, end).join('/')}`}slice(begin, end) {return new Pointer(this.steps.slice(begin, end))}}const toPointer = text => {if (text[0] != '/') return textreturn text.replace(/~/g, '~0').replace(/\//g, '~1')}const toPath = text => {if (text.slice(0, 2) != '~1') return textreturn text.replace(/~1/g, '/').replace(/~0/g, '~')}export { Pointer, toPointer, toPath }
class TopicTree {constructor() {Object.defineProperties(this, {_root: {value: this.createTreeNode()}})}createTreeNode() {const node = Object.create(null)Object.defineProperties(node, {_value: {writable: true}})return node}// expensive, call rarelyall(func = null, output = [], node = this._root) {if (node._topic && node._value && (!func || func(node))) {output.push([node._topic, node._value])}_.forEach(node, (child, key) => {if (!['_value', '_topic'].includes(key)) {this.all(func, output, child)}})return output}apply(func, node = this._root) {func(node)return _.forEach(node, (child, key) => {if (!['_value', '_topic'].includes(key)) {this.apply(func, child)}})}get(topic) {const steps = topic.split('/')let left = this._rootfor (const step of steps) {left = left[step]if (left == null) {left = this.createTreeNode()break}}return left}getWithDefault(topic, value) {const steps = topic.split('/')let node = this._rootfor (const step of steps) {if (node[step] == null) {node[step] = this.createTreeNode()}node = node[step]}if (node._value == null) {node._topic = topicnode._value = value}return node}add(topic, value) {const node = this.getWithDefault(topic)node._topic = topicnode._value = value}values(topic) {const steps = topic.split('/')return this._values(this._root, steps, 0, []).reverse()}_values(node, steps, pivot, values) {if (steps.length == pivot) {if (node._value != null) {values.push(node._value)}return values}const step = steps[pivot]if (node['#'] != null) {values.push(node['#']._value)}if (node['+'] != null) {values = this._values(node['+'], steps, pivot + 1, values)}if (node[step] != null) {values = this._values(node[step], steps, pivot + 1, values)}return values}entries(topic) {const steps = topic.split('/')return this._entries(this._root, steps, 0, []).reverse()}_entries(node, steps, pivot, entries) {if (steps.length == pivot) {if (node._value != null) {entries.push([node._topic, node._value])}if (node['*'] != null) {entries.push([node['*']._topic, node['*']._value])}return entries}const step = steps[pivot]if (node['#'] != null) {entries.push([node['#']._topic, node['#']._value])}if (node['+'] != null) {entries = this._entries(node['+'], steps, pivot + 1, entries)}if (node[step] != null) {entries = this._entries(node[step], steps, pivot + 1, entries)}return entries}}export { TopicTree }
function coppice(object, prefix = '', result = {}) {for (const key in object) {const path = `${prefix}/${key}`const value = object[key]if (isTraversable(value)) {coppice(value, path, result)} else {result[path] = value}}return result}function isCoppice(value) {if (typeof value === 'object') {for (const key in value) {return key.startsWith('/')}}return false}function isBareObject(object) {return _.isPlainObject(object)}function isEmpty(object) {for (const key in object) {if (isBareObject(object)) {return false}if (Object.prototype.hasOwnProperty.call(object, key)) {return false}}return true}function isTraversable(value) {if (value == null) {return false}if (Array.isArray(value)) {return false}if (typeof value === 'object') {return true}return false}export { coppice, isCoppice, isBareObject, isEmpty, isTraversable }
/*** Test Dependencies*/import './helper'import chai, { expect } from 'chai'import sinon from 'sinon'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { ChainableGroup, Injectable, Linkable, Pathable, Preferable, Stubbable } from 'chainable.js'import { Datastore } from 'datastore.js'const datastore = new Datastore()datastore.write('', {session: {system_path: '/systems/test',room_path: '/rooms/1'},systems: {test: {setup: {rooms: {'1': {name: 'Kitchen'}}}}},setup: {rooms: {'1': {name: 'Kitchen',source_path: '/components/1/sources/1'},'2': {name: 'Breakfast Room',source_path: '/components/3/sources/1'}},components: {'1': {name: 'Apple TV',sources: {'1': {name: 'Apple TV'}}},'2': {name: 'Kaleidescape Strato',sources: {'1': {name: 'Kaleidescape'}}},'3': {name: 'Sonos CONNECT',sources: {'1': {name: 'Sonos'}}}}}})describe('ChainableGroup', () => {let groupdescribe('.link("source_path", "setup/rooms/1/source_path")', () => {before(() => {group = new ChainableGroup(datastore)})it('creates 1 chainable', () => {group.link('source_path', 'setup/rooms/1/source_path')expect(group.links).to.have.keys('source_path')expect(group.links['source_path']).to.be.instanceof(Linkable)expect(group.links['source_path'].value).to.eql('/components/1/sources/1')})describe('.link("source_name", "setup/:source_path/name")', () => {it('creates 2 chainables', () => {group.link('source_name', 'setup/:source_path/name')expect(group.links).to.have.keys('source_path', 'source_name')expect(group.links['source_name']).to.be.instanceof(Linkable)expect(group.links['source_name'].value).to.eql('Apple TV')})it('updates dependents', () => {datastore.set('/setup/rooms/1/source_path', '/components/2/sources/1')expect(group.links['source_name'].value).to.eql('Kaleidescape')})describe('.link("source_path", "setup/rooms/2/source_path")', () => {it('updates dependents', () => {group.link('source_path', 'setup/rooms/2/source_path')expect(group.links['source_path'].dependents.size).to.eql(1)expect(group.links['source_name'].value).to.eql('Sonos')})})})})describe('.link("system_path", "/session/system_path")', () => {before(() => {group = new ChainableGroup(datastore)})it('creates 1 chainable', () => {group.link('system_path', '/session/system_path')expect(group.links).to.have.keys('system_path')expect(group.links['system_path']).to.be.instanceof(Linkable)expect(group.links['system_path'].value).to.eql('/systems/test')})describe('.link("power", "/:system_path/state/rooms/1/power")', () => {it('creates power chainable', () => {group.link('power', '/:system_path/state/rooms/1/power')expect(group.links).to.have.keys('system_path', 'power')expect(group.links['power']).to.be.instanceof(Linkable)})it('relinks power chainable', () => {group.link('power', `/:system_path/state/rooms/2/power`)expect(group.links['system_path'].dependents.size).to.eql(1)})})})describe('.path("room_setup_path", ":system_path/setup/:room_path")', () => {before(() => {group = new ChainableGroup(datastore)})it('creates 1 chainable', () => {group.link('system_path', 'session/system_path')group.link('room_path', 'session/room_path')group.path('room_setup_path', ':system_path/setup/:room_path')expect(group.paths).to.have.keys('room_setup_path')expect(group.paths['room_setup_path']).to.be.instanceof(Pathable)expect(group.paths['room_setup_path'].value).to.eql('/systems/test/setup/rooms/1')})})describe('.prefer("name", ["source_name", "component_name"], "", callback)', () => {let callbackbefore(() => {group = new ChainableGroup(datastore)callback = sinon.spy()group.link('component_name', 'setup/components/4/name').link('source_name', 'setup/components/4/sources/1/name').prefer('name', ['source_name', 'component_name'], 'Source Name', callback)})it('has a default value', () => {expect(group.prefs).to.have.keys('name')expect(group.prefs['name']).to.be.instanceof(Preferable)expect(group.prefs['name'].value).to.eql('Source Name')expect(group.prefs['name'].callbacks).to.eql([callback])})it('prefers the component name', () => {datastore.set('/setup/components/4/name', 'Xfinity X1')expect(group.prefs['name'].value).to.eql('Xfinity X1')expect(callback).to.have.been.calledWith('Xfinity X1')})it('prefers the source name', () => {datastore.set('/setup/components/4/sources/1/name', 'His Cable')expect(group.prefs['name'].value).to.eql('His Cable')expect(callback).to.have.been.calledWith('His Cable')})it('falls back to the default value', () => {datastore.set('/setup/components/4/name', null)datastore.set('/setup/components/4/sources/1/name', null)expect(group.prefs['name'].value).to.eql('Source Name')expect(callback).to.have.been.calledWith('Source Name')})})describe('.link("source_path", "setup/:room_path/source_path")', () => {before(() => {group = new ChainableGroup(datastore)})it('creates 2 chainables', () => {group.link('source_path', 'setup/:room_path/source_path')expect(group.links).to.have.keys('source_path')expect(group.links['source_path']).to.be.instanceof(Linkable)expect(group.links['source_path'].value).to.be.undefinedexpect(group.stubs).to.have.keys('room_path')expect(group.stubs['room_path']).to.be.instanceof(Stubbable)expect(group.stubs['room_path'].value).to.be.undefined})describe('.prop("room_path", "/rooms/1")', () => {it('replaces 1 chainable', () => {group.prop('room_path', '/rooms/1')expect(group.props).to.have.keys('room_path')expect(group.props['room_path']).to.be.instanceof(Injectable)expect(group.props['room_path'].value).to.eql('/rooms/1')expect(group.stubs).to.be.empty})})/*describe('.link("source_name", "setup/:source_path/name")', () => {it('creates 2 chainables', () => {group.link('source_name', 'setup/:source_path/name')expect(group.links).to.have.keys('source_path', 'source_name')expect(group.links['source_name']).to.be.instanceof(Linkable)expect(group.links['source_name'].value).to.eql('Apple TV')})it('updates dependents', () => {datastore.set('/setup/rooms/1/source_path', '/components/2/sources/1')expect(group.links['source_name'].value).to.eql('Kaleidescape')})describe('.link("source_path", "setup/rooms/2/source_path")', () => {it('updates dependents', () => {group.link('source_path', 'setup/rooms/2/source_path')expect(group.links['source_path'].dependents.size).to.eql(1)expect(group.links['source_name'].value).to.eql('Sonos')})})})*/})})
/*** Test Dependencies*/import '../helper'import chai, { expect } from 'chai'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Datastore } from 'datastore/base.js'const datastore = new Datastore()describe('Datastore', () => {describe('#set', () => {it('"/a", true', () => {datastore.set('/a', true)const result = datastore._root['a']expect(result).to.equal(true)})it('"/a/b", true', () => {datastore.set('/a/b', true)const result = datastore._root['a']['b']expect(result).to.equal(true)})it('"/a/b/c", true', () => {datastore.set('/a/b/c', true)const result = datastore._root['a']['b']['c']expect(result).to.equal(true)})})describe('#delete', () => {it('"/a"', () => {datastore.set('/a', true)datastore.delete('/a')const result = datastore._rootexpect(result).to.eql({})})it('"/a/b"', () => {datastore.set('/a/b', true)datastore.delete('/a/b')const result = datastore._rootexpect(result).to.eql({})})it('"/a/b/c"', () => {datastore.set('/a/b/c', true)datastore.delete('/a/b/c')const result = datastore._rootexpect(result).to.eql({})})it('"/a2"', () => {datastore.set('/a1', true)datastore.set('/a2', true)datastore.delete('/a2')const result = datastore._rootexpect(result).to.eql({ a1: true })})})})
/*** Test Dependencies*/import '../helper'import chai, { expect } from 'chai'import sinon from 'sinon'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Linkable, Pipable, Injectable } from 'chainable.js'import { Datastore } from 'datastore.js'const datastore = new Datastore()datastore.write('', {setup: {rooms: {'1': {name: 'Kitchen',source_path: '/components/1/sources/1'}},components: {'1': {sources: {'1': {name: 'Apple TV'}}},'2': {sources: {'1': {name: 'Kaleidescape'}}}}},systems: {'1': {setup: {name: 'System 1'}},'2': {setup: {name: 'System 2'}}}})describe('datastore.chain()', () => {let chaindescribe('.prop("system_path", "systems/1")', () => {before(() => {chain = datastore.chain()})after(() => {chain.destroy()})it('creates 1 injectable', () => {chain.prop('system_path', 'systems/1')expect(chain.props).to.have.keys('system_path')expect(chain.props['system_path']).to.be.instanceof(Injectable)expect(chain.props['system_path'].value).to.eql('systems/1')})it('links with dependents', () => {chain.link('system_name', ':system_path/setup/name')expect(chain.links['system_name'].value).to.eql('System 1')})describe('.prop("system_path", "systems/2")', () => {it('updates dependents', () => {chain.prop('system_path', 'systems/2')expect(chain.props).to.have.keys('system_path')expect(chain.props['system_path']).to.be.instanceof(Injectable)expect(chain.props['system_path'].value).to.eql('systems/2')})it('updates dependents', () => {expect(chain.links['system_name'].value).to.eql('System 2')})})})describe('.link("source_path", "setup/rooms/1/source_path")', () => {before(() => {chain = datastore.chain()})after(() => {chain.destroy()})it('creates 1 chainable', () => {chain.link('source_path', 'setup/rooms/1/source_path')expect(chain.links).to.have.keys('source_path')expect(chain.links['source_path']).to.be.instanceof(Linkable)expect(chain.links['source_path'].value).to.eql('/components/1/sources/1')})describe('.link("source_name", "setup/:source_path/name")', () => {it('creates 2 chainables', () => {chain.link('source_name', 'setup/:source_path/name')expect(chain.links).to.have.keys('source_path', 'source_name')expect(chain.links['source_name']).to.be.instanceof(Linkable)expect(chain.links['source_name'].value).to.eql('Apple TV')})it('updates dependents', () => {datastore.set('/setup/rooms/1/source_path', '/components/2/sources/1')expect(chain.links['source_name'].value).to.eql('Kaleidescape')})it('relinks when called twice', () => {const callback = sinon.spy()chain.link('source_name', 'setup/:source_path/name', callback)expect(callback).not.to.have.been.calleddatastore.set('/setup/rooms/1/source_path', '/components/2/sources/2')expect(callback).to.have.been.called})})describe('.pipe("source_trunk_path", "source_path", "trunk_path")', () => {it('creates 3 chainables', () => {chain.pipe('source_trunk_path', 'source_path', value =>value.split('/').slice(0, 3).join('/'))expect(chain.pipes).to.have.keys('source_trunk_path')expect(chain.pipes['source_trunk_path']).to.be.instanceof(Pipable)expect(chain.pipes['source_trunk_path'].value).to.eql('/components/2')})describe('.pipe("source_trunk_path_length", "source_trunk_path", "trunk_path")', () => {it('creates 4 chainables', () => {chain.pipe('source_trunk_path_length', 'source_trunk_path', 'length')expect(chain.pipes).to.have.keys('source_trunk_path', 'source_trunk_path_length')expect(chain.pipes['source_trunk_path_length']).to.be.instanceof(Pipable)expect(chain.pipes['source_trunk_path_length'].value).to.eql(13)})})})})})
/*** Test Dependencies*/import '../helper'import chai, { expect } from 'chai'import sinon from 'sinon'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Pointer } from 'pointer.js'import { Datastore as Base } from 'datastore/base.js'import { Convenience } from 'datastore/convenience.js'class Datastore extends Convenience(Base) {}const data = {'/setup/rooms/1/area_paths': ['/areas/1', '/areas/2'],'/setup/rooms/2/area_paths': ['/areas/1'],'/setup/components/1/name': 'Component 1','/setup/components/1/displays/1/name': 'Display'}describe('Datastore', () => {describe('#each', () => {it('', () => {const datastore = new Datastore()const callback = sinon.spy()for (const path in data) {const value = data[path]datastore.set(path, value)}datastore.each('/setup/rooms/1/area_paths', callback)expect(callback).to.have.been.calledWith('/areas/1')expect(callback).to.have.been.calledWith('/areas/2')})})describe('#includes', () => {it('', () => {const datastore = new Datastore()const callback = sinon.spy()for (const path in data) {const value = data[path]datastore.set(path, value)}datastore.each('/setup/rooms/1/area_paths', callback)expect(callback).to.have.been.calledWith('/areas/1')expect(callback).to.have.been.calledWith('/areas/2')})})describe('#any', () => {it('', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.any('/setup/rooms')expect(result).to.be.true})})describe('#keys', () => {it('', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.keys('/setup/rooms')expect(result).to.be.eql(['1', '2'])})})describe('#branchPaths', () => {it('', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.branchPaths('/setup/components/1')expect(result).to.be.eql(['/components/1', '/components/1/displays/1'])})})describe('#push', () => {it("'/setup/rooms/2/area_paths', '/areas/2'", () => {const datastore = new Datastore()datastore.queue = sinon.spy()for (const path in data) {const value = data[path]datastore.set(path, value)}datastore.push('/setup/rooms/2/area_paths', '/areas/2')const pointer_match = sinon.match(new Pointer('/setup/rooms/2/area_paths').isEqual)expect(datastore.queue).to.have.been.calledWith(pointer_match, ['/areas/1', '/areas/2'])})})describe('#pull', () => {it("'/setup/rooms/1/area_paths', '/areas/2'", () => {const datastore = new Datastore()datastore.queue = sinon.spy()for (const path in data) {const value = data[path]datastore.set(path, value)}datastore.pull('/setup/rooms/1/area_paths', '/areas/2')const pointer_match = sinon.match(new Pointer('/setup/rooms/1/area_paths').isEqual)expect(datastore.queue).to.have.been.calledWith(pointer_match, ['/areas/1'])})})describe('#add', () => {it("'/setup/rooms', { name: 'Dining Room' }", () => {const datastore = new Datastore()datastore.queue = sinon.spy()for (const path in data) {const value = data[path]datastore.set(path, value)}datastore.add('/setup/rooms', { name: 'Dining Room' })const pointer_match = sinon.match(new Pointer('/setup/rooms/3').isEqual)expect(datastore.queue).to.have.been.calledWith(pointer_match, { name: 'Dining Room' })})})})
/*** Test Dependencies*/import '../helper'import chai, { expect } from 'chai'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Datastore as Base } from 'datastore/base.js'import { External } from 'datastore/external.js'class Datastore extends External(Base) {}const data = {'/setup/rooms/1/name': 'Kitchen','/setup/rooms/2/name': 'Breakfast Room'}describe('Datastore', () => {describe('#read', () => {let topicit('"/setup"', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.read('/setup')expect(result).to.eql({rooms: {'1': {name: 'Kitchen'},'2': {name: 'Breakfast Room'}}})})it('"/setup", { coppiced: true }', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.read('/setup', { coppiced: true })expect(result).to.eql({'/setup/rooms/1/name': 'Kitchen','/setup/rooms/2/name': 'Breakfast Room'})})it('"setup/rooms/+/name"', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.read('setup/rooms/+/name', { coppiced: true })expect(result).to.eql({'/setup/rooms/1/name': 'Kitchen','/setup/rooms/2/name': 'Breakfast Room'})})it('"/setup/rooms/+/name"', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.read('/setup/rooms/+/name', { coppiced: true })expect(result).to.eql({'/setup/rooms/1/name': 'Kitchen','/setup/rooms/2/name': 'Breakfast Room'})})it('"setup/rooms/+/+/+"', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.read('setup/rooms/+/+/+', { coppiced: true })expect(result).to.eql({})})it('"setup/rooms/+/+/#"', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore.read('setup/rooms/+/+/+', { coppiced: true })expect(result).to.eql({})})topic = 'systems/local/setup/components/+/displays/+/name'it(`"${topic}"`, () => {const yaml = requireYAML('../fixtures/system.yaml')const datastore = new Datastore()datastore.write('', yaml)const result = datastore.read(topic)expect(result).to.eql({'/systems/local/setup/components/1/displays/1/name': 'Kitchen Display','/systems/local/setup/components/2/displays/1/name': 'Breakfast Room Display'})})})describe('#_search', () => {it('"setup/rooms/1/name"', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore._search(datastore._root, ['setup', 'rooms', '1', 'name'], 0, {})expect(result).to.eql({'/setup/rooms/1/name': 'Kitchen'})})it('"setup/rooms/+/name"', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore._search(datastore._root, ['setup', 'rooms', '+', 'name'], 0, {})expect(result).to.eql({'/setup/rooms/1/name': 'Kitchen','/setup/rooms/2/name': 'Breakfast Room'})})it('"setup/rooms/#"', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}const result = datastore._search(datastore._root, ['setup', 'rooms', '#'], 0, {})expect(result).to.eql({'/setup/rooms/1/name': 'Kitchen','/setup/rooms/2/name': 'Breakfast Room'})})})describe('#write', () => {it('"/setup/rooms", { "1": { name: "Dining Room" } }', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}datastore.write('/setup/rooms', { '1': { name: 'Dining Room' } })const result = datastore.read('/setup/rooms')expect(result).to.eql({'1': {name: 'Dining Room'}})})})describe('#merge', () => {it('"/setup/rooms", { "3": { name: "Dining Room" } }', () => {const datastore = new Datastore()for (const path in data) {const value = data[path]datastore.set(path, value)}datastore.merge('/setup/rooms', { '3': { name: 'Dining Room' } })const result = datastore.read('/setup/rooms')expect(result).to.eql({'1': {name: 'Kitchen'},'2': {name: 'Breakfast Room'},'3': {name: 'Dining Room'}})})})})
/*** Test Dependencies*/import '../helper'import chai, { expect } from 'chai'import sinon from 'sinon'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Pointer } from 'pointer.js'import { Datastore as Base } from 'datastore/base.js'import { External } from 'datastore/external.js'import { PubSub } from 'datastore/pubsub.js'import { Hooks } from 'datastore/hooks.js'class Datastore extends Hooks(PubSub(External(Base))) {}describe('Datastore', () => {describe('#hooks', () => {describe('"/setup/rooms/1/name", "Kitchen"', () => {it('#subscribe "when/setup/rooms/*"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('when/setup/rooms/*', subscriber, callback, {immediate: false})datastore.set('/setup/rooms/1/name', 'Kitchen')const calls = [['when/setup/rooms/*', sinon.match(new Pointer('/setup/rooms/1').isEqual), { 1: {} }],['when/setup/rooms/*', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), { 1: { name: 'Kitchen' } }]]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "#"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('#', subscriber, callback, { immediate: false })datastore.set('/setup/rooms/1/name', 'Kitchen')const calls = [['#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen'],['#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen'],['#', sinon.match(new Pointer('/setup').isEqual), {}],['#', sinon.match(new Pointer('/setup/rooms').isEqual), {}],['#', sinon.match(new Pointer('/setup/rooms/1').isEqual), {}],['#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen'],['#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "when/#"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('when/#', subscriber, callback, {immediate: false})datastore.set('/setup/rooms/1/name', 'Kitchen')const calls = [['when/#', sinon.match(new Pointer('/setup').isEqual), {}],['when/#', sinon.match(new Pointer('/setup/rooms').isEqual), {}],['when/#', sinon.match(new Pointer('/setup/rooms/1').isEqual), {}],['when/#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "before/setup/#"', () => {const datastore = new Datastore()const subscriber = {}const callback = sinon.spy()const pointer = Pointer.create('/setup/rooms/1/name')datastore.subscribe('before/setup/#', subscriber, callback, {immediate: false})datastore.write(pointer, 'Kitchen')expect(callback).to.have.been.calledWith('before/setup/#', sinon.match(pointer.isEqual), 'Kitchen')})it('#subscribe "when/setup/#"', () => {const datastore = new Datastore()const subscriber = {}const callback = sinon.spy()const pointer = Pointer.create('/setup/rooms/1/name')datastore.subscribe('when/setup/#', subscriber, callback, {immediate: false})datastore.write(pointer, 'Kitchen')expect(callback).to.have.been.calledWith('when/setup/#', sinon.match(pointer.isEqual), 'Kitchen')})it('#subscribe "after/setup/#"', () => {const datastore = new Datastore()const subscriber = {}const callback = sinon.spy()const pointer = Pointer.create('/setup/rooms/1/name')datastore.subscribe('after/setup/#', subscriber, callback, {immediate: false})datastore.write(pointer, 'Kitchen')expect(callback).to.have.been.calledWith('after/setup/#', sinon.match(pointer.isEqual), 'Kitchen')})it('#subscribe "when/#", immediate', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.set('/setup/rooms/1/name', 'Kitchen')datastore.subscribe('when/#', subscriber, callback)const calls = [['when/#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "when/setup/#", immediate', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.write('/setup/rooms/1/name', 'Kitchen')datastore.subscribe('when/setup/#', subscriber, callback)const calls = [['when/setup/#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "async/setup/#", immediate', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.write('/setup/rooms/1/name', 'Kitchen')datastore.subscribe('async/setup/#', subscriber, callback)const calls = [['async/setup/#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#unsubscribe "when/#"', () => {const datastore = new Datastore()const subscriber = {}const callback = sinon.spy()datastore.subscribe('when/#', subscriber, callback)datastore.unsubscribe('when/#', subscriber)const map = datastore._topic_tree._root['when']['#']._valueexpect(map).to.be.emptyexpect(() => {datastore.set('/expected/to/error', false)}).to.not.throw()})it('#subscribe "when/setup/+/+"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('when/setup/+/+', subscriber, callback, {immediate: false})datastore.write('/setup/rooms/1/name', 'Kitchen')const calls = [['when/setup/+/+', sinon.match(new Pointer('/setup/rooms/1').isEqual), {}]]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "when/setup/+/#"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('when/setup/+/#', subscriber, callback, {immediate: false})datastore.write('/setup/rooms/1/name', 'Kitchen')const calls = [['when/setup/+/#', sinon.match(new Pointer('/setup/rooms/1').isEqual), {}],['when/setup/+/#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "when/setup/+/*"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('when/setup/+/*', subscriber, callback, {immediate: false})datastore.write('/setup/rooms/1/name', 'Kitchen')const calls = [['when/setup/+/*',sinon.match(new Pointer('/setup/rooms/1').isEqual),{'1': {}}],['when/setup/+/*',sinon.match(new Pointer('/setup/rooms/1/name').isEqual),{'1': {name: 'Kitchen'}}]]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})})})})
/*** Test Dependencies*/import '../helper'import chai, { expect } from 'chai'import sinon from 'sinon'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Pointer } from 'pointer.js'import { Datastore as Base } from 'datastore/base.js'import { External } from 'datastore/external.js'import { PubSub } from 'datastore/pubsub.js'class Datastore extends PubSub(External(Base)) {}describe('Datastore', () => {describe('#pubsub', () => {describe('"/setup/rooms/1/name", "Kitchen"', () => {it('#subscribe "setup/rooms/*"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('setup/rooms/*', subscriber, callback, {immediate: false})datastore.set('/setup/rooms/1/name', 'Kitchen')const calls = [['setup/rooms/*', sinon.match(new Pointer('/setup/rooms/1').isEqual), { 1: {} }],['setup/rooms/*', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), { 1: { name: 'Kitchen' } }]]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "#"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('#', subscriber, callback, {immediate: false})datastore.set('/setup/rooms/1/name', 'Kitchen')const calls = [['#', sinon.match(new Pointer('/setup').isEqual), {}],['#', sinon.match(new Pointer('/setup/rooms').isEqual), {}],['#', sinon.match(new Pointer('/setup/rooms/1').isEqual), {}],['#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "setup/#"', () => {const datastore = new Datastore()const subscriber = {}const callback = sinon.spy()const pointer = Pointer.create('/setup/rooms/1/name')datastore.subscribe('setup/#', subscriber, callback, {immediate: false})datastore.write(pointer, 'Kitchen')expect(callback).to.have.been.calledWith('setup/#', sinon.match(pointer.isEqual), 'Kitchen')})it('#subscribe "#", immediate', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.set('/setup/rooms/1/name', 'Kitchen')datastore.subscribe('#', subscriber, callback)const calls = [['#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "setup/#", immediate', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.write('/setup/rooms/1/name', 'Kitchen')datastore.subscribe('setup/#', subscriber, callback)const calls = [['setup/#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#unsubscribe "#"', () => {const datastore = new Datastore()const subscriber = {}const callback = sinon.spy()datastore.subscribe('#', subscriber, callback)datastore.unsubscribe('#', subscriber)const map = datastore._topic_tree._root['#']._valueexpect(map).to.be.emptyexpect(() => {datastore.set('/expected/to/error', false)}).to.not.throw()})it('#subscribe "setup/+/+"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('setup/+/+', subscriber, callback, {immediate: false})datastore.write('/setup/rooms/1/name', 'Kitchen')const calls = [['setup/+/+', sinon.match(new Pointer('/setup/rooms/1').isEqual), {}]]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "setup/+/#"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('setup/+/#', subscriber, callback, {immediate: false})datastore.write('/setup/rooms/1/name', 'Kitchen')const calls = [['setup/+/#', sinon.match(new Pointer('/setup/rooms/1').isEqual), {}],['setup/+/#', sinon.match(new Pointer('/setup/rooms/1/name').isEqual), 'Kitchen']]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})it('#subscribe "setup/+/*"', () => {const datastore = new Datastore()const subscriber = {}const spy = sinon.spy()const callback = (topic, pointer, value) => {spy(topic, pointer, JSON.parse(JSON.stringify(value)))}datastore.subscribe('setup/+/*', subscriber, callback, {immediate: false})datastore.write('/setup/rooms/1/name', 'Kitchen')const calls = [['setup/+/*',sinon.match(new Pointer('/setup/rooms/1').isEqual),{'1': {}}],['setup/+/*',sinon.match(new Pointer('/setup/rooms/1/name').isEqual),{'1': {name: 'Kitchen'}}]]calls.forEach((call, i) => {expect(spy.getCall(i)).to.have.been.calledWith(...call)})})})})})
/*** Test Dependencies*/import './helper'import chai, { expect } from 'chai'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Datastore } from 'datastore.js'const datastore = new Datastore()describe('Datastore', () => {describe('#set', () => {it('"/a", true', () => {datastore.set('/a', true)const result = datastore._root['a']expect(result).to.equal(true)})it('"/a/b", true', () => {datastore.set('/a/b', true)const result = datastore._root['a']['b']expect(result).to.equal(true)})it('"/a/b/c", true', () => {datastore.set('/a/b/c', true)const result = datastore._root['a']['b']['c']expect(result).to.equal(true)})})describe('#delete', () => {it('"/a"', () => {datastore.set('/a', true)datastore.delete('/a')const result = datastore._rootexpect(result).to.eql({})})it('"/a/b"', () => {datastore.set('/a/b', true)datastore.delete('/a/b')const result = datastore._rootexpect(result).to.eql({})})it('"/a/b/c"', () => {datastore.set('/a/b/c', true)datastore.delete('/a/b/c')const result = datastore._rootexpect(result).to.eql({})})it('"/a2"', () => {datastore.set('/a1', true)datastore.set('/a2', true)datastore.delete('/a2')const result = datastore._rootexpect(result).to.eql({ a1: true })})})})
---systems:2175edf8-5dac-4b9d-9ba5-8f830bef452a:setup:components:'1':name: Component 1location_paths:- /rooms/1'2':name: Component 2'3':name: Component 3displays:'1':name: Display 1
systems:local:setup:rooms:'1':name: Kitchencomponent_paths:- /components/1display_paths:- /components/1/displays/1'2':name: Breakfast Roomcomponent_paths:- /components/2display_paths:- /components/2/displays/1components:'1':name: Displaylocation_paths:- /rooms/1displays:'1':name: Kitchen Display'2':name: Displaylocation_paths:- /rooms/2displays:'1':name: Breakfast Room Display
global.window = {}global.requestAnimationFrame = function() {}global._ = require('lodash')const YAML = require('js-yaml')const fs = require('fs')const path = require('path')function _getCallerFile() {var originalFunc = Error.prepareStackTracevar callerfiletry {var error = new Error()var currentfileError.prepareStackTrace = function(error, stack) {return stack}currentfile = error.stack.shift().getFileName()while (error.stack.length) {callerfile = error.stack.shift().getFileName()if (currentfile !== callerfile) break}} catch (e) {// do nothing}Error.prepareStackTrace = originalFuncreturn callerfile}global.requireYAML = function(file_path) {const dirname = path.dirname(_getCallerFile())const yaml = fs.readFileSync(path.resolve(dirname, file_path))const json = YAML.safeLoad(yaml.toString())return json}
/*** Test Dependencies*/import '../helper'import chai, { expect } from 'chai'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Pointer } from 'pointer.js'describe('Pointer', () => {describe('ControlEnvy', () => {;[['/systems/local/setup/rooms/1/name', '/systems/local', null, 'setup', '/rooms/1', '/rooms/1', '/name', 'name'],['/systems/local/q/setup/rooms/1/name', '/systems/local', 'q', 'setup', '/rooms/1', '/rooms/1', '/name', 'name'],['/setup/components/1/switchers/1/outputs/1/name', null, null, 'setup', '/components/1', '/components/1/switchers/1/outputs/1', '/name', 'name']].forEach(test => {const [path, grove_path, flag, root, trunk_path, branch_path, twig_path, leaf] = testdescribe(`"${path}"`, () => {const pointer = new Pointer(path)it(`grove_path: ${grove_path}`, () => {expect(pointer.grove_path).to.equal(grove_path)})it(`flag: ${flag}`, () => {expect(pointer.flag).to.equal(flag)})it(`root: ${root}`, () => {expect(pointer.root).to.equal(root)})it(`trunk_path: ${trunk_path}`, () => {expect(pointer.trunk_path).to.equal(trunk_path)})it(`branch_path: ${branch_path}`, () => {expect(pointer.branch_path).to.equal(branch_path)})it(`twig_path: ${twig_path}`, () => {expect(pointer.twig_path).to.equal(twig_path)})it(`leaf: ${leaf}`, () => {expect(pointer.leaf).to.equal(leaf)})/*let path =flag === ''? `/${root}${branch_path}${twig_path}`: `/${flag}/${root}${branch_path}${twig_path}`it.skip(`toMessage('r') { o: 'r', c: '${trunk_path}', p: '${path}' }`, () => {expect(pointer.toMessage('r')).to.eql({o: 'r',c: trunk_path,p: path})})it.skip(`toMessage('w', '') { o: 'w', c: '${trunk_path}', p: '${path}', v: '' }`, () => {expect(pointer.toMessage('w', '')).to.eql({o: 'w',c: trunk_path,p: path,v: ''})})it.skip(`toMessage('s', undefined, { i: true }) { o: 'r', c: '${trunk_path}', p: '${path}', i: true }`, () => {expect(pointer.toMessage('s', undefined, { i: true })).to.eql({o: 's',c: trunk_path,p: path,i: true})})*/})})describe('pointer.grove_path =', () => {const pointer = new Pointer('/setup/rooms/1/name')it('"/systems/local"', () => {pointer.grove_path = '/systems/local'expect(pointer.grove_path).to.be.equal('/systems/local')expect(pointer.path).to.be.equal('/systems/local/setup/rooms/1/name')})it('"/systems/test"', () => {pointer.grove_path = '/systems/test'expect(pointer.grove_path).to.be.equal('/systems/test')expect(pointer.path).to.be.equal('/systems/test/setup/rooms/1/name')})it('undefined', () => {pointer.grove_path = undefinedexpect(pointer.grove_path).to.be.nullexpect(pointer.path).to.be.equal('/setup/rooms/1/name')})})describe('pointer.flag =', () => {const pointer = new Pointer('/setup/rooms/1/name')it('"q"', () => {pointer.flag = 'q'expect(pointer.flag).to.be.equal('q')expect(pointer.path).to.be.equal('/q/setup/rooms/1/name')})it('undefined', () => {pointer.flag = undefinedexpect(pointer.flag).to.be.nullexpect(pointer.path).to.be.equal('/setup/rooms/1/name')})})describe('pointer.trunk_path =', () => {const pointer = new Pointer('/setup/rooms/1/name')it('"/rooms/2"', () => {pointer.trunk_path = '/rooms/2'expect(pointer.trunk_path).to.be.equal('/rooms/2')expect(pointer.path).to.be.equal('/setup/rooms/2/name')})it('undefined', () => {pointer.trunk_path = undefinedexpect(pointer.trunk_path).to.be.nullexpect(pointer.path).to.be.equal('/setup/name')})})describe('pointer.branch_path =', () => {const pointer = new Pointer('/setup/rooms/1/name')it('"/components/1/displays/1"', () => {pointer.branch_path = '/components/1/displays/1'expect(pointer.branch_path).to.be.equal('/components/1/displays/1')expect(pointer.path).to.be.equal('/setup/components/1/displays/1/name')})it('undefined', () => {pointer.branch_path = undefinedexpect(pointer.branch_path).to.be.nullexpect(pointer.path).to.be.equal('/setup/name')})})describe('pointer.leaf =', () => {const pointer = new Pointer('/setup/rooms/1/name')it('"/setup/rooms/1/name"', () => {pointer.leaf = 'description'expect(pointer.leaf).to.be.equal('description')expect(pointer.path).to.be.equal('/setup/rooms/1/description')})it('undefined', () => {pointer.leaf = undefinedexpect(pointer.leaf).to.be.nullexpect(pointer.path).to.be.equal('/setup/rooms/1')})})/*;[[{o: 'r',c: '/systems/7360c8b7-46fe-4d3a-994e-ff07bed6aa93',p: '/setup/rooms/1/name'},'','','setup','/systems/7360c8b7-46fe-4d3a-994e-ff07bed6aa93','/rooms/1','/name','name'],[{o: 'r',c: '/systems/7360c8b7-46fe-4d3a-994e-ff07bed6aa93',p: '/setup/rooms/+/name'},'','','setup','/systems/7360c8b7-46fe-4d3a-994e-ff07bed6aa93','/rooms/+','/name','name'],[{o: 'w',c: '/systems/7360c8b7-46fe-4d3a-994e-ff07bed6aa93',p: '/setup/rooms/1/name',v: 'Room 1'},'','','setup','/systems/7360c8b7-46fe-4d3a-994e-ff07bed6aa93','/rooms/1','/name','name'],[{o: 's',c: '/systems/7360c8b7-46fe-4d3a-994e-ff07bed6aa93',p: '/setup/rooms/1/name'},'','','setup','/systems/7360c8b7-46fe-4d3a-994e-ff07bed6aa93','/rooms/1','/name','name']].forEach(test => {const [message, hook, flag, root, trunk_path, branch_path, twig_path, leaf] = testdescribe(JSON.stringify(message), () => {const pointer = Pointer.create(message)it(`hook: ${hook}`, () => {expect(pointer.hook).to.equal(hook)})it(`flag: ${flag}`, () => {expect(pointer.flag).to.equal(flag)})it(`root: ${root}`, () => {expect(pointer.root).to.equal(root)})it(`trunk_path: ${trunk_path}`, () => {expect(pointer.trunk_path).to.equal(trunk_path)})it(`branch_path: ${branch_path}`, () => {expect(pointer.branch_path).to.equal(branch_path)})it(`twig_path: ${twig_path}`, () => {expect(pointer.twig_path).to.equal(twig_path)})it(`leaf: ${leaf}`, () => {expect(pointer.leaf).to.equal(leaf)})let path =flag === ''? `/${root}${branch_path}${twig_path}`: `/${flag}/${root}${branch_path}${twig_path}`it(`toMessage('${message.o}') { o: '${message.o}', c: '${trunk_path}', p: '${path}', v: '${message.v}' }`, () => {expect(pointer.toMessage(message.o, message.v)).to.eql(message)})})})*/})})
/*** Test Dependencies*/import './helper'import chai, { expect } from 'chai'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { Pointer } from 'pointer.js'describe('Pointer', () => {describe('new Pointer', () => {it('""', () => {const pointer = new Pointer('')expect(pointer._path).to.equal('')expect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('')expect(pointer.steps).to.eql([])expect(pointer.topic).to.equal('#')expect(pointer.length).to.equal(0)expect(pointer == '').to.be.true})it('"/a"', () => {const pointer = new Pointer('/a')expect(pointer._path).to.equal('/a')expect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('/a')expect(pointer.steps).to.eql(['a'])expect(pointer.topic).to.equal('a')expect(pointer.length).to.equal(1)expect(pointer == '/a').to.be.true})it('"/a/b"', () => {const pointer = new Pointer('/a/b')expect(pointer._path).to.equal('/a/b')expect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('/a/b')expect(pointer.steps).to.eql(['a', 'b'])expect(pointer.topic).to.equal('a/b')expect(pointer.length).to.equal(2)expect(pointer == '/a/b').to.be.true})it('[]', () => {const pointer = new Pointer([])expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.eql([])expect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('')expect(pointer.steps).to.eql([])expect(pointer.topic).to.equal('#')expect(pointer.length).to.equal(0)expect(pointer == '').to.be.true})it('["a"]', () => {const pointer = new Pointer(['a'])expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.eql(['a'])expect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('/a')expect(pointer.steps).to.eql(['a'])expect(pointer.topic).to.equal('a')expect(pointer.length).to.equal(1)expect(pointer == '/a').to.be.true})it('["a", "b"]', () => {const pointer = new Pointer(['a', 'b'])expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.eql(['a', 'b'])expect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('/a/b')expect(pointer.steps).to.eql(['a', 'b'])expect(pointer.topic).to.equal('a/b')expect(pointer.length).to.equal(2)expect(pointer == '/a/b').to.be.true})it('"#"', () => {const pointer = new Pointer('#')expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.equal('#')expect(pointer.path).to.equal('')expect(pointer.steps).to.eql([])expect(pointer.topic).to.equal('#')expect(pointer.length).to.equal(0)expect(pointer == '').to.be.true})it('"a"', () => {const pointer = new Pointer('a')expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.equal('a')expect(pointer.path).to.equal('/a')expect(pointer.steps).to.eql(['a'])expect(pointer.topic).to.equal('a')expect(pointer.length).to.equal(1)expect(pointer == '/a').to.be.true})it('"a/b"', () => {const pointer = new Pointer('a/b')expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.equal('a/b')expect(pointer.path).to.equal('/a/b')expect(pointer.steps).to.eql(['a', 'b'])expect(pointer.topic).to.equal('a/b')expect(pointer.length).to.equal(2)expect(pointer == '/a/b').to.be.true})})describe('pointer.path =', () => {it('"/a"', () => {const pointer = new Pointer('')pointer.path = '/a'expect(pointer._path).to.equal('/a')expect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('/a')expect(pointer.steps).to.eql(['a'])expect(pointer.topic).to.equal('a')expect(pointer.length).to.equal(1)expect(pointer == '/a').to.be.true})it('"/a/b"', () => {const pointer = new Pointer('')pointer.path = '/a/b'expect(pointer._path).to.equal('/a/b')expect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('/a/b')expect(pointer.steps).to.eql(['a', 'b'])expect(pointer.topic).to.equal('a/b')expect(pointer.length).to.equal(2)expect(pointer == '/a/b').to.be.true})})describe('pointer.steps =', () => {it('["a"]', () => {const pointer = new Pointer([])pointer.steps = ['a']expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.eql(['a'])expect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('/a')expect(pointer.steps).to.eql(['a'])expect(pointer.topic).to.equal('a')expect(pointer.length).to.equal(1)expect(pointer == '/a').to.be.true})it('["a", "b"]', () => {const pointer = new Pointer([])pointer.steps = ['a', 'b']expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.eql(['a', 'b'])expect(pointer._topic).to.be.undefinedexpect(pointer.path).to.equal('/a/b')expect(pointer.steps).to.eql(['a', 'b'])expect(pointer.topic).to.equal('a/b')expect(pointer.length).to.equal(2)expect(pointer == '/a/b').to.be.true})})describe('pointer.topic =', () => {it('"a"', () => {const pointer = new Pointer('#')pointer.topic = 'a'expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.equal('a')expect(pointer.path).to.equal('/a')expect(pointer.steps).to.eql(['a'])expect(pointer.topic).to.equal('a')expect(pointer.length).to.equal(1)expect(pointer == '/a').to.be.true})it('"a/b"', () => {const pointer = new Pointer('#')pointer.topic = 'a/b'expect(pointer._path).to.be.undefinedexpect(pointer._steps).to.be.undefinedexpect(pointer._topic).to.equal('a/b')expect(pointer.path).to.equal('/a/b')expect(pointer.steps).to.eql(['a', 'b'])expect(pointer.topic).to.equal('a/b')expect(pointer.length).to.equal(2)expect(pointer == '/a/b').to.be.true})})})
/*** Test Dependencies*/import './helper'import chai, { expect } from 'chai'import sinonChai from 'sinon-chai'chai.use(sinonChai)import { TopicTree } from 'topic_tree.js'describe('TopicTree', () => {describe('#values', () => {it('matches "/setup/rooms/1/name"', () => {const tree = new TopicTree()tree.add('setup', 3000)tree.add('setup/rooms', 3300)tree.add('setup/rooms/1', 3330)tree.add('setup/rooms/1/name', 3333)tree.add('#', 1000)tree.add('setup/rooms/#', 3310)tree.add('setup/rooms/+/name', 3323)tree.add('setup/rooms/+', 3320)tree.add('setup/rooms/+/power', -3323)tree.add('setup/+/+/name', 3223)const values = tree.values('setup/rooms/1/name')expect(values).to.eql([3333, 3323, 3310, 3223, 1000])})})describe('#entries', () => {it('matches "/setup/rooms/1/name"', () => {const tree = new TopicTree()tree.add('setup', 3000)tree.add('setup/rooms', 3300)tree.add('setup/rooms/1', 3330)tree.add('setup/rooms/1/name', 3333)tree.add('#', 1000)tree.add('setup/rooms/#', 3310)tree.add('setup/rooms/+/name', 3323)tree.add('setup/rooms/+', 3320)tree.add('setup/rooms/+/power', -3323)tree.add('setup/+/+/name', 3223)const entries = tree.entries('setup/rooms/1/name')expect(entries).to.eql([['setup/rooms/1/name', 3333],['setup/rooms/+/name', 3323],['setup/rooms/#', 3310],['setup/+/+/name', 3223],['#', 1000]])})})})
{"name": "@djinlist/datastore-client","version": "3.0.0","type": "module","license": "UNLICENSED","private": true,"main": "src/index.js","dependencies": {"@djinlist/datastore": "0.0.0","lodash": "^4.17.15"}}
import { Pointer } from '@djinlist/datastore'import { v4 as uuidv4 } from 'uuid'import net from 'net'const RECONNECT_TIME = 5 // secondsclass IPCClient {constructor(socket_path, client_id, { subscriptions = [], publications = [] } = {}) {this.name = socket_path.split('/').slice(-1)this.client_id = client_idthis.destroyed = falsethis.socket_path = socket_paththis.readers = {}this.subscriptions = subscriptions || []this.publications = publications || []this.debounceConnect = _.debounce(this.connect.bind(this), 1000, {leading: true,trailing: false})this.debounceConnect()}emptyPromise() {return new Promise(resolve => resolve(undefined))}subscribe(pointer, immediate = true) {pointer = Pointer.create(pointer)// console.log(`SUBSCRIBE (R): ${pointer.path} ${immediate}`)this.subscriptions = _.union(this.subscriptions, [pointer.topic])return immediate ? this.read(pointer) : this.emptyPromise()}unsubscribe(pointer) {pointer = Pointer.create(pointer)// console.log(`UNSUBSCRIBE (R): ${pointer.path}`)this.subscriptions = _.without(this.subscriptions, pointer.topic)}async read(pointer) {pointer = Pointer.create(pointer)// console.log(`READ (R): ${pointer.path}`)return new Promise(resolve => {message = {o: 'r',p: pointer.path,id: new uuidv4()}this.readers[uuid] = resolvethis.send(message)})}async onRead(message) {const resolver = this.readers(message.id)if (!resolver) returnpointer = Pointer.create(message.p)resolver(pointer, message.v)}async write(pointer, value) {pointer = Pointer.create(pointer)// console.log(`WRITE: ${pointer.path}`)const message = {o: 'w',p: pointer.path,v: value}this.send(message)}async merge(pointer, value) {pointer = Pointer.create(pointer)// console.log(`MERGE: ${pointer.path}`)const message = {o: 'm',p: pointer.path,v: value}this.send(message)}async destroy(pointer) {pointer = Pointer.create(pointer)// console.log(`DESTROY: ${pointer.path}`)const message = {o: 'd',p: pointer.path}this.send(message)}/** Connect*/delayConnect() {if (this.connect_timeout) returnconsole.log(`Reconnecting in ${RECONNECT_TIME}s.`)this.connect_timeout = setTimeout(() => {this.debounceConnect()this.connect_timeout = null}, RECONNECT_TIME * 1000)}connect() {if (this.socket && (this.socket.connecting || this.socket.connected)) returntry {console.log(`connect() ${this.socket_path}`)this.socket = net.createConnection(this.socket_path, () => {console.log('Connected')this.socket.connected = truethis.subscriptions.forEach(subscription => {try {const topic = subscription[0]const immediate = subscription[1]this.subscribe(topic, immediate)} catch (e) {console.log(`Error subscribing to ${subscription}:\n${e.stack}`)console.log('Subscriptions should be `[["topic/1", immediate], ["topic/2", immediate]]')}})this.publications.forEach(publication => {try {this.subscribe(publication)} catch (e) {console.log(`Error subscribing to ${publication}:\n${e.stack}`)console.log('Publications should be `["topic/1", "topic/2"]')}})})this.socket.data_buffer = new Buffer.alloc(0)this.socket.on('data', data => {this.socket.data_buffer = Buffer.concat([this.socket.data_buffer, data])this.readBuffer()})this.socket.on('error', e => {console.log(`Error in socket:\n${e.stack}`)})this.socket.on('close', erred => {console.log('Closed')this.socket.connected = falsetry {this.socket.destroy()} catch (e) {console.log(`Error destroying socket:\n${e.stack}`)}if (erred) {console.log('Socket closed due to an error')}this.delayConnect()})} catch (e) {console.log(`Error connecting to socket:\n${e.stack}`)this.delayConnect()}}/** Send*/send(message) {const payload = JSON.stringify(message)// console.log(`#send() ${payload}`)let header = []for (let i = 0; i <= 3; i++) {header.push(Math.floor(Buffer.byteLength(payload, 'utf-8') / Math.pow(256, 3 - i)) % 256)}const buffer = Buffer.concat([Buffer.from(header), Buffer.from(payload)])try {this.socket && this.socket.write(buffer)} catch (e) {console.log(e)}}/** Receive*/getMessageLength(buffer) {// console.log(`#getMessageLength() ${readable(buffer.slice(0, 4).toString())}`)return buffer.readUInt32BE(0)}readBuffer() {if (this.socket.reading) returnthis.socket.reading = truewhile (this.socket.data_buffer.length >= 4) {const length = this.getMessageLength(this.socket.data_buffer)if (this.socket.data_buffer.length < 4 + length) breakconst encoded = this.socket.data_buffer.slice(4, length + 4)this.socket.data_buffer = this.socket.data_buffer.slice(length + 4)try {const message = JSON.parse(encoded)const pointer = Pointer.create(message)if (message.o !== 'p' && pointer.leaf !== 'last_seen' && pointer.leaf !== 'log') {console.log(`#parse ${encoded}`)}this.parse(message)} catch (e) {console.log(`Error processing message:\n${e.stack}`)console.log(` Buffer: '${readable(this.socket.data_buffer.toString())}'`)console.log(` Clearing buffer`)this.socket.data_buffer = new Buffer.alloc(0)}}this.socket.reading = false}parse(message) {switch (message.o) {case 'r':breakthis.onRead(message)case 'p':this.onPublish(message)break}}/** Cleanup*/cleanup() {this.socket.destroy()this.destroyed = true}}export { IPCClient }
push(path, value, { force = false, silent = false, duplicates = false } = {}) {const array = datastore.get(path)?.value ?? []
push(path, value, { force = false, silent = false, duplicates = false, queue = true} = {}) {let array = datastore.get(path)?.value ?? []
button.big {border: none;border-radius: 1rem;text-align: center;color: hsla(100, 25%, 100%, 0.8);background-color: coral;margin: 1rem auto;h3 {font-weight: bold;text-align: center;margin: 0.5em;}}
<div class="side-scroll"><div class="side-scroll-child"><div class="side-scroll-panel"><h2>What is Djinlist?</h2><p>Djinlist, plain and simple, is built to keep the party going.</p><p>Sick and tired of Ashlee playing "Get Lucky" for the third time tonight? Democratize your party with Djinlist. We take the stress out of picking the music for your party or event.</p><h2>Mission</h2><p>We want to provide an alternative to a DJ or a difficult-to-manage play list for your party. You should spend your time with your guests, not dealing with the music. We aim to be your peace of mind that one thing won't go wrong on your big day.</p></div>
<div class="side-scroll"><div class="side-scroll-child"><div class="side-scroll-panel"><h2>What is Djinlist?</h2><p>Djinlist, plain and simple, is built to keep the party going.</p><p>Sick and tired of Ashlee playing "Get Lucky" for the third time tonight? Democratize your party with Djinlist. We take the stress out of picking the music for your party or event.</p><h2>Mission</h2><p>We want to provide an alternative to a DJ or a difficult-to-manage play list for your party. You should spend your time with your guests, not dealing with the music. We aim to be your peace of mind that one thing won't go wrong on your big day.</p>
<div class="side-scroll-child"><div class="side-scroll-panel"><h2>Djinlist Public Weekly lists</h2><p>Sign-in and vote on your favorite Friday night track with others all over the world or in your city. Every Friday the Djinlist Official account publishes weekly playlists on your favorite streaming service. Vote on your party playlist for free and spend your night enjoying the tunes instead of picking the music.</p><h2>Live Events</h2><p>Every Saturday night vote for the top twenty tracks of the week in your timezone</p></div>
</div><div class="side-scroll-child"><div class="side-scroll-panel"><h2>Djinlist Public Weekly lists</h2><p>Sign-in and vote on your favorite Friday night track with others all over the world or in your city. Every Friday the Djinlist Official account publishes weekly playlists on your favorite streaming service. Vote on your party playlist for free and spend your night enjoying the tunes instead of picking the music.</p><h2>Live Events</h2><p>Every Saturday night vote for the top twenty tracks of the week in your timezone</p>
<div class="side-scroll-child"><div class="side-scroll-panel"><h2>Djinlist Private (Coming Soon)</h2><p>We love music and we love parties. There are lot's of great DJ's out there and we recommend using them, but not everyone can afford a private DJ. Are you worried the only DJ in town is going to spend the night hitting on your bridesmaids instead of queueing up the father-daughter dance? Don't trust your maid of honor to handle your Spotify playlist? Use us instead!</p></div>
</div><div class="side-scroll-child"><div class="side-scroll-panel"><h2>Djinlist Private (Coming Soon)</h2><p>We love music and we love parties. There are lot's of great DJ's out there and we recommend using them, but not everyone can afford a private DJ. Are you worried the only DJ in town is going to spend the night hitting on your bridesmaids instead of queueing up the father-daughter dance? Don't trust your maid of honor to handle your Spotify playlist? Use us instead!</p>
<div class="side-scroll-child"><div class="side-scroll-panel"><h2>Supported Services</h2><p>While we are in our current proof-of-concept design cycle, we support or have plans to support the following music services:</p><ul><li>Spotify</li><li>Apple Music (planned)</li></ul><p>We plan to integrate more music services once proof of concept is complete and we can turn this passion into a living.</p></div>
</div><div class="side-scroll-child"><div class="side-scroll-panel"><h2>Supported Services</h2><p>While we are in our current proof-of-concept design cycle, we support or have plans to support the following music services:</p><ul><li>Spotify</li><li>Apple Music (planned)</li></ul><p>We plan to integrate more music services once proof of concept is complete and we can turn this passion into a living.</p>
<Global segment="home"><div class="grid-1 fill" ><div class="panel-opaque"><h1 class="title-big ctr">Djiny</h1><h2 class="ctr">Playlists with Friends</h2>{#if $token && expired === false}<div class="btn btn-big ctr" on:click|preventDefault={explore}><h2>Explore</h2></div>{:else if $token}<p>Refreshing Music Service Session...</p>{:else if $name}<div class="ctr"><div class="grid grid-responsive-columns-1fr"><p>Link a Service:</p><div class="grid-1 btn btn-rnd" on:click|preventDefault={() => authorize('spotify')}><Icon name="spotify"/></div><div class="grid-1 btn btn-rnd" on:click|preventDefault={() => authorize('apple')}><Icon name="apple"/></div></div><p class="drk-txt">Logged in as {$name}</p>
<div class="grid-1 fill" ><div class="panel-opaque grid-1"><h1 class="title-big ctr">Djiny</h1><h2 class="ctr">Playlists with Friends</h2>{#if $token && expired === false}<button class="primary big" on:click|preventDefault={explore}><h2>Explore</h2></button>{:else if $token}<p>Refreshing Music Service Session...</p>{:else if $name}<div class="ctr"><div class="grid grid-responsive-columns-1fr"><p>Link a Service:</p><button class="circle" on:click|preventDefault={() => authorize('spotify')}><Icon name="faSpotify" type="brands"/></button><button class="circle" on:click|preventDefault={() => authorize('apple')}><Icon name="faApple" type="brands"/></button>
{:else}<div class="btn btn-big" on:click|preventDefault={() => access(true)}><h2>Sign Up</h2></div><div class="btn btn-big" on:click|preventDefault={() => access(false)}><h2>Log In</h2></div>{/if}</div>
<p class="drk-txt">Logged in as {$name}</p></div>{:else}<button class="primary big" on:click|preventDefault={() => access(true)}><h2>Sign Up</h2></button><button class="secondary big" on:click|preventDefault={() => access(false)}><h2>Log In</h2></button>{/if}
<button class="invisible" id="track" on:click|preventDefault={() => search("track")}><h2>More Songs</h2><Icon name="caret_right"></Icon></button>
<button class="invisible" id="track" on:click|preventDefault={() => search("track")}><h2>More Songs</h2><Icon name="faCaretRight"></Icon></button>
<button class="invisible" id="artist" on:click|preventDefault={() => search("artist")}><h2>More Artists</h2><Icon name="caret_right"></Icon></button>
<button class="invisible" id="artist" on:click|preventDefault={() => search("artist")}><h2>More Artists</h2><Icon name="faCaretRight"></Icon></button>
<button class="invisible" id="album" on:click|preventDefault={() => search("album")}><h2>More Albums</h2><Icon name="caret_right"></Icon></button>
<button class="invisible" id="album" on:click|preventDefault={() => search("album")}><h2>More Albums</h2><Icon name="faCaretRight"></Icon></button>
<button class="invisible" id="playlist" on:click|preventDefault={() => search("playlist")}><h2>More Playlists</h2><Icon name="caret_right"></Icon></button>
<button class="invisible" id="playlist" on:click|preventDefault={() => search("playlist")}><h2>More Playlists</h2><Icon name="faCaretRight"></Icon></button>
<div class="panel"><h2>{$name}</h2><label>Created on:<p>{created_on}</p></label><label>Email:<p>{$email}</p></label><p>Services:</p><div class="panel panel_horizontal"><div><div class="btn button-circle height-min-content" class:glow={spotify_active} on:click|preventDefault={() => authorize('spotify')}><div class="grid-1 btn-rnd"><Icon name="spotify"/></div><div>Spotify</div>
<div class="panel"><h2>{$name}</h2><label>Created on:<p>{created_on}</p></label><label>Email:<p>{$email}</p></label><p>Services:</p><div class="panel panel_horizontal"><div><button class="button-circle height-min-content" class:glow={spotify_active} on:click|preventDefault={() => authorize('spotify')}><Icon name="faSpotify" type="brands"/><div>Spotify
</div><div><div class="btn button-circle height-min-content" class:glow={apple_active} on:click|preventDefault={() => authorize('apple')}><div class="grid-1 btn-rnd"><Icon name="apple"/></div><div>Apple</div>
</button></div><div><button class="button-circle height-min-content" class:glow={apple_active} on:click|preventDefault={() => authorize('apple')}><Icon name="faApple" type="brands"/><div>Apple
<p>Select Active Event:<Select><select on:blur={changeEvent} bind:value={$user_event_id}><option value={undefined}>None</option>{#each $events as event_path}<Event {event_path} is_private={$private_events.includes(event_path)}></Event>
</div>{#if has_events}<!-- svelte-ignore a11y-label-has-associated-control --><p>Select Active Event:<Theme><select on:change={change} bind:value={$user_event_id}>{#each user_events as event_path}<option value={event_path}><Name path={event_path}></Name></option>
$: user_event_id = datastore.svelte(`state/users/${$user_id}/event_id`)$: spotify_token = datastore.svelte(`state/users/${$user_id}/services/spotify/token`)
$: spotify_token = datastore.svelte(`state/users/${$user_id}/services/spotify/client/token`)
$: events = datastore.svelte(`setup/events/+`, [], pointer => pointer.path.slice(6))$: private_events = datastore.svelte(`setup/events/+/private`, [], pointer => pointer.path.slice(6, -8))
$: owned_events = datastore.svelte(`state/users/${$user_id}/owned`, [])$: followed_events = datastore.svelte(`state/users/${$user_id}/followed`, [])$: user_events = union($owned_events, $followed_events)$: has_events = user_events.length > 0$: user_event_id = datastore.svelte(`state/users/${$user_id}/event_id`)
const changeEvent = ({ target }) => {if ($user_event_id) unsubscribe(`state/events/${$user_event_id}/#`)if (target.value) subscribe(`state/events/${target.value}/#`, true)
function change({ target }) {const { value } = targetif ($user_event_id) { unsubscribe(`state/events/${$user_event_id}/#`) }if (value) { subscribe(`state/events/${value}/#`, true) }
import Global from '$lib/components/global.svelte'import Select from '$lib/components/slots/select.svelte'import Event from './_account/event.svelte'
import Theme from '$lib/components/slots/select.svelte'import Name from '$lib/components/name.svelte'
<div class="btn btn-big width-40" on:click|preventDefault={login}><h3>Log In</h3></div><div class="btn btn-big width-40" on:click|preventDefault={submit}><h3>Sign Up</h3></div>
<button class="secondary big width-40" on:click|preventDefault={login}><h3>Log In</h3></button><button class="primary big width-40" on:click|preventDefault={submit}><h3>Sign Up</h3></button>
<div class="btn btn-big width-40" on:click|preventDefault={signup}><h3>Sign Up</h3></div><div class="btn btn-big width-40" on:click|preventDefault={submit}><h3>Log In</h3></div>
<button class="secondary big width-40" on:click|preventDefault={signup}><h3>Sign Up</h3></button><button class="primary big width-40" on:click|preventDefault={submit}><h3>Log In</h3></button>
<div class="wrapper"><h2>Select Playlist</h2><div class="scroller">{#each $event_paths as event_path }<Name {event_path} on:select={selectEvent}></Name>{:else}<div class="event">No events detected</div>{/each}</div>
<h2>Select Playlist</h2><div class="scroller">{#each $event_paths as event_path }<Name {event_path} on:click={selectEvent}></Name>{:else}<div class="event">No events detected</div>{/each}
$: user_id = datastore.svelte('session/user_id')$: event_paths = datastore.svelte(`setup/users/${$user_id}/events`, [])$: has_events = $event_paths.length > 0$: user_event_id = datastore.svelte(`state/users/${$user_id}/event_id`)
<div class="buttons"><h2>Manage Events</h2></div>
</script><div class="buttons"><button class:active={tab === 'manage'} on:click={() => toggleTab('manage')}>Manage Events</button><button class:active={tab === 'find'} on:click={() => toggleTab('find')}>Find Events</button>{#if has_events}<Theme><select on:change={change} bind:value={user_event_id}>{#each $event_paths as event_path}<option value={event_path}><Name path={event_path}></Name></option>{/each}</select></Theme>{:else}<p>No Linked Events</p>{/if}</div>{#if loading}{#await loading then { default: component } }<svelte:component this={component}></svelte:component>{/await}{/if}<style lang="scss">.buttons {height: min-content;}</style>
onDestroy(() => datastore.set('/session/show_owned_events', null))</script>
{#if can_create}<button on:click|preventDefault>Create</button>{:else}<p>User event limit reached</p><p>Extend your limit</p>{/if}
<div class="name"><p>{$name}</p></div><div class="edit"><button on:click|preventDefault={openModal}><Icon name="faEdit"></Icon></button></div><div class="description"><p>{$description}</p></div><div class="info"><!-- svelte-ignore a11y-label-has-associated-control --><label>time<h1>{display_line_1}</h1><p>{display_line_2}</p></label><!-- svelte-ignore a11y-label-has-associated-control --><label>users<h1>{$users.length}</h1></label></div>
{#if show_modal}<Modal on:close={hideModal}><Text event_path="{path}" attribute="name"></Text><!-- svelte-ignore a11y-label-has-associated-control --><input type="date" on:change={onDateChange} value={date}/><input type="time" on:change={onTimeChange} value={time}/><!-- svelte-ignore a11y-label-has-associated-control --><label>Description<TextArea event_path="{path}" attribute="description" placeholder="No Description"></TextArea></label></Modal>{/if}
$: user_id = datastore.svelte('session/user_id')$: user_owned_events = datastore.svelte(`setup/users/${user_id}/owned`, [])$: number_of_user_owned_events = $user_owned_events.length$: user_type = datastore.svelte(`setup/users/${user_id}/type`, 'free')
export let path
$: event_limit = {free: 1,basic: 3,business: 5}[$user_type]$: can_create = number_of_user_owned_events <= event_limit
import { DateTime } from 'luxon'import Modal from '$lib/components/modal.svelte'import Icon from '$lib/components/Icon.svelte'import Text from '../utils/text.svelte'import TextArea from '../utils/text_area.svelte'$: name = datastore.svelte(`setup${path}/name`)$: description = datastore.svelte(`setup${path}/description`, 'No Description')$: datestore = datastore.svelte(`setup${path}/time`)$: console.log({datestore: $datestore})$: date_time = $datestore ? DateTime.fromMillis($datestore) : null$: date = date_time ? date_time.toFormat('yyyy-MM-dd') : undefined$: time = date_time ? date_time.toFormat('h:mm') : '12:00'$: display_line_1 = date_time ? date_time.toFormat("h:mma") : "N/A"$: display_line_2 = date_time ? date_time.toFormat("MMM dd, yyyy") : ""$: users = datastore.svelte(`setup${path}/users`, [])function onDateChange({ target }) {date = target.valueupdate()}function onTimeChange({ target }) {time = target.valueupdate()}function update() {if (typeof date !== 'undefined' && typeof time !== 'undefined') {const _date_time = DateTime.fromFormat(`${date} ${time}`, 'YYYY-MM-DD hh:mm')datastore.queue(`setup${path}/time`, _date_time.ts)}}let show_modal = falseconst openModal = () => show_modal = trueconst hideModal = () => show_modal = false
<style lang="scss">.panel {padding: 0;display: grid;grid-template: 'name name edit' 'info info info' 'description description description' ;grid-template-columns: 1fr auto;border-radius: 0;.name {padding: 0.5rem;background: hsla(0, 0%, 100%, 0.1);grid-area: name;border-bottom: 2px solid rgb(50, 50, 50);}.edit {padding: 0.5rem;background: hsla(0, 0%, 100%, 0.1);grid-area: edit;border-left: 2px solid rgb(50, 50, 50);border-bottom: 2px solid rgb(50, 50, 50);align-content: center;}.description {grid-area: description;border-bottom: 2px solid rgb(50, 50, 50);padding: 0.5rem;}.info {grid-area: info;display: grid;grid-auto-flow: column;label {padding: 0.5rem;h1 {font-size: 50px;text-align: center;margin-bottom: 0.125em;}p {margin-top: 0;text-align: center;}&:not(:first-child) {border-left: 2px solid rgb(50, 50, 50);}}}}</style>
{#each $followed_events as followed_event}<div class="panel"><FollowedEvent id={followed_event}></FollowedEvent>
<div class="panel"><div class="name"><p>{$name}</p></div><div class="edit"><button on:click|preventDefault={onClick}><Icon name="faMinusSquare"></Icon></button></div><div class="description"><p>{$description}</p></div><div class="info"><!-- svelte-ignore a11y-label-has-associated-control --><label>time<h1>{display_line_1}</h1><p>{display_line_2}</p></label><!-- svelte-ignore a11y-label-has-associated-control --><label>users<h1>{$users.length}</h1></label>
import FollowedEvent from './followed_event.svelte'
export let event_pathimport { subscribe } from '$lib/subscribe.js'import { onMount } from 'svelte'import Icon from '$lib/components/Icon.svelte'$: name = datastore.svelte(`setup${event_path}/name`)$: description = datastore.svelte(`setup${event_path}/description`, 'No Description')$: datestore = datastore.svelte(`setup${event_path}/time`)$: date_time = $datestore ? DateTime.fromMillis($datestore) : null$: display_line_1 = date_time ? date_time.toFormat("h:mma") : "N/A"$: display_line_2 = date_time ? date_time.toFormat("MMM dd, yyyy") : ""$: users = datastore.svelte(`setup${event_path}/users`, [])
$: user_id = datastore.svelte('session/user_id')$: followed_events = datastore.svelte(`setup/users/${user_id}/followed`, [])
$: user_id = datastore.svelte(`session/user_id`)onMount(() => {subscribe(`setup${event_path}/#`)})function onClick() {datastore.pull(`/state/users/${$user_id}/followed`, event_path)datastore.pull(`/setup${event_path}/users`, event_path)}
<style lang="scss">.panel {padding: 0;display: grid;grid-template: 'name name edit' 'info info info' 'description description description' ;grid-template-columns: 1fr auto;border-radius: 0;.name {padding: 0.5rem;background: hsla(0, 0%, 100%, 0.1);grid-area: name;border-bottom: 2px solid rgb(50, 50, 50);}.edit {padding: 0.5rem;background: hsla(0, 0%, 100%, 0.1);grid-area: edit;border-left: 2px solid rgb(50, 50, 50);border-bottom: 2px solid rgb(50, 50, 50);align-content: center;}.description {grid-area: description;border-bottom: 2px solid rgb(50, 50, 50);padding: 0.5rem;}.info {grid-area: info;display: grid;grid-auto-flow: column;label {padding: 0.5rem;h1 {font-size: 50px;text-align: center;margin-bottom: 0.125em;}p {margin-top: 0;text-align: center;}&:not(:first-child) {border-left: 2px solid rgb(50, 50, 50);}}}}</style>
<input type="text" bind:value={search}>
<label style="grid-area: find;" on:click|stopPropagation>Add a public event<input type="text" bind:value={search} on:focus={onFocus}>{#if focused}<div class="results">{#if search.length > 0}{#each $event_paths as event_path}<Filter topic="setup{event_path}/name" filter={search} flags='gi'><Event {event_path} on:close={clear}></Event></Filter>{/each}{/if}</div>{/if}</label>
<style global type="text/scss" >main {box-sizing: border-box;width: 100vw;height: calc(100vh - 5rem);margin: 0;padding: 2em;overflow-y: scroll;}:global(body) {background: radial-gradient(circle at top right, hsl(240, 20%, 0%), transparent), radial-gradient(circle at bottom right, hsl(240, 0%, 0%), hsla(240, 100%, 20%, 0.9));margin: 0;}:global(.grid-1) {display: grid;grid-template: 1 1fr / 1 1fr;align-items: center;justify-items: center;}:global(.flex-row) {display: flex;flex-direction: row;align-content: center;justify-content: space-around;}:global(.side-scroll) {display: grid;grid-auto-flow: column;grid-auto-columns: 80vw;width: 110vw;overflow-x: scroll;scroll-snap-type: x mandatory;justify-items: center;margin-left: -10vw;padding-left: 5vw;&::before {content: '';width: 20vw;}&::after {content: '';width: 20vw;}}:global(.side-scroll-child) {width: 80vw;justify-self: center;scroll-snap-align: center;align-content: center;display: grid;}:global(.side-scroll-panel) {background: var(--bg-shade);padding: 2rem;border-radius: 0.5rem;opacity: 0.6;transform: scale(0.9);height: 70vh;overflow-y: scroll;}@media (prefers-reduced-motion: no-preference) {:global(.side-scroll-panel) {transition: opacity 0.5s ease, transform 0.5s ease;}}:global(.side-scroll-panel-transition) {opacity: 1;transform: none;}:global(.fill-w) {width: 100%;}:global(.fill-h) {height: 100%;}:global(.fill) {width: 100%;height: 100%;}:global(.title-big) {font-weight: 700;font-size: 2.8em;text-transform: uppercase;}:global(.lgt-txt) {color: hsla(100, 25%, 80%);}:global(.drk-txt) {color: hsla(100, 0%, 40%);}:global(h1) {color: hsla(100, 25%, 80%);}:global(h2) {color: hsla(100, 25%, 80%);}:global(h3) {color: hsla(100, 25%, 80%);}:global(p) {color: hsla(100, 25%, 80%);}:global(.panel) {padding: 1rem;background: var(--bg-shade);border-radius: 0.5rem;}:global(svg) {width: auto;height: 2rem;fill: var(--text-primary);}:global(.opaque-8) {opacity: 0.8;}:global(.panel-opaque) {padding: 2rem;background: var(--bg-shade);border-radius: 0.5rem;opacity: 0.8;}:global(.pad-top-05em) {padding-top: 0.5em;}:global(.pad-btm-05em) {padding-bottom: 0.5em;}:global(.pad-top-btm-05em) {padding-top: 0.5em;padding-bottom: 0.5em;}:global(.ctr) {margin: auto auto;text-align: center;}:global(.btn) {transition: all .2s ease-in-out;&:active {transform: scale(0.9) !important;}&:hover {cursor: pointer;transform: scale(1.1);}}:global(.grid-flow-column) {display: grid;grid-auto-flow: column;}:global(.height-min-content) {height: min-content;}:global(.grid-align-center) {align-items: center;}:global(.grid-justify-center) {justify-items: center;}:global(.width-100) {width: 100%;}:global(.width-80) {width: 80%;}:global(.width-60) {width: 60%;}:global(.width-40) {width: 40%;}.btn-bg {background-color: hsla(0, 0%, 45%, 0.5);}:global(.btn-bg) {@extend .btn-bg;}:global(.text-ctr) {text-align: center;}:global(.text-rgt) {text-align: right;}:global(.text-lft) {text-align: left;}:global(.btn-big) {@extend .btn-bg;margin: 1rem auto;color: var(--text-primary);text-align: center;border: none;border-radius: 1rem;:global(h2), :global(h3) {padding: 0.2em;font-weight: bold;text-align: center;}}:global(.grid) {display: grid;}:global(.grid-responsive-columns-1fr) {grid-template-columns: repeat(auto-fit, minmax(1rem, 1fr));}:global(.button-circle) {display: grid;justify-content: center;text-align: center;width: max-content;grid-template-rows: min-content max-content;}:global(.panel_horizontal) {display: grid;grid-auto-flow: column;gap: 3rem;grid-auto-columns: min-content;}:global(.btn-rnd) {@extend .btn-bg;margin: 1rem auto;color: var(--text-primary);text-align: center;border: none;width: 3em;height: 3em;border-radius: 50%;}:global(.title-row) {display: grid;grid-template: 4rem / 4rem auto;align-items: center;:global(svg) {justify-self: center;z-index: 200;}:global(h1) {margin: 0;z-index: 200;}}</style>
<style global src="./global.scss"></style>
if (this.user && this.token) this.saveToken(this.user, this.token)
if (!this.user || !this.token) {return}const expires = datastore.read(`/state/users/${this.user}/services/spotify/client/expiry`)console.log({expires, test: +new Date / 1000, test2: expires < +new Date() / 1000})if (expires < +new Date() / 1000) {datastore.write(`/action/services/spotify/auth_user`, +new Date())} else {console.log(`Token Accepted ${value}`)this.saveToken(this.user, this.token)setTimeout(this.authorizeUser, ((expires * 1000) - new Date) * 0.9)}
<button class="btn btn-big" on:click|preventDefault={confirm}><h2>Confirm</h2></button><button class="btn btn-big" on:click|preventDefault={cancel}><h2>Cancel</h2></button>
<button class="primary big" on:click|preventDefault={confirm}><h2>Confirm</h2></button><button class="secondary big" on:click|preventDefault={cancel}><h2>Cancel</h2></button>
<li on:click={() => goto('/client/')}><span aria-current='{segment === undefined ? "page" : undefined}' class="btn"><Icon name="igloo"/>Home</span></li><li on:click={() => goto('/client/info')}><span aria-current='{segment === "info" ? "page" : undefined}' class="btn"><Icon name="info"/>Djinlist</span></li>
<li on:click={() => goto('/client/')}><span aria-current='{segment === "home" ? "page" : undefined}' class="btn"><Icon name="faIgloo"/>Home</span></li><li on:click={() => goto('/client/info')}><span aria-current='{segment === "info" ? "page" : undefined}' class="btn"><Icon name="faInfo"/>Djinlist</span></li>
<li on:click={toggleLogoutPrompt}><span class="btn"><Icon name="sign-out"/>Log Out</span></li><li on:click={() => goto('/client/tallies')}><span aria-current='{segment === "tallies" ? "page" : undefined}' class="btn"><Icon name="chart"/>Charts</span></li><li on:click={() => goto('/client/account')}><span aria-current='{segment === "account" ? "page" : undefined}' class="btn"><Icon name="user"/>User</span></li><li on:click={() => goto('/client/explore')}><span aria-current='{segment === "explore" ? "page" : undefined}' class="btn"><Icon name="compass"/>Explore</span></li>
<li on:click={toggleLogoutPrompt}><span class="btn"><Icon name="faSignOutAlt"/>Log Out</span></li><li on:click={() => goto('/client/tallies')}><span aria-current='{segment === "tallies" ? "page" : undefined}' class="btn"><Icon name="faChartBar"/>Charts</span></li><li on:click={() => goto('/client/account')}><span aria-current='{segment === "account" ? "page" : undefined}' class="btn"><Icon name="faUser"/>User</span></li><li on:click={() => goto('/client/explore')}><span aria-current='{segment === "explore" ? "page" : undefined}' class="btn"><Icon name="faCompass"/>Explore</span></li>
<div class="grid-1 blur"><div class="grid panel-opaque width-40"><svelte:component this={promptComponents[$prompt]} />
{#await get('prompts', $prompt) then { default: prompt_component } }<div class="grid-1 blur"><div class="grid panel-opaque width-40"><svelte:component this={prompt_component} /></div>
<svelte:component this={Home} {segment} />{#each nav as child}{#if navComponents[child]}<svelte:component this={navComponents[child]} {segment} />{/if}
<Home {segment}></Home>{#each $nav as child}{#await get('navigation', child) then { default: child_component } }<svelte:component this={child_component} {segment} />{/await}
}}nav {position: fixed;bottom: 0;left: 0;right: 0;height: 5em;z-index: 200;font-weight: 300;padding: 0 2em;margin: 0 -1em;background: hsl(0, 100%, 0%);box-shadow: inset 0px 5px 10px 1px hsla(0, 0%, 8%, 1);:global(svg) {fill: hsla(50, 50%, 100%, 0.3);}ul {margin: 0 auto;padding: 0;display: flex;align-content: center;justify-content: space-around;width: fit-content;border-top-right-radius: 0.5rem;:global(h3) {color: hsla(50, 50%, 100%, 0.3) !important;align-self: center;margin: 0 1rem;}:global(span) {margin: 0;display: flex;flex-direction: column;color: hsla(50, 50%, 100%, 0.3);text-align: center;text-decoration: none;padding: 1em 0.5em;}:global(li) {display: block;float: left;margin: 0 1rem;&:hover {:global(svg) {fill: hsla(50, 50%, 100%, 0.8) !important;}:global(span), :global(a) {color: hsla(50, 50%, 100%, 0.8);}}}:global([aria-current]) {position: relative;}:global([aria-current]::before) {position: absolute;content: '';width: calc(100% - 1em);height: 2px;background-color: rgb(255,62,0);display: block;top: 1px;}
import { faCaretUp, faCaretDown, faCaretRight, faCheck, faCircle, faExclamationTriangle, faTrash, faStickyNote, faWrench, faIgloo, faSearch, faUser, faRecordVinyl, faChartBar, faFolderOpen, faAngleDoubleLeft, faGuitar, faCompass, faSignOutAlt, faInfo } from '@fortawesome/free-solid-svg-icons'import { faSpotify, faApple, faCanadianMapleLeaf } from '@fortawesome/free-brands-svg-icons'
export let nameexport let type = 'solid'
export let name = 'dot'let className = ''export { className as class }export let found = trueexport let scale = 1// font awesome properties are take as additional props via metalet widthlet heightlet pathlet labellet box = `0 0 0 0`let style
async function loadIcon(src) {const response = await fetch(`/Icons/${type}/${src}.js`)const data = await response.json()console.log({data})
const solid_icons = {caret_up: faCaretUp,caret_down: faCaretDown,caret_right: faCaretRight,check: faCheck,circle: faCircle,exclamation_triangle: faExclamationTriangle,trash: faTrash,note: faStickyNote,wrench: faWrench,info: faInfo,igloo: faIgloo,search: faSearch,user: faUser,chart: faChartBar,folder: faFolderOpen,angle_double_left: faAngleDoubleLeft,guitar: faGuitar,compass: faCompass,record: faRecordVinyl,'sign-out': faSignOutAlt}const brand_icons = {spotify: faSpotify,apple: faApple,maple_leaf: faCanadianMapleLeaf}const classEval = (className, svgName) => {if (className != '') {if (solid_icons[svgName]) {found = truereturn [solid_icons[svgName], className]} else if (brand_icons[svgName]) {found = truereturn [brand_icons[svgName], className]} else {found = falsereturn [faCircle, className]
if (typeof data !== 'undefined') {return {...destructure(data),className: data.prefix + ' ' + data.iconName
} else if (solid_icons[svgName]) {found = truereturn [solid_icons[svgName],solid_icons[svgName].prefix + ' ' + solid_icons[svgName].iconName]} else if (brand_icons[svgName]) {found = truereturn [brand_icons[svgName],brand_icons[svgName].prefix + ' ' + brand_icons[svgName].iconName]} else {found = falsereturn [ faCircle, 'fas fa-circle' ]
let [data, svgClassName] = classEval(className, name)$: [data, svgClassName] = classEval(className, name)const propEval = props => {const entries = Object.entries(props)return entries.reduce((result, [key, value]) => {if (['class', 'name', 'found', 'scale'].includes(key)) {return result}
let props = propEval($$props)$: props = propEval($$props)
// import { faCaretUp, faCaretDown, faCaretRight, faCheck, faCircle, faExclamationTriangle, faTrash, faStickyNote, faWrench, faIgloo, faSearch, faUser, faRecordVinyl, faChartBar, faFolderOpen, faAngleDoubleLeft, faGuitar, faCompass, faSignOutAlt, faInfo } from '@fortawesome/free-solid-svg-icons'
$: {const [_width, _height /* _ligatures */ /* _unicode */, , , _svgPathData] = data.icon
export let scale = 1// font awesome properties are take as additional props via meta// const solid_icons = {// caret_up: faCaretUp,// caret_down: faCaretDown,// caret_right: faCaretRight,// check: faCheck,// circle: faCircle,// exclamation_triangle: faExclamationTriangle,// trash: faTrash,// note: faStickyNote,// wrench: faWrench,// info: faInfo,// igloo: faIgloo,// search: faSearch,// user: faUser,// chart: faChartBar,// folder: faFolderOpen,// angle_double_left: faAngleDoubleLeft,// guitar: faGuitar,// compass: faCompass,// record: faRecordVinyl,// 'sign-out': faSignOutAlt// }
<svgversion="1.1"class="fa-icon {className} {props}"x={0}y={0}{width}{height}data-icon={name}aria-label={label}role={label ? 'img' : 'presentation'}viewBox={box}{style}><path d={path} /></svg>
{#await loadIcon(name) then { width, height, path, label, box, className }}<svgversion="1.1"class="fa-icon {className} {$$props}"x={0}y={0}{width}{height}data-icon={name}aria-label={label}role={label ? 'img' : 'presentation'}viewBox={box}{style}><path d={path} /></svg>{/await}
datastore.write(`/session/local/user_id`, packet.u)
console.log({o: 's', p: `+/users/${packet.u}/#`, i: true})ws.send({o: 's', t: `+/users/${packet.u}/#`, i: true})datastore.write(`/session/user_id`, packet.u)
datastore.subscribe('q/action/#', (topic, event) => {const { pointer, value } = eventif (lodash.isPlainObject(value) || event.type !== '=') { return }send(value, pointer.path)})datastore.subscribe('q/setup/#', (topic, event) => {const { pointer, value } = eventif (lodash.isPlainObject(value) || event.type !== '=') { return }send(value, pointer.path)})
__sapper__/.svelte-kit/