P6QOQ6LCNINOHPDWJESN7V43M6E5KY5CUECCQ74QW4HSPAU3J3QQC
#!/usr/bin/env bash
if [[ "$(uname -s)" == "Darwin" ]]; then
echo "Don't run this on your local computer!"
exit 1
fi
echo "[remote] Updating processor"
cd djinmusic
pnpm install -r
cd ..
echo "[remote] Installed"
#!/usr/bin/env bash
. $BIN_DIR/_lib.sh
rsync --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) {
// 0
const salt = packet.s
const verifier = packet.v
const 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) {
// 1
const 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 ephemeral
if (salt && verifier) {
console.log(`#authorize() ${packet.u} found`)
ephemeral = srp.generateEphemeral(verifier)
datastore.write(server_secret_key_pointer, ephemeral.secret)
response.v.k = ephemeral.public
response.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 = ephemeral
response.v.s = salt
}
return response
}
prove(pointer, packet) {
// 3
const 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.u
const verifier = datastore.read(verifier_pointer)
const proof = packet.p
var session, response
try {
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 = topic
permissions._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 = false
for (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 parse
if (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 = false
for (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, {})._value
if (_.isArray(subscribers[subscriber])) {
subscribers[subscriber].push(callback)
} else {
subscribers[subscriber] = [callback]
}
}
unsubscribe(subscriber) {
console.log(`#unsubscribe ${subscriber}`)
const removeSubscriber = ({ _value }) => {
if (!_value) return
delete _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 = Pointer
import fs from 'fs'
const loadConfig = async () => {
// configuration information goes here
datastore.set('/session/node_id', '952ede89-4c91-4df7-bdab-c6dda4257abb')
let filePath
fs.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)
}
break
default:
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 playlist
const intervalDays = datastore.read(pointer.path + '/interval')
const intervalSeconds = intervalDays * 24 * 60 * 60
datastore.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 = true
if (current_list) {
for (var idx in current_list) {
if (current_list[idx] != sorted_list[idx]) { match = false }
}
} else {
match = false
}
if (match) return
datastore.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 = eventId
const 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_in
datastore.write('/setup/admin/token', message.access_token)
datastore.write('/setup/admin/expiry', new_expiry)
}
onPlaylistItemsQueued(value, pointer) {
if (!value) return
console.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 Driver
const 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) return
console.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 = 25706
const wss = new WebSocket.Server({ port })
console.log(`Listenning on ${port}`)
wss.on('connection', function connection(ws, request) {
const ip = request.socket.remoteAddress
ws.uuid = uuid()
ws.toString = () => ws.uuid
console.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_id
hello.p = '/session/node_id'
ws.send(JSON.stringify(hello))
const publish = (topic, pointer, value) => {
const message = {}
message.o = 'p'
message.p = pointer.path
message.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, channel
switch (message.o) {
case 'a':
console.dir(message)
switch (message.c) {
case 0:
// createAccount
pointer = Pointer.create(message.t)
channel = channels[pointer.trunk_steps[0]]
channel.createAuthorization(pointer, message.v, ws)
break
case 1:
// startSession
pointer = Pointer.create(message.t)
channel = channels[pointer.trunk_steps[0]]
channel.authorize(pointer, message.v, ws)
break
case 4:
// verifySession
pointer = Pointer.create(message.t)
channel = channels[pointer.trunk_steps[0]]
if (channel.prove(pointer, message.v, ws)) {
session.addSubscription(message.v.u, pointer)
}
break
case 5:
// post session
session.fetchSession(message.v, ws, channels)
break
case 6:
// fetch session
session.requestSession(message.v, ws, users)
default:
break
}
break
case 'm':
pointer = Pointer.create(message.p)
channel = channels[pointer.trunk_steps[0]]
channel.merge(pointer, message.v, ws)
break
case 'w':
pointer = Pointer.create(message.p)
channel = channels[pointer.trunk_steps[0]]
channel.write(pointer, message.v, ws)
break
case 'd':
pointer = Pointer.create(message.p)
channel = channels[pointer.trunk_steps[0]]
channel.delete(pointer, ws)
break
case 's':
pointer = Pointer.create(message.p)
channel = channels[pointer.trunk_steps[0]]
channel.subscribe(pointer, ws, publish)
if (!message.i) break
case '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 = path
response.v = value
ws.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-console
process.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) return
let 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')) &&
timestamp > + 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 + ':' + token
console.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 bash
if [[ "$(uname -s)" == "Darwin" ]]; then
echo "Don't run this on your local computer!"
exit 1
fi
echo "[remote] Updating processor"
cd djinmusic
pnpm install -r
cd ..
echo "[remote] Installed"
#!/usr/bin/env bash
. $BIN_DIR/_lib.sh
rsync --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) {
// 0
const salt = packet.s
const verifier = packet.v
const 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) {
// 1
const 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 ephemeral
if (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.public
response.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 = ephemeral
response.v.s = salt
}
return response
}
prove(topic, packet) {
// 3
const 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.u
const verifier = datastore.read('/' + verifier_topic.pattern)
const proof = packet.p
var session, response
try {
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 = topic
permissions._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 !== '=') return
const { pointer } = event
const value = pointer?.tree?.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.delete(private_path)
} else {
this.dewhitelist(syndicated_path)
datastore.write(private_path, true)
}
}
publish(topic, event) {
if (event.type !== '=') return
const { pointer } = event
console.log(`${this.toString()} #publish ${topic} @ ${pointer.path}`)
const permissions = this._permissions.entries(pointer.path.slice(1))
let whitelisted = false
for (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 parse
if (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 = false
for (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, {})._value
if (_.isArray(subscribers[subscriber])) {
subscribers[subscriber].push(callback)
} else {
subscribers[subscriber] = [callback]
}
}
unsubscribe(subscriber) {
console.log(`#unsubscribe ${subscriber}`)
const removeSubscriber = ({ _value }) => {
if (!_value) return
delete _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 rarely
all(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._root
for (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._root
for (const step of steps) {
if (node[step] == null) {
node[step] = this.createTreeNode()
}
node = node[step]
}
if (node._value == null) {
node._topic = topic
node._value = value
}
return node
}
add(topic, value) {
const node = this.getWithDefault(topic)
node._topic = topic
node._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 = Pointer
if (!global.Topic) global.Topic = Topic
import fs from 'fs'
const loadConfig = async () => {
// configuration information goes here
datastore.set('/session/node_id', '952ede89-4c91-4df7-bdab-c6dda4257abb')
let filePath
fs.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 !== '=') return
const { pointer } = event
const value = pointer?.tree?.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.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 !== '=') return
const { pointer } = event
const value = pointer?.tree?.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)
}
break
default:
this.accept(pointer, value)
break
}
}
onSetupQueued(topic, event) {
if (event.type !== '=') return
const { pointer } = event
const value = pointer?.tree?.value
console.log(`${this.toString()} #onSetupQueued: ${pointer.path}, ${JSON.stringify(value)}`)
this.accept(pointer, value)
}
onUserQueued(topic, event) {
if (event.type !== '=') return
const { pointer } = event
const value = pointer?.tree?.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 playlist
const intervalDays = datastore.read(pointer.path + '/interval')
const intervalSeconds = intervalDays * 24 * 60 * 60
datastore.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 = true
if (current_list) {
for (var idx in current_list) {
if (current_list[idx] != sorted_list[idx]) { match = false }
}
} else {
match = false
}
if (match) return
datastore.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 !== '=') return
const { pointer } = event
const value = pointer?.tree?.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.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 } = event
console.log(`#initialize_playlist ${pointer.path}`)
const eventId = pointer.branch.slice(-1)[0]
const name = eventId
const 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 !== '=') return
const { pointer } = event
const value = pointer?.tree?.value
datastore.write(`/setup${pointer.branch_path}/details_synced`, false)
this.debouncePushPlaylistDetails(value, pointer)
}
pushPlaylistDetails(value, pointer) {
if (event.type !== '=') return
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, event) {
if (event.type !== '=') return
const { pointer } = event
const value = pointer?.tree?.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_in
datastore.write('/setup/admin/token', message.access_token)
datastore.write('/setup/admin/expiry', new_expiry)
}
onPlaylistItemsQueued(value, pointer) {
if (!value) return
console.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 Driver
const 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) return
console.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 !== '=') return
const { pointer } = event
const value = pointer?.tree?.value
console.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/2021
this.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 event
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(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 })
} break
case '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 !== '=') return
const { pointer } = event
switch(pointer.leaf) {
// leafs that don't require validation
case '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 validation
case 'pin': {
this.accept(pointer, pointer.value)
} break
case 'time': {
this.accept(pointer, pointer.value)
} break
case 'type': {
if (!['recurring', 'one_time'].includes(pointer.value)) return
this.accept(pointer, pointer.value)
} break
// special cases
case 'blacklist': {
if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {
datastore.push(`/setup/events/${pointer.setps[5]}/blacklist`, pointer.value)
}
} break
case 'whitelist': {
if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {
datastore.pull(`/setup/events/${pointer.setps[5]}/blacklist`, pointer.value)
}
} break
case '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)
}
}
} break
case '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 !== '=') return
const { pointer } = event
const value = pointer?.tree?.value
if (!value) return
// NOTE: check the account type and the number of owned events
// TODO: @thomascowan add support for accoun types
const 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 event
switch (account_type) {
case 'business': {
// NOTE: Placeholder do nothing for now
} break
case 'basic': {
if (owned.length >= 5) return
} break
case 'free': {
if (owned.length >= 3) return
}
}
// NOTE: create an event with the user_id
const 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 } = event
const value = pointer?.tree?.value
if (!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 = 25706
const wss = new WebSocket.Server({ port })
console.log(`Listenning on ${port}`)
wss.on('connection', function connection(ws, request) {
const ip = request.socket.remoteAddress
ws.uuid = uuid()
ws.toString = () => ws.uuid
console.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_id
hello.p = '/session/node_id'
ws.send(JSON.stringify(hello))
const publish = (topic, pointer) => {
const message = {}
const { tree, path } = pointer
const { value } = tree
message.o = 'p'
message.t = topic
message.p = path
message.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, topic, channel
switch (message.o) {
case 'a': {
switch (message.c) {
case 0: {
// createAccount
topic = new Topic(message.t)
channel = channels[topic.trunk[0]]
channel.createAuthorization(topic, message.v, ws)
} break
case 1: {
// startSession
topic = new Topic(message.t)
channel = channels[topic.trunk[0]]
channel.authorize(topic, message.v, ws)
} break
case 4: {
// verifySession
topic = new Topic(message.t)
channel = channels[topic.trunk[0]]
if (channel.prove(topic, message.v, ws)) {
session.addSubscription(message.v.u, topic)
}
} break
case 5: {
// post session
session.fetchSession(message.v, ws, channels)
} break
case 6: {
// fetch session
session.requestSession(message.v, ws, users)
} break
default:
break
}
} break
case 'm': {
pointer = new Pointer(message.p)
channel = channels[pointer.trunk[0]]
channel.merge(pointer, message.v, ws)
} break
case 'w': {
pointer = new Pointer(message.p)
channel = channels[pointer.trunk[0]]
channel.write(pointer, message.v, ws)
} break
case 'd': {
pointer = new Pointer(message.p)
channel = channels[pointer.trunk[0]]
channel.delete(pointer, ws)
} break
case '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 = path
response.v = value
ws.send(JSON.stringify(response))
})
} break
case '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-console
process.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) return
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')) &&
timestamp > + 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 + ':' + token
console.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: true
resolution:
integrity: sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==
/varint/6.0.0:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
dev: false
svelte: 3.42.1
svelte-preprocess: 3.9.12_svelte@3.42.1
svelte-preprocess-sass: 0.2.0
app/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.35
eslint: ^7.22.0
eslint-config-prettier: ^8.1.0
eslint-plugin-svelte3: ^3.2.0
lodash: ^4.17.21
node-sass: ^6.0.1
prettier: ~2.2.1
prettier-plugin-svelte: ^2.2.0
query-string: 4.3.2
sanitize.css: ^12.0.1
sass: ^1.35.2
secure-remote-password: ^0.3.1
svelte: ^3.34.0
svelte-preprocess: ^4.7.3
uuid: ^8.0.0
dependencies:
'@fortawesome/free-brands-svg-icons': 5.15.4
'@fortawesome/free-solid-svg-icons': 5.15.4
sanitize.css: 12.0.1
sass: 1.37.5
devDependencies:
'@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.35
eslint: 7.32.0
eslint-config-prettier: 8.3.0_eslint@7.32.0
eslint-plugin-svelte3: 3.2.0_eslint@7.32.0+svelte@3.42.1
lodash: 4.17.21
node-sass: 6.0.1
prettier: 2.2.1
prettier-plugin-svelte: 2.3.1_prettier@2.2.1+svelte@3.42.1
query-string: 4.3.2
secure-remote-password: 0.3.1
svelte: 3.42.1
svelte-preprocess: 4.7.4_6197623e5ed34153d1bcd9290e2954d7
uuid: 8.3.2
lib/datastore_old:
lib/ipc:
specifiers:
uuid: ^8.0.0
varint: ^6.0.0
dependencies:
uuid: 8.3.2
varint: 6.0.0
services/channel_ws_server:
specifiers:
'@djinlist/env': 1.0.0
'@djinlist/ipc': 1.0.0
uuid: ^8.0.0
ws: ^6.0.0
dependencies:
'@djinlist/env': link:../../lib/env
'@djinlist/ipc': link:../../lib/ipc
uuid: 8.3.2
ws: 6.2.2
services/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.6
debug: 4.3.2
espree: 7.3.1
globals: 13.10.0
ignore: 4.0.6
import-fresh: 3.3.0
js-yaml: 3.14.1
minimatch: 3.0.4
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
dev: 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: true
dev: 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: true
dependencies:
'@fortawesome/fontawesome-common-types': 0.2.36
dev: 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: true
dependencies:
'@fortawesome/fontawesome-common-types': 0.2.36
dev: false
/@humanwhocodes/config-array/0.5.0:
resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==}
engines: {node: '>=10.10.0'}
dependencies:
'@humanwhocodes/object-schema': 1.2.0
debug: 4.3.2
minimatch: 3.0.4
transitivePeerDependencies:
- supports-color
dev: 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: true
peerDependencies:
svelte: ^3.34.0
dependencies:
'@sveltejs/vite-plugin-svelte': 1.0.0-next.15_svelte@3.42.1+vite@2.4.4
cheap-watch: 1.0.3
sade: 1.7.4
svelte: 3.42.1
vite: 2.4.4
transitivePeerDependencies:
- 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.5
svelte: ^3.34.0
vite: ^2.3.7
peerDependenciesMeta:
diff-match-patch:
optional: true
dependencies:
'@rollup/pluginutils': 4.1.1
debug: 4.3.2
kleur: 4.1.4
magic-string: 0.25.7
require-relative: 0.8.7
svelte: 3.42.1
svelte-hmr: 0.14.7_svelte@3.42.1
vite: 2.4.4
transitivePeerDependencies:
- supports-color
dev: true
/@taylorzane/sveltejs-adapter-node/1.0.0-next.35:
resolution: {integrity: sha512-5DVAmeCgcKtU+DZ36HoglNHjPZbx52/cP3V9s/RcwAzEa9VKh+MVwxW/fYm8M7x3bD4YUhaAy5ECdKjCPxUwcw==}
dependencies:
esbuild: 0.12.19
tiny-glob: 0.2.9
dev: 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.2
dev: true
/chokidar/3.5.2:
resolution: {integrity: sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==}
engines: {node: '>= 8.10.0'}
dependencies:
anymatch: 3.1.2
braces: 3.0.2
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.1
normalize-path: 3.0.0
readdirp: 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.0
dependencies:
eslint: 7.32.0
svelte: 3.42.1
dev: 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: true
dependencies:
'@babel/code-frame': 7.12.11
'@eslint/eslintrc': 0.4.3
'@humanwhocodes/config-array': 0.5.0
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.2
doctrine: 3.0.0
enquirer: 2.3.6
escape-string-regexp: 4.0.0
eslint-scope: 5.1.1
eslint-utils: 2.1.0
eslint-visitor-keys: 2.1.0
espree: 7.3.1
esquery: 1.4.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 6.0.1
functional-red-black-tree: 1.0.1
glob-parent: 5.1.2
globals: 13.10.0
ignore: 4.0.6
import-fresh: 3.3.0
imurmurhash: 0.1.4
is-glob: 4.0.1
js-yaml: 3.14.1
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
lodash.merge: 4.6.2
minimatch: 3.0.4
natural-compare: 1.4.0
optionator: 0.9.1
progress: 2.0.3
regexpp: 3.2.0
semver: 7.3.5
strip-ansi: 6.0.0
strip-json-comments: 3.1.1
table: 6.7.1
text-table: 0.2.0
v8-compile-cache: 2.3.0
transitivePeerDependencies:
- supports-color
dev: 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.1
acorn-jsx: 5.3.2_acorn@7.4.1
eslint-visitor-keys: 1.3.0
dev: 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.0
combined-stream: 1.0.8
mime-types: 2.1.32
/gauge/2.7.4:
resolution: {integrity: sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=}
dependencies:
aproba: 1.2.0
console-control-strings: 1.1.0
has-unicode: 2.0.1
object-assign: 4.1.1
signal-exit: 3.0.3
string-width: 1.0.2
strip-ansi: 3.0.1
wide-align: 1.1.3
dev: true
/gaze/1.1.3:
resolution: {integrity: sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==}
engines: {node: '>= 4.0.0'}
dependencies:
globule: 1.3.2
dev: 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.7
lodash: 4.17.21
minimatch: 3.0.4
dev: true
/har-validator/5.1.5:
resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==}
engines: {node: '>=6'}
deprecated: this library is no longer supported
dependencies:
ajv: 6.12.6
har-schema: 2.0.0
dev: 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.2
camelcase-keys: 6.2.2
decamelize: 1.2.0
decamelize-keys: 1.1.0
hard-rejection: 2.1.0
minimist-options: 4.1.0
normalize-package-data: 3.0.2
read-pkg-up: 7.0.1
redent: 3.0.0
trim-newlines: 3.0.1
type-fest: 0.18.1
yargs-parser: 20.2.9
/minipass/3.1.3:
resolution: {integrity: sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==}
engines: {node: '>=8'}
dependencies:
yallist: 4.0.0
dev: true
/minizlib/2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
dependencies:
minipass: 3.1.3
yallist: 4.0.0
dev: true
/node-gyp/7.1.2:
resolution: {integrity: sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==}
engines: {node: '>= 10.12.0'}
hasBin: true
dependencies:
env-paths: 2.2.1
glob: 7.1.7
graceful-fs: 4.2.8
nopt: 5.0.0
npmlog: 4.1.2
request: 2.88.2
rimraf: 3.0.2
semver: 7.3.5
tar: 6.1.7
which: 2.0.2
dev: true
/node-sass/6.0.1:
resolution: {integrity: sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
dependencies:
async-foreach: 0.1.3
chalk: 1.1.3
cross-spawn: 7.0.3
gaze: 1.1.3
get-stdin: 4.0.1
glob: 7.1.7
lodash: 4.17.21
meow: 9.0.0
nan: 2.15.0
node-gyp: 7.1.2
npmlog: 4.1.2
request: 2.88.2
sass-graph: 2.2.5
stdout-stream: 1.4.1
true-case-path: 1.0.3
dev: true
/normalize-package-data/3.0.2:
resolution: {integrity: sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==}
engines: {node: '>=10'}
dependencies:
hosted-git-info: 4.0.2
resolve: 1.20.0
semver: 7.3.5
validate-npm-package-license: 3.0.4
dev: true
word-wrap: 1.2.3
dev: true
/optionator/0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
engines: {node: '>= 0.8.0'}
dependencies:
deep-is: 0.1.3
fast-levenshtein: 2.0.6
levn: 0.4.1
prelude-ls: 1.2.1
type-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.0
svelte: ^3.2.0
dependencies:
prettier: 2.2.1
svelte: 3.42.1
dev: true
/prettier/2.2.1:
resolution: {integrity: sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==}
engines: {node: '>=10.13.0'}
hasBin: true
dev: 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.1
strict-uri-encode: 1.1.0
dev: true
dev: true
/readable-stream/2.3.7:
resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
dependencies:
core-util-is: 1.0.2
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-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/3142
dependencies:
aws-sign2: 0.7.0
aws4: 1.11.0
caseless: 0.12.0
combined-stream: 1.0.8
extend: 3.0.2
forever-agent: 0.6.1
form-data: 2.3.3
har-validator: 5.1.5
http-signature: 1.2.0
is-typedarray: 1.0.0
isstream: 0.1.2
json-stringify-safe: 5.0.1
mime-types: 2.1.32
oauth-sign: 0.9.0
performance-now: 2.1.0
qs: 6.5.2
safe-buffer: 5.2.1
tough-cookie: 2.5.0
tunnel-agent: 0.6.0
uuid: 3.4.0
dev: 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: true
dependencies:
glob: 7.1.7
lodash: 4.17.21
scss-tokenizer: 0.2.3
yargs: 13.3.2
dev: true
/sass/1.37.5:
resolution: {integrity: sha512-Cx3ewxz9QB/ErnVIiWg2cH0kiYZ0FPvheDTVC6BsiEGBTZKKZJ1Gq5Kq6jy3PKtL6+EJ8NIoaBW/RSd2R6cZOA==}
engines: {node: '>=8.9.0'}
hasBin: true
dependencies:
chokidar: 3.5.2
dev: false
/scss-tokenizer/0.2.3:
resolution: {integrity: sha1-jrBtualyMzOCTT9VMGQRSYR85dE=}
dependencies:
js-base64: 2.6.4
source-map: 0.4.4
dev: true
/sshpk/1.16.1:
resolution: {integrity: sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==}
engines: {node: '>=0.10.0'}
hasBin: true
dependencies:
asn1: 0.2.4
assert-plus: 1.0.0
bcrypt-pbkdf: 1.0.2
dashdash: 1.14.1
ecc-jsbn: 0.1.2
getpass: 0.1.7
jsbn: 0.1.1
safer-buffer: 2.1.2
tweetnacl: 0.14.5
dev: true
/svelte-hmr/0.14.7_svelte@3.42.1:
resolution: {integrity: sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==}
peerDependencies:
svelte: '>=3.19.0'
dependencies:
svelte: 3.42.1
dev: 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.0
dev: true
/svelte-preprocess/3.9.12_svelte@3.42.1:
resolution: {integrity: sha512-OX8a7drmlYcX/bLKbtRTvcc0lYu5Ub78D4B/GVxac2zeyrj1e5vEJU6BsxFbc/8kFDqI6BgsCLZAqsFDr/KrDQ==}
engines: {node: '>= 7.6.0'}
requiresBuild: true
peerDependencies:
'@babel/core': ^7.10.2
coffeescript: ^2.5.1
less: ^3.11.3
node-sass: '*'
postcss: ^7.0.32
postcss-load-config: ^2.1.0
pug: ^3.0.0
sass: ^1.26.8
stylus: ^0.54.7
svelte: ^3.23.0
typescript: ^3.9.5
peerDependenciesMeta:
'@babel/core':
optional: true
coffeescript:
optional: true
less:
optional: true
node-sass:
optional: true
postcss:
optional: true
postcss-load-config:
optional: true
pug:
optional: true
sass:
optional: true
stylus:
optional: true
svelte:
optional: true
typescript:
optional: true
dependencies:
'@types/pug': 2.0.5
'@types/sass': 1.16.1
detect-indent: 6.1.0
strip-indent: 3.0.0
svelte: 3.42.1
dev: true
/svelte-preprocess/4.7.4_6197623e5ed34153d1bcd9290e2954d7:
resolution: {integrity: sha512-mDAmaltQl6e5zU2VEtoWEf7eLTfuOTGr9zt+BpA3AGHo8MIhKiNSPE9OLTCTOMgj0vj/uL9QBbaNmpG4G1CgIA==}
engines: {node: '>= 9.11.2'}
requiresBuild: true
peerDependencies:
'@babel/core': ^7.10.2
coffeescript: ^2.5.1
less: ^3.11.3
node-sass: '*'
postcss: ^7 || ^8
postcss-load-config: ^2.1.0 || ^3.0.0
pug: ^3.0.0
sass: ^1.26.8
stylus: ^0.54.7
sugarss: ^2.0.0
svelte: ^3.23.0
typescript: ^3.9.5 || ^4.0.0
peerDependenciesMeta:
'@babel/core':
optional: true
coffeescript:
optional: true
less:
optional: true
node-sass:
optional: true
postcss:
optional: true
postcss-load-config:
optional: true
pug:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
typescript:
optional: true
dependencies:
'@types/pug': 2.0.5
'@types/sass': 1.16.1
detect-indent: 6.1.0
node-sass: 6.0.1
sass: 1.37.5
strip-indent: 3.0.0
svelte: 3.42.1
dev: 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.2
lodash.clonedeep: 4.5.0
lodash.truncate: 4.4.2
slice-ansi: 4.0.0
string-width: 4.2.2
strip-ansi: 6.0.0
/tar/6.1.7:
resolution: {integrity: sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA==}
engines: {node: '>= 10'}
dependencies:
chownr: 2.0.0
fs-minipass: 2.1.0
minipass: 3.1.3
minizlib: 2.1.2
mkdirp: 1.0.4
yallist: 4.0.0
dev: 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: true
dev: true
/verror/1.10.0:
resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=}
engines: {'0': node >=0.6.0}
dependencies:
assert-plus: 1.0.0
core-util-is: 1.0.2
extsprintf: 1.3.0
dev: 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: true
dependencies:
esbuild: 0.12.19
postcss: 8.3.6
resolve: 1.20.0
rollup: 2.56.1
optionalDependencies:
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 = group
this.name = name
this.callbacks = callbacks
this.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, Pathable
dependent.steps[position] = this.value
dependent.update()
} else {
// Pipable
dependent.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 = value
this.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 = callbacks
if (this.topic_or_path === topic_or_path) {
return false
}
this.topic_or_path = topic_or_path
this.pointer = Pointer.create(topic_or_path)
this.steps = []
this.is_static = true
this.pointer.steps.forEach((step, i) => {
if (step.startsWith(':')) {
step = step.slice(1)
this.is_static = false
this.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.topic
return
}
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 = value
if (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 = callbacks
if (this.topic_or_path === topic_or_path) {
return false
}
this.topic_or_path = topic_or_path
this.pointer = Pointer.create(topic_or_path)
this.steps = []
this.is_static = true
this.pointer.steps.forEach((step, i) => {
if (step.startsWith(':')) {
step = step.slice(1)
this.is_static = false
this.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 = path
if (!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_name
this.replacement = replacement
this.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) return
this.value = next
console.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 = preferences
this.default_value = default_value
this.steps.forEach((step, i) => {
this.group.addDependent(step, this, i)
})
}
toString() {
return `[Preferable ${JSON.stringify(this.name)}]`
}
update() {
this.value = this.default_value
for (const step of this.steps) {
if (step != null) {
this.value = step
break
}
}
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 = datastore
Object.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 = callbacks
pipe.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.dependents
delete 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._root
for (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._root
pointer.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] = value
if (!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 = 1
while (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] = key
const next = node[key]
if (steps.length - 1 === pivot) {
const path = `/${steps.join('/')}`
results[path] = next
continue
}
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] = next
return 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._root
pointer.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, 'when')
}
}
left = left[step]
})
left[last] = value
if (!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] = entry
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 = `${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] = entry
subscription_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 topics
const subscription_map = this._topic_tree.getWithDefault(topic, new Map())._value
subscription_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.topic
const subscription_maps = this._topic_tree.entries(topic)
if (subscription_maps == null) {
return
}
subscription_maps.forEach(entry => {
const [topic, subscription_map] = entry
if (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] = entry
subscription_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 topics
const subscription_map = this._topic_tree.getWithDefault(topic, new Map())._value
subscription_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') return
callback(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) return
subscriber_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)._value
if (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_topic
return
}
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 = value
return
}
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 = value
return
}
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 = 0
if (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 = 0
if (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 = 0
if (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 = 0
if (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 = null
this._twig_end = null
}
return
}
if (this.leaf == null) {
this._twig_start = this.steps.length
this._twig_end = this._twig_start + 1
}
this.steps[this._twig_end - 1] = value
this.steps = this.steps // eslint-disable-line
}
parse() {
let cursor = 0
Object.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 = cursor
cursor += 1
break
case 'session':
this._root_start = cursor
cursor += 1
this.parseSession(cursor)
return
}
switch (this.steps[cursor]) {
case 'action':
case 'state':
case 'setup':
case '+':
this._root_start = cursor
cursor += 1
break
}
switch (this.steps[cursor]) {
case 'users':
case 'events':
case 'items':
this._trunk_start = cursor
this._branch_start = cursor
this._trunk_end = cursor + 2
break
case 'admin':
this._trunk_start = cursor
this._branch_start = cursor
this._trunk_end = cursor + 1
break
case 'services':
this._trunk_start = cursor
this._trunk_end = cursor + 1
cursor += 1
this.parseService(cursor)
return
}
const limit = this.steps.length - 1
while (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 += 2
this._branch_end = cursor
}
if (cursor < this.steps.length) {
this._twig_start = cursor
this._twig_end = this.steps.length
}
this._parsed = true
}
parseSession(cursor) {
switch (this.steps[cursor]) {
case 'path':
case 'redirect':
case 'user':
this.twig_start = cursor
cursor += 1
this.twig_end = this.steps.length
return
case 'services':
this._trunk_start = cursor
this._trunk_end = cursor + 1
cursor += 1
this.parseService(cursor)
return
}
this._parsed = true
}
parseService(cursor) {
switch (this.steps[cursor]) {
case 'spotify':
case 'apple-music':
this._branch_start = cursor
this._branch_end = cursor + 1
cursor += 1
break
}
const limit = this.steps.length - 1
while (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 += 2
this._branch_end = cursor
}
if (cursor < this.steps.length) {
this._twig_start = cursor
this._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 this
return this.replace(`/${this.root}/`, `/q/${this.root}/`)
}
dequeue() {
if (this.flag != 'q') return this
return 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 text
return text.replace(/~/g, '~0').replace(/\//g, '~1')
}
const toPath = text => {
if (text.slice(0, 2) != '~1') return text
return 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 rarely
all(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._root
for (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._root
for (const step of steps) {
if (node[step] == null) {
node[step] = this.createTreeNode()
}
node = node[step]
}
if (node._value == null) {
node._topic = topic
node._value = value
}
return node
}
add(topic, value) {
const node = this.getWithDefault(topic)
node._topic = topic
node._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 group
describe('.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 callback
before(() => {
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.undefined
expect(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._root
expect(result).to.eql({})
})
it('"/a/b"', () => {
datastore.set('/a/b', true)
datastore.delete('/a/b')
const result = datastore._root
expect(result).to.eql({})
})
it('"/a/b/c"', () => {
datastore.set('/a/b/c', true)
datastore.delete('/a/b/c')
const result = datastore._root
expect(result).to.eql({})
})
it('"/a2"', () => {
datastore.set('/a1', true)
datastore.set('/a2', true)
datastore.delete('/a2')
const result = datastore._root
expect(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 chain
describe('.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.called
datastore.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 topic
it('"/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']['#']._value
expect(map).to.be.empty
expect(() => {
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['#']._value
expect(map).to.be.empty
expect(() => {
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._root
expect(result).to.eql({})
})
it('"/a/b"', () => {
datastore.set('/a/b', true)
datastore.delete('/a/b')
const result = datastore._root
expect(result).to.eql({})
})
it('"/a/b/c"', () => {
datastore.set('/a/b/c', true)
datastore.delete('/a/b/c')
const result = datastore._root
expect(result).to.eql({})
})
it('"/a2"', () => {
datastore.set('/a1', true)
datastore.set('/a2', true)
datastore.delete('/a2')
const result = datastore._root
expect(result).to.eql({ a1: true })
})
})
})
---
systems:
2175edf8-5dac-4b9d-9ba5-8f830bef452a:
setup:
components:
'1':
name: Component 1
location_paths:
- /rooms/1
'2':
name: Component 2
'3':
name: Component 3
displays:
'1':
name: Display 1
systems:
local:
setup:
rooms:
'1':
name: Kitchen
component_paths:
- /components/1
display_paths:
- /components/1/displays/1
'2':
name: Breakfast Room
component_paths:
- /components/2
display_paths:
- /components/2/displays/1
components:
'1':
name: Display
location_paths:
- /rooms/1
displays:
'1':
name: Kitchen Display
'2':
name: Display
location_paths:
- /rooms/2
displays:
'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.prepareStackTrace
var callerfile
try {
var error = new Error()
var currentfile
Error.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 = originalFunc
return 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] = test
describe(`"${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 = undefined
expect(pointer.grove_path).to.be.null
expect(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 = undefined
expect(pointer.flag).to.be.null
expect(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 = undefined
expect(pointer.trunk_path).to.be.null
expect(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 = undefined
expect(pointer.branch_path).to.be.null
expect(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 = undefined
expect(pointer.leaf).to.be.null
expect(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] = test
describe(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.undefined
expect(pointer._topic).to.be.undefined
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.equal('/a')
expect(pointer._steps).to.be.undefined
expect(pointer._topic).to.be.undefined
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.equal('/a/b')
expect(pointer._steps).to.be.undefined
expect(pointer._topic).to.be.undefined
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
})
it('[]', () => {
const pointer = new Pointer([])
expect(pointer._path).to.be.undefined
expect(pointer._steps).to.eql([])
expect(pointer._topic).to.be.undefined
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.undefined
expect(pointer._steps).to.eql(['a'])
expect(pointer._topic).to.be.undefined
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.undefined
expect(pointer._steps).to.eql(['a', 'b'])
expect(pointer._topic).to.be.undefined
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
})
it('"#"', () => {
const pointer = new Pointer('#')
expect(pointer._path).to.be.undefined
expect(pointer._steps).to.be.undefined
expect(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.undefined
expect(pointer._steps).to.be.undefined
expect(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.undefined
expect(pointer._steps).to.be.undefined
expect(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.undefined
expect(pointer._topic).to.be.undefined
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.path = '/a/b'
expect(pointer._path).to.equal('/a/b')
expect(pointer._steps).to.be.undefined
expect(pointer._topic).to.be.undefined
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.steps =', () => {
it('["a"]', () => {
const pointer = new Pointer([])
pointer.steps = ['a']
expect(pointer._path).to.be.undefined
expect(pointer._steps).to.eql(['a'])
expect(pointer._topic).to.be.undefined
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.steps = ['a', 'b']
expect(pointer._path).to.be.undefined
expect(pointer._steps).to.eql(['a', 'b'])
expect(pointer._topic).to.be.undefined
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.topic =', () => {
it('"a"', () => {
const pointer = new Pointer('#')
pointer.topic = 'a'
expect(pointer._path).to.be.undefined
expect(pointer._steps).to.be.undefined
expect(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.undefined
expect(pointer._steps).to.be.undefined
expect(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 // seconds
class IPCClient {
constructor(socket_path, client_id, { subscriptions = [], publications = [] } = {}) {
this.name = socket_path.split('/').slice(-1)
this.client_id = client_id
this.destroyed = false
this.socket_path = socket_path
this.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] = resolve
this.send(message)
})
}
async onRead(message) {
const resolver = this.readers(message.id)
if (!resolver) return
pointer = 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) return
console.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)) return
try {
console.log(`connect() ${this.socket_path}`)
this.socket = net.createConnection(this.socket_path, () => {
console.log('Connected')
this.socket.connected = true
this.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 = false
try {
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) return
this.socket.reading = true
while (this.socket.data_buffer.length >= 4) {
const length = this.getMessageLength(this.socket.data_buffer)
if (this.socket.data_buffer.length < 4 + length) break
const 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':
break
this.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 } = target
if ($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.value
update()
}
function onTimeChange({ target }) {
time = target.value
update()
}
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 = false
const openModal = () => show_modal = true
const 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_path
import { 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 name
export let type = 'solid'
export let name = 'dot'
let className = ''
export { className as class }
export let found = true
export let scale = 1
// font awesome properties are take as additional props via meta
let width
let height
let path
let label
let 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 = true
return [solid_icons[svgName], className]
} else if (brand_icons[svgName]) {
found = true
return [brand_icons[svgName], className]
} else {
found = false
return [faCircle, className]
if (typeof data !== 'undefined') {
return {
...destructure(data),
className: data.prefix + ' ' + data.iconName
} else if (solid_icons[svgName]) {
found = true
return [
solid_icons[svgName],
solid_icons[svgName].prefix + ' ' + solid_icons[svgName].iconName
]
} else if (brand_icons[svgName]) {
found = true
return [
brand_icons[svgName],
brand_icons[svgName].prefix + ' ' + brand_icons[svgName].iconName
]
} else {
found = false
return [ 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
// }
<svg
version="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 }}
<svg
version="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 } = event
if (lodash.isPlainObject(value) || event.type !== '=') { return }
send(value, pointer.path)
})
datastore.subscribe('q/setup/#', (topic, event) => {
const { pointer, value } = event
if (lodash.isPlainObject(value) || event.type !== '=') { return }
send(value, pointer.path)
})
__sapper__/
.svelte-kit/