5FJK2FV6GEVYXYIA757UE2SKX2XPLZKYVBHN47EMK6UC7FO3L23QC
import http from 'http'
class Server {
constructor(client) {
this.client = client
this.server = http.createServer(async function(request, response) {
switch (request.method) {
case "GET": {
const user_path = request.url
const found = !!(await client.read(`state${user_path}/email`))
if (found) {
response.writeHead(204, "User found")
response.end()
} else {
response.writeHead(404, "User not found")
response.end()
}
} break
}
})
}
async start() {
this.server.listen(25608)
}
close() {
this.server.close()
}
}
export { Server }
import { Server } from './server.js'
import { Client } from '@djinlist/ipc/client'
import { DATASTORE_PIPENAME } from '@djinlist/env'
async function main() {
const client = new Client('User Portal', DATASTORE_PIPENAME)
await client.connect()
const server = new Server(client)
await server.start()
}
main()
{
"name": "@djinlist/services_user_portal",
"version": "1.0.0",
"type": "module",
"license": "UNLICENSED",
"private": true,
"main": "src/index.js",
"dependencies": {
"ws": "^6.0.0",
"uuid": "^8.0.0",
"@djinlist/ipc": "1.0.0",
"@djinlist/env": "1.0.0"
},
"scripts": {
"start": "node src/index.js"
},
"exports": {
".": "./src/index.js"
}
}
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 fs from 'fs'
import { Datastore } from '@controlenvy/datastore'
import { Server } from '@djinlist/ipc/server'
import { DATASTORE_PIPENAME } from '@djinlist/env'
if (!global.datastore) global.datastore = new Datastore()
async function loadConfig() {
// configuration information goes here
datastore.set('/session/node_id', '952ede89-4c91-4df7-bdab-c6dda4257abb')
function 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)
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
}
})
}
function main() {
const server = new Server(DATASTORE_PIPENAME, datastore)
server.start()
function cleanup() {
server.cleanup()
process.removeListener('SIGINT', cleanup)
process.removeListener('SIGTERM', cleanup)
process.exit()
}
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
}
await loadConfig()
import './stash.js'
main()
{
"name": "@djinlist/services_datastore",
"version": "1.0.0",
"type": "module",
"license": "UNLICENSED",
"private": true,
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"@djinlist/ipc": "^1.0.0",
"@djinlist/env": "^1.0.0",
"secure-remote-password": "^0.3.1"
},
"exports": {
".": "./src/index.js"
}
}
{
"state": {
"users": {
"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {
"auth": {
"salt": "0b7a988a178f368841cd2f3c05d407d18de26dba6b78e3afb5967c71d9b146d2",
"verifier": "8f935c53fa9b18082f8a06060fa46064be5978adde30aafa89083ac5b25552685f58a33be35cc3f697dab8ceccba18320678ace3970e1a082c7b61117a6af4301a93f40e4c73f26302b10cfbb0fa7f2acb99df708c3eae77ad4f6ae62753692f91e2e76e48cae12a1206eced2feefea1acd0697697c3853248798f791ee30a35e842bea12bcca853ebee71a31a660a8eba5fbdc4b8a2859fabfb8adf8f97717371e8b415057726f78599ceda50413683cfcf3bf5885d8e70831d90c8afabb2d935b62970b1e4f77227a55d0916c65a6b91db329f1903fbbaf5d7fdd6d5ffa3332b50b36db5fc00fb186696adf16f23f0a6fcbcbdb73ce947326b660e0663078a",
"client_public_key": "2ac46f9b921e0d6bceeb021824784acee3ef3f493fde38bdd837cfa844acb21fab549729b4d75891cf05fed07b46bd0464be37f95c2a9f700dff6abc8559187bf9c12d46c4e19fc71d1c6c852d37fe13869d2e6ec6c1729a7c7ab4d8e5aef7d3088fe65e1cd07d43c6b8d08a7b2dcb624b397bdf9c1480f7ace54d7f37e4d5f7b59863ecbb6dbc97c3b28133ecabe5c45f44cd64e27e613635d193fc23033b936318157101fa72fc8c463e0a3e19353ddac1ffafaf2fef1e3a53903434e02dcbf4a17f482e1288ec29a0712a8660d772d9adbf51794e6781888d08df819cac114645f30586f84fe192dee0b974a16af1f4c26d498cf50731854b29fa737b3723",
"server_secret_key": "a9d7ffbcee7ea9394b00f2d050ce484c27049b69e9eb31f445a10aff605e1a41",
"session_key": "57ff063d88ffe91e306dec874e5ff9471dad9e03d9ba249fc9cf085e7481153b"
},
"email": "thomas.p.cowan@gmail.com",
"name": "tpcowan",
"created_at": 1639825217687,
"services": {
"spotify": {
"client": {
"expiry": 1644813102,
"token": "BQAXF6uKCYJnzkJ-zI3wqCAKgKidMWj0jl7swTJVBkX0EC6t6nXyX8CMdLCfk3Jr29CjqBO6W_azFdl_NHNVXHcVqRQh-25LtqLZWjTbIxnur9NOEqUrn0h43Zc04-ZeL77GuByGUn9sG0yPzdm8IuN81N3DO8OAi--LkPzhIxlRydpYQ_ppxiyF5TvzDw"
}
}
},
"followed": [
"/events/62854dc2-7d97-45d3-be03-f0bac69119f8"
],
"topics": [
"+/events/56bf15ad-0b26-4fd9-b838-aebe8d73b244/#",
"+/events/a7313474-90e4-4938-b59d-6eb7a59a0f68/#"
],
"owned": [
"/events/56bf15ad-0b26-4fd9-b838-aebe8d73b244",
"/events/a7313474-90e4-4938-b59d-6eb7a59a0f68"
],
"event_id": "/events/62854dc2-7d97-45d3-be03-f0bac69119f8"
},
"919340b1-1217-548e-914b-a876f726b62a": {
"auth": {
"client_public_key": "a9af1bcbea8f295550dda1a5bf491e3e6e586cc6b9297ec629911fef567f19c2e82dbbab07e9cf5e0de0106a29fe2a3e05aecde84e0d326894e018096f99a8a0869a51146fa253f79e4859b7e93ea37fd3cc589a7ef0f3129e087d190627a7b9e63fbea7a233232651195943f02fa2d86244c220ce457d65ee9985edacb9126c52c92e16dea1064d0f1a2b6b7d0c5b1f49ea4872cc0d0def94198b11731d4e2296368a5ab35b587ca33bd5a19de124c6f2f6a3511589f39e98ecd1e8b3b2f283496aae91ce6fc440117e8cdd492d09796c951c2819faa3934ea6e4cf1585ccf5b54947a62adf7ecbede8d77d5bfbaeec77618fbeab673fafa72933158480efe6"
}
}
}
},
"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"
},
"56bf15ad-0b26-4fd9-b838-aebe8d73b244": {
"private": true,
"owner": "8e97bb55-8b08-5eea-b6d8-3456ec25d301",
"name": "My Birthday",
"admin": [
"8e97bb55-8b08-5eea-b6d8-3456ec25d301"
],
"users": [
"8e97bb55-8b08-5eea-b6d8-3456ec25d301"
]
},
"a7313474-90e4-4938-b59d-6eb7a59a0f68": {
"private": true,
"owner": "8e97bb55-8b08-5eea-b6d8-3456ec25d301",
"name": "Event two",
"time": 1643082402000,
"description": "What's up",
"admin": [
"8e97bb55-8b08-5eea-b6d8-3456ec25d301"
],
"users": [
"8e97bb55-8b08-5eea-b6d8-3456ec25d301"
]
}
}
}
}
{
"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"
}
}
}
}
import crypto from "crypto"
import { forEach, find } from 'lodash-es'
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, channel) {
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}`)
const topics = datastore.read(`/state/users/${user}/topics`) || []
console.log({session, topics})
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: topics }
return true
}
})
}
if (response.v) {
forEach(topics, pattern => {
const topic = new Topic(pattern)
channel.resume(topic, ws)
})
channel.resume(new Topic(`+/users/${user}/#`), ws)
datastore.write(`/session/connections/${user}/ws`, ws.toString())
}
ws.send(JSON.stringify(response))
}
requestSession(user, ws, client) {
if (!client.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')
datastore.write(`/session/connections/${user}/tokens/${token}`, + new Date + 60 * 60 * 24 * 7 * 1000)
datastore.write(`/session/connections/${user}/ws`, ws.toString())
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(`/state/users/${user}/topics`) || [])
topics.add(topic.pattern)
datastore.write(`/state/users/${user}/topics`, Array.from(topics))
}
toString() {
return 'Channel-Connections'
}
}
const session = new Session()
export { session }
import WebSocket from 'ws'
import { v4 as uuid } from 'uuid'
import { forEach } from 'lodash-es'
import './models/item.js'
import './models/event.js'
import './models/user.js'
import './models/admin.js'
import './models/spotify.js'
import { session } from './session.js'
import { client } from './channels/client.js'
let main = () => {
/* Models */
/* currently empty */
/* 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)
client.createAuthorization(topic, message.v, ws)
} break
case 1: {
// startSession
topic = new Topic(message.t)
client.authorize(topic, message.v, ws)
} break
case 4: {
// verifySession
topic = new Topic(message.t)
if (client.prove(topic, message.v, ws)) {
session.addSubscription(message.v.u, topic)
}
} break
case 5: {
// post session
session.fetchSession(message.v, ws, client)
} break
case 6: {
// fetch session
session.requestSession(message.v, ws, client)
} break
default:
break
}
} break
case 'm': {
pointer = new Pointer(message.p)
client.merge(pointer, message.v, ws)
} break
case 'w': {
pointer = new Pointer(message.p)
client.write(pointer, message.v, ws)
} break
case 'd': {
pointer = new Pointer(message.p)
client.delete(pointer, ws)
} break
case 's': {
topic = new Topic(message.t)
console.log({topic})
client.subscribe(topic, ws, publish)
if (!message.i) { break }
console.log('IMMEDIATE')
message.p = `/${message.t}`
}
case 'r': {
pointer = new Pointer(message.p)
forEach(client.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)
client.unsubscribe(topic, ws, publish)
if (!message.i) break
}
}
})
ws.on('close', () => {
datastore.pull('/session/connections', ip)
client.unsubscribe(ws)
console.log('disconnected')
})
})
const cleanup = () => {
console.log('\rShutting down ws server') // eslint-disable-line no-console
process.removeListener('SIGINT', cleanup)
process.removeListener('SIGTERM', cleanup)
wss.close(() => {
process.exit()
})
}
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
}
main()
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) {
console.log('onEventSetup', event.pointer.path)
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])) {
console.log(`/setup${event.pointer.sliceBranch(2)}/${pointer.leaf}`, pointer.tree.value)
datastore.write(`/setup${event.pointer.sliceBranch(2)}/${pointer.leaf}`, pointer.tree.value)
}
} break
// leafs that require some validation
case 'pin': {
if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {
datastore.push(`/setup/events/${pointer.setps[5]}/blacklist`, pointer.tree.value)
}
} break
case 'time': {
if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {
datastore.push(`/setup/events/${pointer.setps[5]}/blacklist`, pointer.tree.value)
}
} break
case 'type': {
if (!['recurring', 'one_time'].includes(pointer.value)) return
this.accept(pointer, pointer.tree.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.tree.value)
}
} break
case 'whitelist': {
if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {
datastore.pull(`/setup/events/${pointer.setps[5]}/blacklist`, pointer.tree.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.tree.value)
datastore.push(`/setup/events/${pointer.setps[5]}/banned`, pointer.tree.value)
}
} else {
datastore.pull(`/setup/events/${pointer.setps[5]}/banned`, pointer.tree.value)
}
}
} break
case 'unban': {
if (this.permissionFor('admin', pointer.steps[3], pointer.steps[5])) {
datastore.pull(`/setup/events/${pointer.setps[5]}/banned`, pointer.tree.value)
}
} break
}
}
permissionFor(level, user, event) {
if (level === 'owner') {
return datastore.read(`/setup/events/${event}/owner`) === user
} else if (level === 'admin') {
return (datastore.read(`/setup/events/${event}/admin`) || []).includes(user)
} else {
return (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 account 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.push(`/state/users/${user_id}/topics`, `+/events/${new_event_id}/#`, { queue: false })
datastore.push(`/state/users/${user_id}/owned`, `/events/${new_event_id}`, { queue: false })
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])
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 https from 'https'
import { debounce, reduce, concat } from 'lodash-es'
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 }
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 { forEach, reverse, sortBy, entries} from 'lodash-es'
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 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 }
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
const main = async () => {
// configuration information goes here
datastore.set('/session/node_id', '952ede89-4c91-4df7-bdab-c6dda4257abb')
import('./client.js')
import('./server.js')
}
main()
import { Client } from '@djinlist/ipc/client'
import { DATASTORE_PIPENAME } from '@djinlist/env'
async function main() {
const client = new Client('Channel', DATASTORE_PIPENAME)
await client.connect()
datastore.subscribe("state/#", (topic, { type, pointer }) => {
if (type === '=') {
client.write(pointer.path, pointer.tree?.value)
}
if (type === '-') {
client.delete(pointer.path)
}
})
datastore.subscribe("setup/#", (topic, { type, pointer }) => {
if (type === '=') {
client.write(pointer.path, pointer.tree?.value)
}
if (type === '-') {
client.delete(pointer.path)
}
})
client.subscribe("setup/#", (topic, path, value) => {
console.log('write', {path, value})
datastore.write(path, value)
})
client.subscribe("state/#", (topic, path, value) => {
console.log('write', {path, value})
datastore.write(path, value)
})
}
main()
import { forEach } from 'lodash-es'
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'
import { difference } from 'lodash-es'
class Client extends Base {
constructor() {
super()
this._sessions = {}
// user data
this.blacklist(new Topic('state/users/+/auth/#'))
this.blacklist(new Topic('state/users/+/auth/*'))
this.blacklist(new Topic('state/users/+/topics'))
datastore.subscribe('state/users/+/topics', this.approve.bind(this))
datastore.subscribe('state/users/+/#', this.publish.bind(this))
// item data
this.whitelist(new Topic('state/items/+/#'))
// event data
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))
}
approve(topic, event) {
console.log(`#approve ${event.pointer.path}: ${event.type}`)
if (event.type != '=') return
console.log(`#approve ${event.pointer.path}: ${event.pointer.tree?.value}`)
let { pointer: { tree: { value } }} = event
const user_id = topic.steps[2]
const websocket = datastore.read(`/session/connections/${user_id}/ws`)
console.log({user_id, test: datastore.read(`/session/connections`), added: difference(value, (this._sessions[user_id] || [])), removed: difference((this._sessions[user_id] || []), value)})
if (websocket != null) {
value ||= []
const added = difference(value, (this._sessions[user_id] || []))
console.log({added})
for (const pattern of added) {
const permissions = this._permissions.getWithDefault(pattern, [])
console.log({permissions, websocket})
if (permissions._value.includes(websocket)) { continue }
permissions._value.push(websocket)
console.log({permissions})
}
const removed = difference((this._sessions[user_id] || []), value)
console.log({removed})
for (const pattern of removed) {
const permissions = this._permissions.getWithDefault(pattern, [])
console.log({permissions, websocket})
const found_index = permissions._value.indexOf(websocket)
if (found_index === -1) { continue }
permissions._value.splice(found_index, 1)
console.log({permissions})
}
}
this._sessions[user_id] = value
}
toString() {
return 'Channel-Client'
}
}
const client = new Client()
export { client }
import { forEach, remove, concat, reduce, fromPairs, isPlainObject } from 'lodash-es'
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.pattern
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))
console.log({permissions})
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}`, {branch: topic.branch})
subscriber.send(JSON.stringify(response))
console.log({test: `/state${topic.branch_path}/topics`})
const topics = datastore.read(`/state${topic.branch_path}/topics`) || []
for (const topic of topics) {
const permissions = this._permissions.getWithDefault(topic, [])
if (permissions._value.includes(subscriber.toString())) { continue }
permissions._value.push(subscriber.toString())
}
}
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({test1: topic.dequeue().pattern, test2: 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]
console.log({callbacks})
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 (Array.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 srp from "secure-remote-password/server.js"
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 }
{
"name": "@djinlist/services_channel",
"version": "1.0.0",
"type": "module",
"license": "UNLICENSED",
"private": true,
"main": "src/index.js",
"dependencies": {
"ws": "^6.0.0",
"uuid": "^8.0.0",
"@djinlist/ipc": "1.0.0",
"@djinlist/env": "1.0.0"
},
"scripts": {
"start": "node src/index.js"
},
"exports": {
".": "./src/index.js"
}
}
{
"state": {
"users": {
"8e97bb55-8b08-5eea-b6d8-3456ec25d301": {
"auth": {
"salt": "0b7a988a178f368841cd2f3c05d407d18de26dba6b78e3afb5967c71d9b146d2",
"verifier": "8f935c53fa9b18082f8a06060fa46064be5978adde30aafa89083ac5b25552685f58a33be35cc3f697dab8ceccba18320678ace3970e1a082c7b61117a6af4301a93f40e4c73f26302b10cfbb0fa7f2acb99df708c3eae77ad4f6ae62753692f91e2e76e48cae12a1206eced2feefea1acd0697697c3853248798f791ee30a35e842bea12bcca853ebee71a31a660a8eba5fbdc4b8a2859fabfb8adf8f97717371e8b415057726f78599ceda50413683cfcf3bf5885d8e70831d90c8afabb2d935b62970b1e4f77227a55d0916c65a6b91db329f1903fbbaf5d7fdd6d5ffa3332b50b36db5fc00fb186696adf16f23f0a6fcbcbdb73ce947326b660e0663078a",
"client_public_key": "1bc131d1a2cbba81d11874f03b2c3d55825aea5a709f36414503d6a53527429ba3011f6d49dafdfe0742d96f6f86068ce0ec119d588307a9932b9b27501253793af85c1814c849a3df55c3eaa18ab66fbc0bed71d22ba5a55d2c40f130da9db57f352d96162ab17a244a4901f4d54e3af2090e001b0b48a177780ece5b246c353ebb74579eb5367f4e924ec9fa17077e09a4edf55e6f57dcaa209b04c249751e56287e0d85b43eb514442fdd5af9fed087e946f8ec77982cef2b2d368eb5f084e7c859a5951dd660a2683befb75c653b4cae2e6dfaecd786b7fa81e5b1c397ac3e95d6118f2b2e31a977bc7cf6e8d97f90ada0eda0386bed56582d2c9b015f14",
"server_secret_key": "7d199e6b82b390f3ceed1c3ba32e4ec9a5893049ba0b5155f358e98c23984b71",
"session_key": "d53e81b5222afc85c3cb288df34f5bf1b1d1e6479be56ccb00e39dfce7adc74d"
},
"email": "thomas.p.cowan@gmail.com",
"name": "tpcowan",
"created_at": 1639825217687,
"followed": [
"/events/62854dc2-7d97-45d3-be03-f0bac69119f8"
],
"event_id": "/events/62854dc2-7d97-45d3-be03-f0bac69119f8",
"topics": [
"+/events/56bf15ad-0b26-4fd9-b838-aebe8d73b244/#",
"+/events/a7313474-90e4-4938-b59d-6eb7a59a0f68/#"
],
"owned": [
"/events/56bf15ad-0b26-4fd9-b838-aebe8d73b244",
"/events/a7313474-90e4-4938-b59d-6eb7a59a0f68"
]
},
"919340b1-1217-548e-914b-a876f726b62a": {
"auth": {
"client_public_key": "a9af1bcbea8f295550dda1a5bf491e3e6e586cc6b9297ec629911fef567f19c2e82dbbab07e9cf5e0de0106a29fe2a3e05aecde84e0d326894e018096f99a8a0869a51146fa253f79e4859b7e93ea37fd3cc589a7ef0f3129e087d190627a7b9e63fbea7a233232651195943f02fa2d86244c220ce457d65ee9985edacb9126c52c92e16dea1064d0f1a2b6b7d0c5b1f49ea4872cc0d0def94198b11731d4e2296368a5ab35b587ca33bd5a19de124c6f2f6a3511589f39e98ecd1e8b3b2f283496aae91ce6fc440117e8cdd492d09796c951c2819faa3934ea6e4cf1585ccf5b54947a62adf7ecbede8d77d5bfbaeec77618fbeab673fafa72933158480efe6"
}
}
}
},
"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"
},
"56bf15ad-0b26-4fd9-b838-aebe8d73b244": {
"private": true,
"owner": "8e97bb55-8b08-5eea-b6d8-3456ec25d301",
"admin": [
"8e97bb55-8b08-5eea-b6d8-3456ec25d301"
],
"users": [
"8e97bb55-8b08-5eea-b6d8-3456ec25d301"
],
"name": "My Birthday"
},
"a7313474-90e4-4938-b59d-6eb7a59a0f68": {
"private": true,
"owner": "8e97bb55-8b08-5eea-b6d8-3456ec25d301",
"admin": [
"8e97bb55-8b08-5eea-b6d8-3456ec25d301"
],
"users": [
"8e97bb55-8b08-5eea-b6d8-3456ec25d301"
]
}
}
}
}
#!/usr/bin/env bash
. $BIN_DIR/_lib.sh
node --experimental-modules --experimental-json-modules --es-module-specifier-resolution=node src/index.js
.git
*.log
node_modules
app/
nginx.conf
#!/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"
#!/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"
#!/bin/zsh
autoenv_source_parent
autostash WORKING_DIR=$(dirname ${0})
autostash WORKING_BIN_DIR="${WORKING_DIR}/.bin"
autostash alias start="${WORKING_BIN_DIR}/start.sh"
4620c8a2ebdb3dca9a2cc9e001b63408a16232fba35dc5ec72d70ea48d2bfcdf:b73f475d060617a870cd42d5496c5ee86fbccdd8efaace3e81079411872c2564:42384237314242382d453531392d343241352d384634422d464238414442373646463546:66643731666435382d373332332d346435392d623164302d383464396231666232386661
import net from 'net'
import varint from 'varint'
class Client {
constructor(client, datastore) {
this.client = client
this.datastore = datastore
this._buffer = Buffer.alloc(0)
this.onData = this.onData.bind(this)
this.onMessage = this.onMessage.bind(this)
this.publish = this.publish.bind(this)
}
start() {
this.client.on('data', this.onData)
this.client.on('message', this.onMessage)
}
onData(data) {
console.log({data})
this._buffer = Buffer.concat([this._buffer, data])
console.log({buffer: this._buffer})
this._buffer = this.parseEnvelope(this._buffer)
}
onMessage(message) {
try {
message = JSON.parse(message)
console.log({message})
if (!message.o) throw `Unkown message operation`
switch (message.o) {
case 'm': {
this.datastore.merge(message.p, message.v)
} break
case 'w': {
this.datastore.write(message.p, message.v)
} break
case 'ps': {
this.datastore.push(message.p, message.v)
if (message.i) {
this.read(message)
}
} break
case 'pl': {
this.datastore.pull(message.p, message.v)
if (message.i) {
this.read(message)
}
} break
case 'd': {
this.datastore.delete(pointer.path)
} break
case 's': {
this.datastore.subscribe(message.t, this.publish, { immediate: false })
if (message.i) {
this.search(message)
}
} break
case 'se': {
this.search(message)
} break
case 'r': {
this.read(message)
} break
case 'u': {
this.datastore.unsubscribe(message.t, publish)
break
}
}
} catch (error) {
console.log(`Erroring parsing message: ${message}`)
console.log(error)
}
}
send(json) {
console.log('sending', { json })
this.client.write(Buffer.concat([Buffer.from(varint.encode(json.length)), Buffer.from(json, 'utf-8')]))
}
read(message) {
const value = this.datastore.read(message.p)
const response = {
o: 'r' ,
v: value,
i: message.i
}
this.send(JSON.stringify(response))
}
search(message) {
const results = this.datastore.search(message.t)
const response = {
o: message.o,
i: message.i,
t: message.t,
v: results
}
this.send(JSON.stringify(response))
}
publish(topic, { type, pointer}) {
const message = {
o: 'p',
t: topic.path,
p: pointer.path,
v: pointer.tree?.value
}
this.send(JSON.stringify(message))
}
parseEnvelope(buffer) {
while (true) {
if (buffer.length == 0) return buffer
try {
const message_length = varint.decode(buffer)
const total_length = varint.decode.bytes + message_length
if (buffer.length >= total_length) {
const message = buffer.slice(varint.decode.bytes, total_length)
this.client.emit('message', message)
buffer = buffer.slice(total_length)
} else {
return buffer
}
} catch (error) {
return Buffer.alloc(0)
}
}
}
}
class Server {
constructor(path, datastore) {
this.path = path
this.datastore = datastore
this.cleanup = this.cleanup.bind(this)
}
async start() {
/* IPC Sockets */
this.server = net.createServer(function(client) {
const instance = new Client(client, this.datastore)
instance.start()
}.bind(this))
this.server.listen(this.path)
}
cleanup() {
console.log('\rShutting down ipc server') // eslint-disable-line no-console
this.server.close()
}
}
export { Server }
const PubSub = (Base) => class extends Base {
async subscribe(topic, callback, { immediate = true } = {}) {
console.log('Client subscribe', topic)
const callbacks = this.subscriptions[topic]
if (Array.isArray(callbacks)) {
this.subscriptions[topic].push(callback)
} else {
this.subscriptions[topic] = [callback]
const message = {
o: 's',
t: topic,
i: immediate
}
this.send(message)
}
}
unsubscribe(topic, callback) {
const callbacks = this.subscriptions[topic]
if (typeof callbacks === 'undefined') return
if (callbacks.incluseds(callback)) {
this.subscriptions[topic] = callbacks.splice(callbacks.indexOf(callback), 1)
}
if (this.subscriptions[topic].length === 0) {
delete this.subscriptions[topic]
const message = {
o: 'u',
t: topic
}
this.send(message)
}
}
}
export { PubSub }
import net from 'net'
import varint from 'varint'
import { Convenience } from './convenience.js'
import { Api } from './api.js'
import { PubSub } from './pubsub.js'
const RECONNECT_TIME = 5
class IPC {
constructor(name, path) {
this.name = name
this.path = path
this.destroyed = false
this.waiters = {}
this.subscriptions = {}
}
async awaitResponse(message) {
this.send(message)
return new Promise((resolve, reject) => {
this.waiters[message.i] = (response) => resolve(response)
setTimeout(reject, 5000)
})
}
parseEnvelope(buffer) {
while (true) {
if (buffer.length == 0) return buffer
try {
const message_length = varint.decode(buffer)
console.log(message_length)
const total_length = varint.decode.bytes + message_length
console.log(total_length)
if (buffer.length >= total_length) {
const message = buffer.slice(varint.decode.bytes, total_length)
const json = JSON.parse(message.toString('utf-8'))
this.socket.emit('message', json)
buffer = buffer.slice(total_length)
} else {
return buffer
}
} catch (error) {
console.log("E", error)
return Buffer.alloc(0)
}
}
}
/*
* Connect
*/
async delayConnect() {
if (this.connect_timeout) return
console.log(`Reconnecting in ${RECONNECT_TIME}s.`)
return new Promise((resolve) => {
this.connect_timeout = setTimeout(() => {
this.connect_timeout = null
resolve(this.connect())
}, RECONNECT_TIME * 1000)
})
}
async connect() {
if (this.socket && (this.socket.connecting || this.socket.connected)) return
try {
console.log(`Connect() ${this.path}`)
await new Promise(function(resolve, reject) {
const connectTimeout = setTimeout(() => {
reject(new Error("Connection Timeout"))
}, 5000)
this.socket = net.createConnection(this.path)
this.socket.once('connect', () => {
console.log('Connected')
clearTimeout(connectTimeout)
this.socket.connected = true
resolve()
})
this.socket.on('error', error => {
this.connecting = false
console.log(`Error connecting to socketc: ${error}`)
clearTimeout(connectTimeout)
reject(error)
})
}.bind(this))
this.socket.data_buffer = new Buffer.alloc(0)
this.socket.on('data', (data) => {
console.log('data', {data})
this.socket.data_buffer = Buffer.concat([this.socket.data_buffer, data])
this.socket.data_buffer = this.parseEnvelope(this.socket.data_buffer)
})
this.socket.on('message', (message) => {
console.log('message', { message })
switch (message.o) {
case 'ps':
case 'pl':
case 'se':
case 'r': {
const waiter = this.waiters[message.i]
if (typeof waiter === 'function') {
waiter(message)
delete this.waiters[message.i]
}
} break
case 'p': {
const subscriptions = this.subscriptions[message.i]
if (Array.isArray(subscriptions)) {
for (const subscription of subscriptions) {
subscription(message.t, message.p, message.v)
}
}
}
case 's': {
const subscriptions = this.subscriptions[message.t]
console.log({subscriptions})
if (Array.isArray(subscriptions)) {
for (const path in message.v) {
const value = message.v[path]
for (const subscription of subscriptions) {
console.log(message.t, path, value)
subscription(message.t, path, value)
}
}
}
}
}
})
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()
})
return
} catch (e) {
this.connecting = false
console.log(`Error connecting to socket: ${e}`)
return await this.delayConnect()
}
}
/*
* Send
*/
send(message) {
const payload = JSON.stringify(message)
console.log(`#send() ${payload}`)
const buffer = Buffer.concat([Buffer.from(varint.encode(payload.length)), Buffer.from(payload)])
try {
this.socket && this.socket.write(buffer)
} catch (e) {
console.log(e)
}
}
/*
* Cleanup
*/
cleanup() {
this.socket.destroy()
this.destroyed = true
}
}
const Client = Convenience(PubSub(Api(IPC)))
export { Client }
import { v4 as uuid } from 'uuid'
const Convenience = (Base) => class extends Base {
async push(path, { immediate = true } = {}) {
// console.log(`READ (R): ${pointer.path}`)
const message = {
o: 'ps',
p: path
}
if (immediate) {
message.i = uuid()
const response = await this.awaitResponse(message)
return response.v
} else {
this.send(message)
return Promise.resolve()
}
}
async pull(path, { immediate = true } = {}) {
// console.log(`READ (R): ${pointer.path}`)
const message = {
o: 'pl',
p: path
}
if (immediate) {
message.i = uuid()
const response = await this.awaitResponse(message)
return response.v
} else {
this.send(message)
return Promise.resolve()
}
}
}
export { Convenience }
import { v4 as uuid } from 'uuid'
const Api = (Base) => class extends Base {
async merge(path, value) {
// console.log(`MERGE: ${pointer.path}`)
const message = {
o: 'm',
p: path,
v: value
}
this.send(message)
}
async write(path, value) {
// console.log(`WRITE: ${pointer.path}`)
const message = {
o: 'w',
p: path,
v: value
}
this.send(message)
}
async delete(path) {
// console.log(`DESTROY: ${pointer.path}`)
const message = {
o: 'd',
p: path
}
this.send(message)
}
async read(path) {
// console.log(`READ (R): ${pointer.path}`)
const message = {
o: 'r',
p: path,
i: uuid()
}
const response = await this.awaitResponse(message)
console.log({response})
return response.v
}
async search(pointer) {
// console.log(`READ (R): ${pointer.path}`)
const message = {
o: 'se',
t: topic,
i: uuid()
}
const response = await this.awaitResponse(message)
return response.v
}
}
export { Api }
{
"name": "@djinlist/ipc",
"version": "1.0.0",
"type": "module",
"license": "UNLICENSED",
"private": true,
"main": "src/index.js",
"exports": {
"./server": "./src/server/index.js",
"./client": "./src/client/index.js"
},
"dependencies": {
"varint": "^6.0.0",
"uuid": "^8.0.0"
}
}
import path from 'path'
import url from 'url'
import { access, mkdir, constants } from 'fs'
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const IPC_DIRNAME = path.join(__dirname, '..', '..', '..', 'env', 'tmp', 'ipc')
await new Promise((resolve) => {
access(path.join(__dirname, '..', '..', '..', 'env', 'tmp', 'ipc'), constants.F_OK, (err) => {
if (err) {
mkdir(path.join(__dirname, '..', '..', '..', 'env', 'tmp', 'ipc'), { recursive: true }, (err) => {
if (typeof err === 'undefined') {
resolve()
}
})
} else {
resolve()
}
})
})
const DATASTORE_PIPENAME = path.join(IPC_DIRNAME, 'datastore.ipc')
export { IPC_DIRNAME, DATASTORE_PIPENAME }
{
"name": "@djinlist/env",
"version": "1.0.0",
"type": "module",
"license": "UNLICENSED",
"private": true,
"main": "src/index.js",
"dependencies": {
"lodash": "^4.17.15"
}
}
module.exports = {
apps : [{
name : "frontend",
cwd: './app/djiny',
script : "pnpm",
args: "run dev",
env_production: {
NODE_ENV: "production"
},
env_development: {
NODE_ENV: "development"
}
},{
name : "datastore",
cwd: './services/datastore_ipc_server',
script : "./src/index.js",
},{
name : "channel",
cwd: './services/channel_ws_server',
script : "./src/index.js",
},{
name : "user_portal",
cwd: './services/user_portal_http_server',
script : "./src/index.js",
}]
}
digraph{
subgraph cluster10604544 {
label="Page 10604544, rc 0 112";
color=black;
n_10604544_0[label="0: Inode(HT7GA5KVLSRV6):global.css -> Inode(SVLEQ7VDSJCXU)"];
n_10604544_0->n_10604544_1[color="blue"];
n_10604544_1[label="1: Inode(QIVCPWRQJNZS2):albums.svelte -> Inode(66XEAAZWKFAIE)"];
n_10604544_1->n_10604544_2[color="blue"];
n_10604544_2[label="2: Inode(3DJEOGW5L5HCE): -> Inode(3DJEOGW5L5HCE)"];
}
n_10604544_0->n_10608640_0[color="ForestGreen"];
n_10604544_0->n_9728000_0[color="red"];
n_10604544_1->n_9756672_0[color="red"];
n_10604544_2->n_9805824_0[color="red"];
subgraph cluster10608640 {
label="Page 10608640, rc 0 2732";
color=black;
n_10608640_0[label="0: Inode(AAAAAAAAAAAAA):.autoenv.zsh -> Inode(Q2IFVLZRC36F4)"];
n_10608640_0->n_10608640_1[color="blue"];
n_10608640_1[label="1: Inode(AAAAAAAAAAAAA):.bin -> Inode(WQMKGUAN3HGPM)"];
n_10608640_1->n_10608640_2[color="blue"];
n_10608640_2[label="2: Inode(AAAAAAAAAAAAA):.ignore -> Inode(6OEXOKTZKXN2M)"];
n_10608640_2->n_10608640_3[color="blue"];
n_10608640_3[label="3: Inode(AAAAAAAAAAAAA):.nginx -> Inode(PZD6LOZIK35ES)"];
n_10608640_3->n_10608640_4[color="blue"];
n_10608640_4[label="4: Inode(AAAAAAAAAAAAA):.node-version -> Inode(VHCKXZRYKYWPO)"];
n_10608640_4->n_10608640_5[color="blue"];
n_10608640_5[label="5: Inode(AAAAAAAAAAAAA):.npmrc -> Inode(QG2K5UAACTJVM)"];
n_10608640_5->n_10608640_6[color="blue"];
n_10608640_6[label="6: Inode(AAAAAAAAAAAAA):.prettierignore -> Inode(DAAA7FS2JR7VY)"];
n_10608640_6->n_10608640_7[color="blue"];
n_10608640_7[label="7: Inode(AAAAAAAAAAAAA):.prettierrc -> Inode(BHKWO2MUFHJJI)"];
n_10608640_7->n_10608640_8[color="blue"];
n_10608640_8[label="8: Inode(AAAAAAAAAAAAA):.stylelintignore -> Inode(K3N24GAKRRFAS)"];
n_10608640_8->n_10608640_9[color="blue"];
n_10608640_9[label="9: Inode(AAAAAAAAAAAAA):.stylelintrc -> Inode(ZQYD3PLZGJFS2)"];
n_10608640_9->n_10608640_10[color="blue"];
n_10608640_10[label="10: Inode(AAAAAAAAAAAAA):app -> Inode(65TLXQ7KZYYJ4)"];
n_10608640_10->n_10608640_11[color="blue"];
n_10608640_11[label="11: Inode(AAAAAAAAAAAAA):ecosystem.config.js -> Inode(BH3N27R4V72E2)"];
n_10608640_11->n_10608640_12[color="blue"];
n_10608640_12[label="12: Inode(AAAAAAAAAAAAA):lib -> Inode(BLFA4SONQC4RC)"];
n_10608640_12->n_10608640_13[color="blue"];
n_10608640_13[label="13: Inode(AAAAAAAAAAAAA):package.json -> Inode(2RCNJFCVEVJD6)"];
n_10608640_13->n_10608640_14[color="blue"];
n_10608640_14[label="14: Inode(AAAAAAAAAAAAA):pnpm-lock.yaml -> Inode(VXCTX4J7VNL4Y)"];
n_10608640_14->n_10608640_15[color="blue"];
n_10608640_15[label="15: Inode(AAAAAAAAAAAAA):pnpm-workspace.yaml -> Inode(FD4VNPTDANJZ2)"];
n_10608640_15->n_10608640_16[color="blue"];
n_10608640_16[label="16: Inode(AAAAAAAAAAAAA):proxy.conf -> Inode(3EIM3JNV6WSTS)"];
n_10608640_16->n_10608640_17[color="blue"];
n_10608640_17[label="17: Inode(AAAAAAAAAAAAA):redirect.conf -> Inode(XNV3NBFDAM4HA)"];
n_10608640_17->n_10608640_18[color="blue"];
n_10608640_18[label="18: Inode(AAAAAAAAAAAAA):services -> Inode(CFCFTRHVRS3NE)"];
n_10608640_18->n_10608640_19[color="blue"];
n_10608640_19[label="19: Inode(AYO4INBMW4MC6): -> Inode(AYO4INBMW4MC6)"];
n_10608640_19->n_10608640_20[color="blue"];
n_10608640_20[label="20: Inode(AYO4INBMW4MC6):index.svelte -> Inode(XVC4OAALSIDVE)"];
n_10608640_20->n_10608640_21[color="blue"];
n_10608640_21[label="21: Inode(AYO4INBMW4MC6):wave.svelte -> Inode(4LQVCAQ57DFRM)"];
n_10608640_21->n_10608640_22[color="blue"];
n_10608640_22[label="22: Inode(BC72NKTJI6CHK): -> Inode(BC72NKTJI6CHK)"];
n_10608640_22->n_10608640_23[color="blue"];
n_10608640_23[label="23: Inode(BC72NKTJI6CHK):_error.svelte -> Inode(WRZLXXZBLEGN6)"];
n_10608640_23->n_10608640_24[color="blue"];
n_10608640_24[label="24: Inode(BC72NKTJI6CHK):_global.svelte -> Inode(DQOV3W2AJK2WM)"];
n_10608640_24->n_10608640_25[color="blue"];
n_10608640_25[label="25: Inode(BC72NKTJI6CHK):_layout.svelte -> Inode(TUHKQKMA5TYD2)"];
n_10608640_25->n_10608640_26[color="blue"];
n_10608640_26[label="26: Inode(BC72NKTJI6CHK):_oauth -> Inode(LFVZUHPU5CAKS)"];
n_10608640_26->n_10608640_27[color="blue"];
n_10608640_27[label="27: Inode(BC72NKTJI6CHK):event.svelte -> Inode(SHOPTS5SEAO4Q)"];
n_10608640_27->n_10608640_28[color="blue"];
n_10608640_28[label="28: Inode(BC72NKTJI6CHK):index.svelte -> Inode(2CZ5FAIFDGJW2)"];
n_10608640_28->n_10608640_29[color="blue"];
n_10608640_29[label="29: Inode(BLFA4SONQC4RC): -> Inode(BLFA4SONQC4RC)"];
n_10608640_29->n_10608640_30[color="blue"];
n_10608640_30[label="30: Inode(BLFA4SONQC4RC):datastore -> Inode(TACHSLXLDQETM)"];
n_10608640_30->n_10608640_31[color="blue"];
n_10608640_31[label="31: Inode(CFCFTRHVRS3NE): -> Inode(CFCFTRHVRS3NE)"];
n_10608640_31->n_10608640_32[color="blue"];
n_10608640_32[label="32: Inode(CFCFTRHVRS3NE):pnpm-lock.yaml -> Inode(6YHCEN6RVEXUE)"];
n_10608640_32->n_10608640_33[color="blue"];
n_10608640_33[label="33: Inode(CFCFTRHVRS3NE):pnpm-workspace.yaml -> Inode(QSQHBRGBBX2GU)"];
n_10608640_33->n_10608640_34[color="blue"];
n_10608640_34[label="34: Inode(COXOPYDYKF3XG): -> Inode(COXOPYDYKF3XG)"];
n_10608640_34->n_10608640_35[color="blue"];
n_10608640_35[label="35: Inode(COXOPYDYKF3XG):api.js.dep -> Inode(YNCX42XQFZ65G)"];
n_10608640_35->n_10608640_36[color="blue"];
n_10608640_36[label="36: Inode(COXOPYDYKF3XG):app.html -> Inode(UAUPZ7OL2HPSW)"];
n_10608640_36->n_10608640_37[color="blue"];
n_10608640_37[label="37: Inode(COXOPYDYKF3XG):client.js.dep -> Inode(NSGYKJ6WNYTWC)"];
n_10608640_37->n_10608640_38[color="blue"];
n_10608640_38[label="38: Inode(COXOPYDYKF3XG):lib -> Inode(7GVTKXM3HLWNQ)"];
n_10608640_38->n_10608640_39[color="blue"];
n_10608640_39[label="39: Inode(COXOPYDYKF3XG):routes -> Inode(SPVUATTTWOMBC)"];
n_10608640_39->n_10608640_40[color="blue"];
n_10608640_40[label="40: Inode(CVXDRUYJN25PO): -> Inode(CVXDRUYJN25PO)"];
n_10608640_40->n_10608640_41[color="blue"];
n_10608640_41[label="41: Inode(CVXDRUYJN25PO):album.svelte -> Inode(Q3LJU42ZMZW5U)"];
n_10608640_41->n_10608640_42[color="blue"];
n_10608640_42[label="42: Inode(CVXDRUYJN25PO):artist.svelte -> Inode(NGCHD7KEOYI4Q)"];
n_10608640_42->n_10608640_43[color="blue"];
n_10608640_43[label="43: Inode(CVXDRUYJN25PO):playlist.svelte -> Inode(VFGER7YRVYQLQ)"];
n_10608640_43->n_10608640_44[color="blue"];
n_10608640_44[label="44: Inode(CVXDRUYJN25PO):track.svelte -> Inode(QU6MSJXO7RQ2A)"];
n_10608640_44->n_10608640_45[color="blue"];
n_10608640_45[label="45: Inode(C3ZNORJPW55RC): -> Inode(C3ZNORJPW55RC)"];
n_10608640_45->n_10608640_46[color="blue"];
n_10608640_46[label="46: Inode(C3ZNORJPW55RC):empty_object.js -> Inode(6R52MGAG4BSWO)"];
n_10608640_46->n_10608640_47[color="blue"];
n_10608640_47[label="47: Inode(DD3J6NXBHNLGU): -> Inode(DD3J6NXBHNLGU)"];
n_10608640_47->n_10608640_48[color="blue"];
n_10608640_48[label="48: Inode(DD3J6NXBHNLGU):datastore -> Inode(6TMTDO45E3YQY)"];
n_10608640_48->n_10608640_49[color="blue"];
n_10608640_49[label="49: Inode(DD3J6NXBHNLGU):datastore.js -> Inode(TZ6UPCSH3BSLI)"];
n_10608640_49->n_10608640_50[color="blue"];
n_10608640_50[label="50: Inode(ESKHFJ2QX7P5C): -> Inode(ESKHFJ2QX7P5C)"];
n_10608640_50->n_10608640_51[color="blue"];
n_10608640_51[label="51: Inode(ESKHFJ2QX7P5C):index.js -> Inode(IG3HCGWHCKBNK)"];
n_10608640_51->n_10608640_52[color="blue"];
n_10608640_52[label="52: Inode(EWGIKU7N4TFWO): -> Inode(EWGIKU7N4TFWO)"];
n_10608640_52->n_10608640_53[color="blue"];
n_10608640_53[label="53: Inode(EWGIKU7N4TFWO):controlenvy_pointer.js -> Inode(KO67Z3WXJTYGW)"];
n_10608640_53->n_10608640_54[color="blue"];
n_10608640_54[label="54: Inode(EWGIKU7N4TFWO):controlenvy_topic.js -> Inode(QGCRDEDFXUZHM)"];
n_10608640_54->n_10608640_55[color="blue"];
n_10608640_55[label="55: Inode(EWGIKU7N4TFWO):pointer.js -> Inode(K3TC36J7SYF7U)"];
n_10608640_55->n_10608640_56[color="blue"];
n_10608640_56[label="56: Inode(EWGIKU7N4TFWO):topic.js -> Inode(XQJT6RBLBSTAK)"];
n_10608640_56->n_10608640_57[color="blue"];
n_10608640_57[label="57: Inode(FACHIVHPFYCQA): -> Inode(FACHIVHPFYCQA)"];
n_10608640_57->n_10608640_58[color="blue"];
n_10608640_58[label="58: Inode(FACHIVHPFYCQA):followed.svelte -> Inode(QCUQSEAR7KL2U)"];
n_10608640_58->n_10608640_59[color="blue"];
n_10608640_59[label="59: Inode(FACHIVHPFYCQA):owned.svelte -> Inode(VXLRVVS37U56C)"];
n_10608640_59->n_10608640_60[color="blue"];
n_10608640_60[label="60: Inode(FNO44B3I6OJIG): -> Inode(FNO44B3I6OJIG)"];
n_10608640_60->n_10608640_61[color="blue"];
n_10608640_61[label="61: Inode(FNO44B3I6OJIG):example.json -> Inode(GX2BLCQQ3Q72K)"];
n_10608640_61->n_10608640_62[color="blue"];
n_10608640_62[label="62: Inode(FPEFGAHOE333Y): -> Inode(FPEFGAHOE333Y)"];
n_10608640_62->n_10608640_63[color="blue"];
n_10608640_63[label="63: Inode(FWTL334DDZDFG): -> Inode(FWTL334DDZDFG)"];
n_10608640_63->n_10608640_64[color="blue"];
n_10608640_64[label="64: Inode(FWTL334DDZDFG):index.js -> Inode(IBVEXPVMM7YRW)"];
n_10608640_64->n_10608640_65[color="blue"];
n_10608640_65[label="65: Inode(GHPSHGSWVRTC2): -> Inode(GHPSHGSWVRTC2)"];
n_10608640_65->n_10608640_66[color="blue"];
n_10608640_66[label="66: Inode(GHPSHGSWVRTC2):.autoenv.zsh -> Inode(QPSRGSGOLZTPI)"];
n_10608640_66->n_10608640_67[color="blue"];
n_10608640_67[label="67: Inode(GHPSHGSWVRTC2):.bin -> Inode(WYD4M6RXKB7OC)"];
n_10608640_67->n_10608640_68[color="blue"];
n_10608640_68[label="68: Inode(GHPSHGSWVRTC2):.gitignore -> Inode(UVTRBKEMBJXYK)"];
n_10608640_68->n_10608640_69[color="blue"];
n_10608640_69[label="69: Inode(GHPSHGSWVRTC2):README.md -> Inode(UNZETESGWNSRC)"];
n_10608640_69->n_10608640_70[color="blue"];
n_10608640_70[label="70: Inode(GHPSHGSWVRTC2):cypress -> Inode(7BD75A5OV3RRU)"];
n_10608640_70->n_10608640_71[color="blue"];
n_10608640_71[label="71: Inode(GHPSHGSWVRTC2):package.json -> Inode(UQDJX74DAJPIG)"];
n_10608640_71->n_10608640_72[color="blue"];
n_10608640_72[label="72: Inode(GHPSHGSWVRTC2):rollup.config.js -> Inode(G5AGB5VNI7ZS2)"];
n_10608640_72->n_10608640_73[color="blue"];
n_10608640_73[label="73: Inode(GHPSHGSWVRTC2):src -> Inode(HT7GA5KVLSRV6)"];
n_10608640_73->n_10608640_74[color="blue"];
n_10608640_74[label="74: Inode(GHPSHGSWVRTC2):static -> Inode(VSXBCX4TPIMJ4)"];
n_10608640_74->n_10608640_75[color="blue"];
n_10608640_75[label="75: Inode(GN4XK57WEBIZ2): -> Inode(GN4XK57WEBIZ2)"];
n_10608640_75->n_10608640_76[color="blue"];
n_10608640_76[label="76: Inode(GN4XK57WEBIZ2):album.svelte -> Inode(DQ3OVUM3IUDYU)"];
n_10608640_76->n_10608640_77[color="blue"];
n_10608640_77[label="77: Inode(GN4XK57WEBIZ2):artist.svelte -> Inode(J6J2YZLLNIEBI)"];
n_10608640_77->n_10608640_78[color="blue"];
n_10608640_78[label="78: Inode(GN4XK57WEBIZ2):playlist.svelte -> Inode(BKTJTGA64SXDQ)"];
n_10608640_78->n_10608640_79[color="blue"];
n_10608640_79[label="79: Inode(GN4XK57WEBIZ2):track.svelte -> Inode(UHI7XCDD3VDI4)"];
n_10608640_79->n_10608640_80[color="blue"];
n_10608640_80[label="80: Inode(HDJNI6MNYEHJQ): -> Inode(HDJNI6MNYEHJQ)"];
n_10608640_80->n_10608640_81[color="blue"];
n_10608640_81[label="81: Inode(HDJNI6MNYEHJQ):global.css -> Inode(XXXHI67UNHQVK)"];
n_10608640_81->n_10608640_82[color="blue"];
n_10608640_82[label="82: Inode(HT7GA5KVLSRV6): -> Inode(HT7GA5KVLSRV6)"];
n_10608640_82->n_10608640_83[color="blue"];
n_10608640_83[label="83: Inode(HT7GA5KVLSRV6):bootstrap.js -> Inode(SFB6HENNG2M5A)"];
n_10608640_83->n_10608640_84[color="blue"];
n_10608640_84[label="84: Inode(HT7GA5KVLSRV6):client.js -> Inode(HZVGFJJV77CPQ)"];
n_10608640_84->n_10608640_85[color="blue"];
n_10608640_85[label="85: Inode(HT7GA5KVLSRV6):components -> Inode(5ODKHB7CLODEU)"];
}
subgraph cluster9728000 {
label="Page 9728000, rc 0 2270";
color=black;
n_9728000_0[label="0: Inode(HT7GA5KVLSRV6):lib -> Inode(QZDUDJDCZJFQC)"];
n_9728000_0->n_9728000_1[color="blue"];
n_9728000_1[label="1: Inode(HT7GA5KVLSRV6):routes -> Inode(BC72NKTJI6CHK)"];
n_9728000_1->n_9728000_2[color="blue"];
n_9728000_2[label="2: Inode(HT7GA5KVLSRV6):server.js -> Inode(4LRQK6WXFX5V4)"];
n_9728000_2->n_9728000_3[color="blue"];
n_9728000_3[label="3: Inode(HT7GA5KVLSRV6):service-worker.js -> Inode(PBOADSJ5HZP4I)"];
n_9728000_3->n_9728000_4[color="blue"];
n_9728000_4[label="4: Inode(HT7GA5KVLSRV6):template.html -> Inode(NK4AJB5QYKL6I)"];
n_9728000_4->n_9728000_5[color="blue"];
n_9728000_5[label="5: Inode(IIJXKYQFPEGVO): -> Inode(IIJXKYQFPEGVO)"];
n_9728000_5->n_9728000_6[color="blue"];
n_9728000_6[label="6: Inode(IIJXKYQFPEGVO):index.js -> Inode(QJSNITID4KDNO)"];
n_9728000_6->n_9728000_7[color="blue"];
n_9728000_7[label="7: Inode(JWFQTQXTTBAM4): -> Inode(JWFQTQXTTBAM4)"];
n_9728000_7->n_9728000_8[color="blue"];
n_9728000_8[label="8: Inode(JWFQTQXTTBAM4):album.svelte -> Inode(VRA6U3PZUW5WQ)"];
n_9728000_8->n_9728000_9[color="blue"];
n_9728000_9[label="9: Inode(JWFQTQXTTBAM4):album_track.svelte -> Inode(QLHMQ5W5CGJIO)"];
n_9728000_9->n_9728000_10[color="blue"];
n_9728000_10[label="10: Inode(JWFQTQXTTBAM4):playlist.svelte -> Inode(5CJTHSCSD76MA)"];
n_9728000_10->n_9728000_11[color="blue"];
n_9728000_11[label="11: Inode(JWFQTQXTTBAM4):track.svelte -> Inode(H57OXKYNDTI44)"];
n_9728000_11->n_9728000_12[color="blue"];
n_9728000_12[label="12: Inode(J6JGAUVUWUSBO): -> Inode(J6JGAUVUWUSBO)"];
n_9728000_12->n_9728000_13[color="blue"];
n_9728000_13[label="13: Inode(J6JGAUVUWUSBO):client.svelte -> Inode(QW7QHVJARSXUU)"];
n_9728000_13->n_9728000_14[color="blue"];
n_9728000_14[label="14: Inode(KILB4TFGRUWYO): -> Inode(KILB4TFGRUWYO)"];
n_9728000_14->n_9728000_15[color="blue"];
n_9728000_15[label="15: Inode(KILB4TFGRUWYO):user.js -> Inode(5IUPI52DKPDBS)"];
n_9728000_15->n_9728000_16[color="blue"];
n_9728000_16[label="16: Inode(LFVZUHPU5CAKS): -> Inode(LFVZUHPU5CAKS)"];
n_9728000_16->n_9728000_17[color="blue"];
n_9728000_17[label="17: Inode(LFVZUHPU5CAKS):pin.svelte -> Inode(SG2UN5I7QLN7A)"];
n_9728000_17->n_9728000_18[color="blue"];
n_9728000_18[label="18: Inode(LVAQPHMUKJPW2): -> Inode(LVAQPHMUKJPW2)"];
n_9728000_18->n_9728000_19[color="blue"];
n_9728000_19[label="19: Inode(LVAQPHMUKJPW2):_pin.svelte -> Inode(5SMMKDRXWOKGG)"];
n_9728000_19->n_9728000_20[color="blue"];
n_9728000_20[label="20: Inode(LVAQPHMUKJPW2):admin.svelte -> Inode(TINDCAXEPW6XC)"];
n_9728000_20->n_9728000_21[color="blue"];
n_9728000_21[label="21: Inode(LVAQPHMUKJPW2):client.svelte -> Inode(IIIJPMO6KEAAW)"];
n_9728000_21->n_9728000_22[color="blue"];
n_9728000_22[label="22: Inode(LXA4G77TCZMIA): -> Inode(LXA4G77TCZMIA)"];
n_9728000_22->n_9728000_23[color="blue"];
n_9728000_23[label="23: Inode(LXA4G77TCZMIA):_account -> Inode(3F4CWNT24Y26K)"];
n_9728000_23->n_9728000_24[color="blue"];
n_9728000_24[label="24: Inode(LXA4G77TCZMIA):_event_select -> Inode(TKXEXNWSWJ7KU)"];
n_9728000_24->n_9728000_25[color="blue"];
n_9728000_25[label="25: Inode(LXA4G77TCZMIA):_event_select.svelte -> Inode(263KQXTUNMAE6)"];
n_9728000_25->n_9728000_26[color="blue"];
n_9728000_26[label="26: Inode(LXA4G77TCZMIA):_log_in.svelte -> Inode(ZRNEIHKDP6WYM)"];
n_9728000_26->n_9728000_27[color="blue"];
n_9728000_27[label="27: Inode(LXA4G77TCZMIA):_sign_up.svelte -> Inode(I5V3M6EGCACT2)"];
n_9728000_27->n_9728000_28[color="blue"];
n_9728000_28[label="28: Inode(LXA4G77TCZMIA):account.svelte -> Inode(UDRU4XIBL6S3Q)"];
n_9728000_28->n_9728000_29[color="blue"];
n_9728000_29[label="29: Inode(LXA4G77TCZMIA):explore -> Inode(XS7ILRT4DODSM)"];
n_9728000_29->n_9728000_30[color="blue"];
n_9728000_30[label="30: Inode(LXA4G77TCZMIA):index.svelte -> Inode(YBO7PSIJXEWMW)"];
n_9728000_30->n_9728000_31[color="blue"];
n_9728000_31[label="31: Inode(LXA4G77TCZMIA):info.svelte -> Inode(SA42HG3UET25C)"];
n_9728000_31->n_9728000_32[color="blue"];
n_9728000_32[label="32: Inode(LXA4G77TCZMIA):tallies -> Inode(4QGMJHUA2VKVY)"];
n_9728000_32->n_9728000_33[color="blue"];
n_9728000_33[label="33: Inode(L4F3IGWMZ2T7O): -> Inode(L4F3IGWMZ2T7O)"];
n_9728000_33->n_9728000_34[color="blue"];
n_9728000_34[label="34: Inode(L4F3IGWMZ2T7O):area.yaml -> Inode(6LYALMMF5QBVW)"];
n_9728000_34->n_9728000_35[color="blue"];
n_9728000_35[label="35: Inode(L4F3IGWMZ2T7O):component.yaml -> Inode(C5M45PW73QPK2)"];
n_9728000_35->n_9728000_36[color="blue"];
n_9728000_36[label="36: Inode(L4F3IGWMZ2T7O):room.yaml -> Inode(2SCBQIIGC5VNY)"];
n_9728000_36->n_9728000_37[color="blue"];
n_9728000_37[label="37: Inode(L4F3IGWMZ2T7O):schema.yaml -> Inode(OIVBAWVB7O7QK)"];
n_9728000_37->n_9728000_38[color="blue"];
n_9728000_38[label="38: Inode(L4F3IGWMZ2T7O):schema_tree.yaml -> Inode(7MVP25HSK2FOS)"];
n_9728000_38->n_9728000_39[color="blue"];
n_9728000_39[label="39: Inode(L4F3IGWMZ2T7O):system.yaml -> Inode(3UFVMJGXBEKC2)"];
n_9728000_39->n_9728000_40[color="blue"];
n_9728000_40[label="40: Inode(MWO6HOVXPRJHC): -> Inode(MWO6HOVXPRJHC)"];
n_9728000_40->n_9728000_41[color="blue"];
n_9728000_41[label="41: Inode(MWO6HOVXPRJHC):event_auth.js -> Inode(46ZG3OGE7LSNW)"];
n_9728000_41->n_9728000_42[color="blue"];
n_9728000_42[label="42: Inode(MWO6HOVXPRJHC):user_auth.js -> Inode(FADI6NLB3NNA4)"];
n_9728000_42->n_9728000_43[color="blue"];
n_9728000_43[label="43: Inode(N2ELXII4EF5PE): -> Inode(N2ELXII4EF5PE)"];
n_9728000_43->n_9728000_44[color="blue"];
n_9728000_44[label="44: Inode(N2ELXII4EF5PE):datastore -> Inode(UEQ6N3RUPIS72)"];
n_9728000_44->n_9728000_45[color="blue"];
n_9728000_45[label="45: Inode(N2ELXII4EF5PE):datastore.js -> Inode(R54CACJWN5RJQ)"];
n_9728000_45->n_9728000_46[color="blue"];
n_9728000_46[label="46: Inode(N2ELXII4EF5PE):datastore.yaml -> Inode(7AHKCC4GE5HIA)"];
n_9728000_46->n_9728000_47[color="blue"];
n_9728000_47[label="47: Inode(N2ELXII4EF5PE):fixtures -> Inode(L4F3IGWMZ2T7O)"];
n_9728000_47->n_9728000_48[color="blue"];
n_9728000_48[label="48: Inode(N2ELXII4EF5PE):helper.js -> Inode(UUEVN7POPWZRI)"];
n_9728000_48->n_9728000_49[color="blue"];
n_9728000_49[label="49: Inode(N2ELXII4EF5PE):pointer -> Inode(5USYOF7HSPTLS)"];
n_9728000_49->n_9728000_50[color="blue"];
n_9728000_50[label="50: Inode(N2ELXII4EF5PE):pointer.js -> Inode(3TYWLIY6W44C6)"];
n_9728000_50->n_9728000_51[color="blue"];
n_9728000_51[label="51: Inode(N2ELXII4EF5PE):schema.js -> Inode(PCXWS2WVHV656)"];
n_9728000_51->n_9728000_52[color="blue"];
n_9728000_52[label="52: Inode(N2ELXII4EF5PE):tests.yaml -> Inode(SRWSU45S4MXUQ)"];
n_9728000_52->n_9728000_53[color="blue"];
n_9728000_53[label="53: Inode(N2ELXII4EF5PE):topic_tree.js -> Inode(OW23H46BY5XGG)"];
n_9728000_53->n_9728000_54[color="blue"];
n_9728000_54[label="54: Inode(N2ELXII4EF5PE):v3-compatible -> Inode(WB6PIH4O4EPXQ)"];
n_9728000_54->n_9728000_55[color="blue"];
n_9728000_55[label="55: Inode(O6W4GJHUO2OQY): -> Inode(O6W4GJHUO2OQY)"];
n_9728000_55->n_9728000_56[color="blue"];
n_9728000_56[label="56: Inode(O6W4GJHUO2OQY):grid.svelte -> Inode(TRKUPEIKQLPYI)"];
n_9728000_56->n_9728000_57[color="blue"];
n_9728000_57[label="57: Inode(O6W4GJHUO2OQY):header -> Inode(Y74VQVJLZBCQK)"];
n_9728000_57->n_9728000_58[color="blue"];
n_9728000_58[label="58: Inode(O6W4GJHUO2OQY):index.svelte -> Inode(UZP2LKGF5KUWM)"];
n_9728000_58->n_9728000_59[color="blue"];
n_9728000_59[label="59: Inode(O6W4GJHUO2OQY):inspect -> Inode(JWFQTQXTTBAM4)"];
n_9728000_59->n_9728000_60[color="blue"];
n_9728000_60[label="60: Inode(O6W4GJHUO2OQY):inspect.svelte -> Inode(ZDMRTTEPFX2YW)"];
n_9728000_60->n_9728000_61[color="blue"];
n_9728000_61[label="61: Inode(O6W4GJHUO2OQY):inspect_router.svelte -> Inode(R5KAAVZJDOBTO)"];
n_9728000_61->n_9728000_62[color="blue"];
n_9728000_62[label="62: Inode(O6W4GJHUO2OQY):item.svelte -> Inode(REK2TCSSCZXNM)"];
n_9728000_62->n_9728000_63[color="blue"];
n_9728000_63[label="63: Inode(O6W4GJHUO2OQY):line -> Inode(CVXDRUYJN25PO)"];
n_9728000_63->n_9728000_64[color="blue"];
n_9728000_64[label="64: Inode(O6W4GJHUO2OQY):list.svelte -> Inode(NONQLJPKV7RLK)"];
n_9728000_64->n_9728000_65[color="blue"];
n_9728000_65[label="65: Inode(O6W4GJHUO2OQY):meta -> Inode(GN4XK57WEBIZ2)"];
n_9728000_65->n_9728000_66[color="blue"];
n_9728000_66[label="66: Inode(O6W4GJHUO2OQY):search_results.svelte -> Inode(HGSX5EUHWOQEE)"];
n_9728000_66->n_9728000_67[color="blue"];
n_9728000_67[label="67: Inode(PIUOP5YSQE4RC): -> Inode(PIUOP5YSQE4RC)"];
n_9728000_67->n_9728000_68[color="blue"];
n_9728000_68[label="68: Inode(PZD6LOZIK35ES): -> Inode(PZD6LOZIK35ES)"];
n_9728000_68->n_9728000_69[color="blue"];
n_9728000_69[label="69: Inode(QIVCPWRQJNZS2): -> Inode(QIVCPWRQJNZS2)"];
n_9728000_69->n_9728000_70[color="blue"];
n_9728000_70[label="70: Inode(QIVCPWRQJNZS2):album.svelte -> Inode(XPATCSY5XREGU)"];
}
subgraph cluster9756672 {
label="Page 9756672, rc 0 3410";
color=black;
n_9756672_0[label="0: Inode(QIVCPWRQJNZS2):categories.svelte -> Inode(26I4RIFGWU76E)"];
n_9756672_0->n_9756672_1[color="blue"];
n_9756672_1[label="1: Inode(QIVCPWRQJNZS2):category.svelte -> Inode(6G3INXRFY7DNK)"];
n_9756672_1->n_9756672_2[color="blue"];
n_9756672_2[label="2: Inode(QIVCPWRQJNZS2):playlist.svelte -> Inode(CGAUFIV4SCO62)"];
n_9756672_2->n_9756672_3[color="blue"];
n_9756672_3[label="3: Inode(QIVCPWRQJNZS2):playlists.svelte -> Inode(R4RI26S4ZXTEQ)"];
n_9756672_3->n_9756672_4[color="blue"];
n_9756672_4[label="4: Inode(QIVCPWRQJNZS2):search_results.svelte -> Inode(ZPZ73HIMHBPJM)"];
n_9756672_4->n_9756672_5[color="blue"];
n_9756672_5[label="5: Inode(QIVCPWRQJNZS2):track.svelte -> Inode(ASLPYCAKNA43A)"];
n_9756672_5->n_9756672_6[color="blue"];
n_9756672_6[label="6: Inode(QIVCPWRQJNZS2):tracks.svelte -> Inode(7MV7KPBTB3QVQ)"];
n_9756672_6->n_9756672_7[color="blue"];
n_9756672_7[label="7: Inode(QWW5O47FCNWJC): -> Inode(QWW5O47FCNWJC)"];
n_9756672_7->n_9756672_8[color="blue"];
n_9756672_8[label="8: Inode(QZDUDJDCZJFQC): -> Inode(QZDUDJDCZJFQC)"];
n_9756672_8->n_9756672_9[color="blue"];
n_9756672_9[label="9: Inode(QZDUDJDCZJFQC):authorization.js -> Inode(S5QXOGFCOWY2M)"];
n_9756672_9->n_9756672_10[color="blue"];
n_9756672_10[label="10: Inode(QZDUDJDCZJFQC):send.js -> Inode(MQSRSSLXTSELC)"];
n_9756672_10->n_9756672_11[color="blue"];
n_9756672_11[label="11: Inode(QZDUDJDCZJFQC):spotify -> Inode(PIUOP5YSQE4RC)"];
n_9756672_11->n_9756672_12[color="blue"];
n_9756672_12[label="12: Inode(QZDUDJDCZJFQC):subscribe.js -> Inode(E2HPDKYVMEWZG)"];
n_9756672_12->n_9756672_13[color="blue"];
n_9756672_13[label="13: Inode(QZDUDJDCZJFQC):theme.js -> Inode(6RYHU3BG66OKE)"];
n_9756672_13->n_9756672_14[color="blue"];
n_9756672_14[label="14: Inode(QZDUDJDCZJFQC):unsubscribe.js -> Inode(G7XTEX2JUJDCG)"];
n_9756672_14->n_9756672_15[color="blue"];
n_9756672_15[label="15: Inode(QZDUDJDCZJFQC):websocket -> Inode(IIJXKYQFPEGVO)"];
n_9756672_15->n_9756672_16[color="blue"];
n_9756672_16[label="16: Inode(SPVUATTTWOMBC): -> Inode(SPVUATTTWOMBC)"];
n_9756672_16->n_9756672_17[color="blue"];
n_9756672_17[label="17: Inode(SPVUATTTWOMBC):__error.svelte -> Inode(V3TG7DZP2376A)"];
n_9756672_17->n_9756672_18[color="blue"];
n_9756672_18[label="18: Inode(SPVUATTTWOMBC):__layout.svelte -> Inode(AUT5SNQA2HDNK)"];
n_9756672_18->n_9756672_19[color="blue"];
n_9756672_19[label="19: Inode(SPVUATTTWOMBC):admin -> Inode(FPEFGAHOE333Y)"];
n_9756672_19->n_9756672_20[color="blue"];
n_9756672_20[label="20: Inode(SPVUATTTWOMBC):client -> Inode(LXA4G77TCZMIA)"];
n_9756672_20->n_9756672_21[color="blue"];
n_9756672_21[label="21: Inode(SPVUATTTWOMBC):index.svelte -> Inode(JDSTTHRVQEQO6)"];
n_9756672_21->n_9756672_22[color="blue"];
n_9756672_22[label="22: Inode(SPVUATTTWOMBC):oauth -> Inode(LVAQPHMUKJPW2)"];
n_9756672_22->n_9756672_23[color="blue"];
n_9756672_23[label="23: Inode(S6K7IPAUN7LBY): -> Inode(S6K7IPAUN7LBY)"];
n_9756672_23->n_9756672_24[color="blue"];
n_9756672_24[label="24: Inode(S6K7IPAUN7LBY):boolean.svelte -> Inode(RPPTHPVGYWZ6C)"];
n_9756672_24->n_9756672_25[color="blue"];
n_9756672_25[label="25: Inode(S6K7IPAUN7LBY):filter.svelte -> Inode(FI3MAYGRVNIAU)"];
n_9756672_25->n_9756672_26[color="blue"];
n_9756672_26[label="26: Inode(S6K7IPAUN7LBY):select.svelte -> Inode(HDIGDKSZZP2Q2)"];
n_9756672_26->n_9756672_27[color="blue"];
n_9756672_27[label="27: Inode(S6K7IPAUN7LBY):string.svelte -> Inode(FGYYVSXN4TNNY)"];
n_9756672_27->n_9756672_28[color="blue"];
n_9756672_28[label="28: Inode(TACHSLXLDQETM): -> Inode(TACHSLXLDQETM)"];
n_9756672_28->n_9756672_29[color="blue"];
n_9756672_29[label="29: Inode(TACHSLXLDQETM):.gitignore -> Inode(ZLZM2ASTO3GTA)"];
n_9756672_29->n_9756672_30[color="blue"];
n_9756672_30[label="30: Inode(TACHSLXLDQETM):README.md -> Inode(ZVAHLLO3PNRZY)"];
n_9756672_30->n_9756672_31[color="blue"];
n_9756672_31[label="31: Inode(TACHSLXLDQETM):benchmark -> Inode(C3ZNORJPW55RC)"];
n_9756672_31->n_9756672_32[color="blue"];
n_9756672_32[label="32: Inode(TACHSLXLDQETM):package.json -> Inode(YM4XJWTH2XKVM)"];
n_9756672_32->n_9756672_33[color="blue"];
n_9756672_33[label="33: Inode(TACHSLXLDQETM):src -> Inode(DD3J6NXBHNLGU)"];
n_9756672_33->n_9756672_34[color="blue"];
n_9756672_34[label="34: Inode(TACHSLXLDQETM):test -> Inode(N2ELXII4EF5PE)"];
n_9756672_34->n_9756672_35[color="blue"];
n_9756672_35[label="35: Inode(TKXEXNWSWJ7KU): -> Inode(TKXEXNWSWJ7KU)"];
n_9756672_35->n_9756672_36[color="blue"];
n_9756672_36[label="36: Inode(TKXEXNWSWJ7KU):name.svelte -> Inode(RYIOCDX2ZP7O2)"];
n_9756672_36->n_9756672_37[color="blue"];
n_9756672_37[label="37: Inode(UEQ6N3RUPIS72): -> Inode(UEQ6N3RUPIS72)"];
n_9756672_37->n_9756672_38[color="blue"];
n_9756672_38[label="38: Inode(UEQ6N3RUPIS72):base.js -> Inode(AUFOXIVZYTB7K)"];
n_9756672_38->n_9756672_39[color="blue"];
n_9756672_39[label="39: Inode(UEQ6N3RUPIS72):chain.js -> Inode(NCV35UUVAJCM2)"];
n_9756672_39->n_9756672_40[color="blue"];
n_9756672_40[label="40: Inode(UEQ6N3RUPIS72):convenience.js -> Inode(ZE3ZKVKOYFSGS)"];
n_9756672_40->n_9756672_41[color="blue"];
n_9756672_41[label="41: Inode(UEQ6N3RUPIS72):external.js -> Inode(MAQN3T44U7KQI)"];
n_9756672_41->n_9756672_42[color="blue"];
n_9756672_42[label="42: Inode(UEQ6N3RUPIS72):hooks.js -> Inode(W6LCU4BX6FDKC)"];
n_9756672_42->n_9756672_43[color="blue"];
n_9756672_43[label="43: Inode(UEQ6N3RUPIS72):pubsub.js -> Inode(OKSU5WIOAMZRG)"];
n_9756672_43->n_9756672_44[color="blue"];
n_9756672_44[label="44: Inode(UEQ6N3RUPIS72):schema.js -> Inode(DAW22JP7F2KSA)"];
n_9756672_44->n_9756672_45[color="blue"];
n_9756672_45[label="45: Inode(UEQ6N3RUPIS72):transact.js -> Inode(Z2OUZ4VQAWP64)"];
n_9756672_45->n_9756672_46[color="blue"];
n_9756672_46[label="46: Inode(UFNCC47RMZ2R6): -> Inode(UFNCC47RMZ2R6)"];
n_9756672_46->n_9756672_47[color="blue"];
n_9756672_47[label="47: Inode(UFNCC47RMZ2R6):Icon.svelte -> Inode(4UMIMRFQB4JKY)"];
n_9756672_47->n_9756672_48[color="blue"];
n_9756672_48[label="48: Inode(UFNCC47RMZ2R6):background -> Inode(AYO4INBMW4MC6)"];
n_9756672_48->n_9756672_49[color="blue"];
n_9756672_49[label="49: Inode(UFNCC47RMZ2R6):global.svelte -> Inode(BX65WPXO3SBDK)"];
n_9756672_49->n_9756672_50[color="blue"];
n_9756672_50[label="50: Inode(UFNCC47RMZ2R6):name.svelte -> Inode(TNNGQOLKFRBSE)"];
n_9756672_50->n_9756672_51[color="blue"];
n_9756672_51[label="51: Inode(UFNCC47RMZ2R6):navigation -> Inode(J6JGAUVUWUSBO)"];
n_9756672_51->n_9756672_52[color="blue"];
n_9756672_52[label="52: Inode(UFNCC47RMZ2R6):prompts -> Inode(ZHWMZ7OLLI2DC)"];
n_9756672_52->n_9756672_53[color="blue"];
n_9756672_53[label="53: Inode(UFNCC47RMZ2R6):slots -> Inode(S6K7IPAUN7LBY)"];
n_9756672_53->n_9756672_54[color="blue"];
n_9756672_54[label="54: Inode(U5SVCHKJ2EPXQ): -> Inode(U5SVCHKJ2EPXQ)"];
n_9756672_54->n_9756672_55[color="blue"];
n_9756672_55[label="55: Inode(U5SVCHKJ2EPXQ):base.js -> Inode(RHGR5HAAWQUHA)"];
n_9756672_55->n_9756672_56[color="blue"];
n_9756672_56[label="56: Inode(U5SVCHKJ2EPXQ):convenience.js -> Inode(M772QGPRSESBA)"];
n_9756672_56->n_9756672_57[color="blue"];
n_9756672_57[label="57: Inode(U5SVCHKJ2EPXQ):external.js -> Inode(ZFL463SNDJCWK)"];
n_9756672_57->n_9756672_58[color="blue"];
n_9756672_58[label="58: Inode(U5SVCHKJ2EPXQ):pubsub.js -> Inode(M36WQCXBJTLKC)"];
n_9756672_58->n_9756672_59[color="blue"];
n_9756672_59[label="59: Inode(VDXMC7T3ZYEYG): -> Inode(VDXMC7T3ZYEYG)"];
n_9756672_59->n_9756672_60[color="blue"];
n_9756672_60[label="60: Inode(VDXMC7T3ZYEYG):coppice.js -> Inode(PLYE6FNYWKQY4)"];
n_9756672_60->n_9756672_61[color="blue"];
n_9756672_61[label="61: Inode(VDXMC7T3ZYEYG):is_coppice.js -> Inode(PTCUKEWM4Q6YG)"];
n_9756672_61->n_9756672_62[color="blue"];
n_9756672_62[label="62: Inode(VDXMC7T3ZYEYG):is_empty.js -> Inode(AIEDUCHX6VS6W)"];
n_9756672_62->n_9756672_63[color="blue"];
n_9756672_63[label="63: Inode(VDXMC7T3ZYEYG):is_traversable.js -> Inode(RQI2R5EYSAQ4K)"];
n_9756672_63->n_9756672_64[color="blue"];
n_9756672_64[label="64: Inode(VSXBCX4TPIMJ4): -> Inode(VSXBCX4TPIMJ4)"];
n_9756672_64->n_9756672_65[color="blue"];
n_9756672_65[label="65: Inode(VSXBCX4TPIMJ4):global.css -> Inode(WUOCEGDPKW3R2)"];
n_9756672_65->n_9756672_66[color="blue"];
n_9756672_66[label="66: Inode(WB6PIH4O4EPXQ): -> Inode(WB6PIH4O4EPXQ)"];
n_9756672_66->n_9756672_67[color="blue"];
n_9756672_67[label="67: Inode(WB6PIH4O4EPXQ):datastore -> Inode(U5SVCHKJ2EPXQ)"];
n_9756672_67->n_9756672_68[color="blue"];
n_9756672_68[label="68: Inode(WB6PIH4O4EPXQ):datastore.js -> Inode(YMTY2KW35DQAU)"];
n_9756672_68->n_9756672_69[color="blue"];
n_9756672_69[label="69: Inode(WB6PIH4O4EPXQ):datastore.yaml -> Inode(P34TGHAAECU2W)"];
n_9756672_69->n_9756672_70[color="blue"];
n_9756672_70[label="70: Inode(WB6PIH4O4EPXQ):fixtures -> Inode(6NSCSDVPRVUD6)"];
n_9756672_70->n_9756672_71[color="blue"];
n_9756672_71[label="71: Inode(WB6PIH4O4EPXQ):helper.js -> Inode(ZH3E3UOS5CADE)"];
n_9756672_71->n_9756672_72[color="blue"];
n_9756672_72[label="72: Inode(WB6PIH4O4EPXQ):patterns -> Inode(EWGIKU7N4TFWO)"];
n_9756672_72->n_9756672_73[color="blue"];
n_9756672_73[label="73: Inode(WB6PIH4O4EPXQ):pointer.yaml -> Inode(7D2JIKU4LZB46)"];
n_9756672_73->n_9756672_74[color="blue"];
n_9756672_74[label="74: Inode(WB6PIH4O4EPXQ):schema.js -> Inode(V5ARTKEXQAG64)"];
n_9756672_74->n_9756672_75[color="blue"];
n_9756672_75[label="75: Inode(WB6PIH4O4EPXQ):tests.yaml -> Inode(RNLJ5LJGUSEFU)"];
n_9756672_75->n_9756672_76[color="blue"];
n_9756672_76[label="76: Inode(WB6PIH4O4EPXQ):topic.yaml -> Inode(MRXIDRFCR6BMI)"];
n_9756672_76->n_9756672_77[color="blue"];
n_9756672_77[label="77: Inode(WB6PIH4O4EPXQ):topic_tree.dep -> Inode(WIWUDZKNML6KS)"];
n_9756672_77->n_9756672_78[color="blue"];
n_9756672_78[label="78: Inode(WQMKGUAN3HGPM): -> Inode(WQMKGUAN3HGPM)"];
n_9756672_78->n_9756672_79[color="blue"];
n_9756672_79[label="79: Inode(WYD4M6RXKB7OC): -> Inode(WYD4M6RXKB7OC)"];
n_9756672_79->n_9756672_80[color="blue"];
n_9756672_80[label="80: Inode(WYD4M6RXKB7OC):_deploy-remote.sh -> Inode(3TL33Z6QL5BO4)"];
n_9756672_80->n_9756672_81[color="blue"];
n_9756672_81[label="81: Inode(WYD4M6RXKB7OC):_deploy.sh -> Inode(KQTF3WPQXAVV6)"];
n_9756672_81->n_9756672_82[color="blue"];
n_9756672_82[label="82: Inode(WYD4M6RXKB7OC):_test.sh -> Inode(ZTMX7LIPRAHI2)"];
n_9756672_82->n_9756672_83[color="blue"];
n_9756672_83[label="83: Inode(WYD4M6RXKB7OC):build.sh -> Inode(T4KYTWLLF22P4)"];
n_9756672_83->n_9756672_84[color="blue"];
n_9756672_84[label="84: Inode(WYD4M6RXKB7OC):rsync-deploy.ignore -> Inode(GDCBHLWPWTI6U)"];
n_9756672_84->n_9756672_85[color="blue"];
n_9756672_85[label="85: Inode(WYD4M6RXKB7OC):start.sh -> Inode(3CTO5MJNEJ7G6)"];
n_9756672_85->n_9756672_86[color="blue"];
n_9756672_86[label="86: Inode(XS7ILRT4DODSM): -> Inode(XS7ILRT4DODSM)"];
n_9756672_86->n_9756672_87[color="blue"];
n_9756672_87[label="87: Inode(XS7ILRT4DODSM):_categories -> Inode(QIVCPWRQJNZS2)"];
n_9756672_87->n_9756672_88[color="blue"];
n_9756672_88[label="88: Inode(XS7ILRT4DODSM):_explore -> Inode(O6W4GJHUO2OQY)"];
n_9756672_88->n_9756672_89[color="blue"];
n_9756672_89[label="89: Inode(XS7ILRT4DODSM):categories.svelte -> Inode(TNUOEZIDMWNAS)"];
n_9756672_89->n_9756672_90[color="blue"];
n_9756672_90[label="90: Inode(XS7ILRT4DODSM):index.svelte -> Inode(UFCB7DT5I536C)"];
n_9756672_90->n_9756672_91[color="blue"];
n_9756672_91[label="91: Inode(XS7ILRT4DODSM):navigate.js -> Inode(EZRQRQB2W3T2M)"];
n_9756672_91->n_9756672_92[color="blue"];
n_9756672_92[label="92: Inode(XS7ILRT4DODSM):search.svelte -> Inode(YEBOLZGYVGG6E)"];
n_9756672_92->n_9756672_93[color="blue"];
n_9756672_93[label="93: Inode(X7MBB5IZL2DBA): -> Inode(X7MBB5IZL2DBA)"];
n_9756672_93->n_9756672_94[color="blue"];
n_9756672_94[label="94: Inode(X7MBB5IZL2DBA):_test.sh -> Inode(CKKEQRIACBCQ4)"];
n_9756672_94->n_9756672_95[color="blue"];
n_9756672_95[label="95: Inode(X7MBB5IZL2DBA):build.sh -> Inode(5DZXVOWKBSMGI)"];
n_9756672_95->n_9756672_96[color="blue"];
n_9756672_96[label="96: Inode(X7MBB5IZL2DBA):rsync-deploy.ignore -> Inode(HRPR7SKXCJN44)"];
n_9756672_96->n_9756672_97[color="blue"];
n_9756672_97[label="97: Inode(X7MBB5IZL2DBA):start.sh -> Inode(IAY4KMPNWY2UC)"];
n_9756672_97->n_9756672_98[color="blue"];
n_9756672_98[label="98: Inode(Y74VQVJLZBCQK): -> Inode(Y74VQVJLZBCQK)"];
n_9756672_98->n_9756672_99[color="blue"];
n_9756672_99[label="99: Inode(Y74VQVJLZBCQK):album.svelte -> Inode(MEBSSOZEGZBUS)"];
n_9756672_99->n_9756672_100[color="blue"];
n_9756672_100[label="100: Inode(Y74VQVJLZBCQK):artist.svelte -> Inode(SCXNCVLQ53QUS)"];
n_9756672_100->n_9756672_101[color="blue"];
n_9756672_101[label="101: Inode(Y74VQVJLZBCQK):playlist.svelte -> Inode(A5F4M52V6BAA6)"];
n_9756672_101->n_9756672_102[color="blue"];
n_9756672_102[label="102: Inode(Y74VQVJLZBCQK):track.svelte -> Inode(HZL4LIPH35X46)"];
n_9756672_102->n_9756672_103[color="blue"];
n_9756672_103[label="103: Inode(ZHWMZ7OLLI2DC): -> Inode(ZHWMZ7OLLI2DC)"];
n_9756672_103->n_9756672_104[color="blue"];
n_9756672_104[label="104: Inode(ZHWMZ7OLLI2DC):logout.svelte -> Inode(TR4NT2N6B7WVA)"];
}
subgraph cluster9805824 {
label="Page 9805824, rc 0 2718";
color=black;
n_9805824_0[label="0: Inode(3DJEOGW5L5HCE):commands.js -> Inode(HLTW65F3OX2I6)"];
n_9805824_0->n_9805824_1[color="blue"];
n_9805824_1[label="1: Inode(3DJEOGW5L5HCE):index.js -> Inode(NM333TUJAJ2CA)"];
n_9805824_1->n_9805824_2[color="blue"];
n_9805824_2[label="2: Inode(3F4CWNT24Y26K): -> Inode(3F4CWNT24Y26K)"];
n_9805824_2->n_9805824_3[color="blue"];
n_9805824_3[label="3: Inode(3F4CWNT24Y26K):basic -> Inode(3MMLSKVUCENQE)"];
n_9805824_3->n_9805824_4[color="blue"];
n_9805824_4[label="4: Inode(3F4CWNT24Y26K):business -> Inode(4ORPES5WVHU6Y)"];
n_9805824_4->n_9805824_5[color="blue"];
n_9805824_5[label="5: Inode(3F4CWNT24Y26K):event.svelte -> Inode(2MV5VU5G3OAFU)"];
n_9805824_5->n_9805824_6[color="blue"];
n_9805824_6[label="6: Inode(3F4CWNT24Y26K):find.svelte -> Inode(NGPD2KP5CNANY)"];
n_9805824_6->n_9805824_7[color="blue"];
n_9805824_7[label="7: Inode(3F4CWNT24Y26K):free -> Inode(QWW5O47FCNWJC)"];
n_9805824_7->n_9805824_8[color="blue"];
n_9805824_8[label="8: Inode(3F4CWNT24Y26K):manage -> Inode(FACHIVHPFYCQA)"];
n_9805824_8->n_9805824_9[color="blue"];
n_9805824_9[label="9: Inode(3F4CWNT24Y26K):manage.svelte -> Inode(2WDUNOOWYEEAI)"];
n_9805824_9->n_9805824_10[color="blue"];
n_9805824_10[label="10: Inode(3F4CWNT24Y26K):user_events.svelte -> Inode(D5GT6MAEWVN3E)"];
n_9805824_10->n_9805824_11[color="blue"];
n_9805824_11[label="11: Inode(3MMLSKVUCENQE): -> Inode(3MMLSKVUCENQE)"];
n_9805824_11->n_9805824_12[color="blue"];
n_9805824_12[label="12: Inode(4BEYCSKD6NO7G): -> Inode(4BEYCSKD6NO7G)"];
n_9805824_12->n_9805824_13[color="blue"];
n_9805824_13[label="13: Inode(4BEYCSKD6NO7G):spotify -> Inode(MWO6HOVXPRJHC)"];
n_9805824_13->n_9805824_14[color="blue"];
n_9805824_14[label="14: Inode(4BEYCSKD6NO7G):spotify.js -> Inode(GRXYBAHYY3CNC)"];
n_9805824_14->n_9805824_15[color="blue"];
n_9805824_15[label="15: Inode(4ORPES5WVHU6Y): -> Inode(4ORPES5WVHU6Y)"];
n_9805824_15->n_9805824_16[color="blue"];
n_9805824_16[label="16: Inode(4QGMJHUA2VKVY): -> Inode(4QGMJHUA2VKVY)"];
n_9805824_16->n_9805824_17[color="blue"];
n_9805824_17[label="17: Inode(4QGMJHUA2VKVY):_event.svelte -> Inode(LIG2ID3QLCZXE)"];
n_9805824_17->n_9805824_18[color="blue"];
n_9805824_18[label="18: Inode(4QGMJHUA2VKVY):_item.svelte -> Inode(EKGMRO63ZPTAQ)"];
n_9805824_18->n_9805824_19[color="blue"];
n_9805824_19[label="19: Inode(4QGMJHUA2VKVY):index.svelte -> Inode(X6DYH2NU5SYEG)"];
n_9805824_19->n_9805824_20[color="blue"];
n_9805824_20[label="20: Inode(5ODKHB7CLODEU): -> Inode(5ODKHB7CLODEU)"];
n_9805824_20->n_9805824_21[color="blue"];
n_9805824_21[label="21: Inode(5ODKHB7CLODEU):Icon.svelte -> Inode(UYPXQBB5W67YW)"];
n_9805824_21->n_9805824_22[color="blue"];
n_9805824_22[label="22: Inode(5USYOF7HSPTLS): -> Inode(5USYOF7HSPTLS)"];
n_9805824_22->n_9805824_23[color="blue"];
n_9805824_23[label="23: Inode(5USYOF7HSPTLS):base.js -> Inode(O5VDB4HZVW7T2)"];
n_9805824_23->n_9805824_24[color="blue"];
n_9805824_24[label="24: Inode(5USYOF7HSPTLS):controlenvy.js -> Inode(NY4T5WSV4EJ72)"];
n_9805824_24->n_9805824_25[color="blue"];
n_9805824_25[label="25: Inode(6NSCSDVPRVUD6): -> Inode(6NSCSDVPRVUD6)"];
n_9805824_25->n_9805824_26[color="blue"];
n_9805824_26[label="26: Inode(6NSCSDVPRVUD6):area.yaml -> Inode(Y3B3GCUUHVFSW)"];
n_9805824_26->n_9805824_27[color="blue"];
n_9805824_27[label="27: Inode(6NSCSDVPRVUD6):component.yaml -> Inode(G3ZNRKPQQTNEI)"];
n_9805824_27->n_9805824_28[color="blue"];
n_9805824_28[label="28: Inode(6NSCSDVPRVUD6):room.yaml -> Inode(53UVDWGEJLQ5M)"];
n_9805824_28->n_9805824_29[color="blue"];
n_9805824_29[label="29: Inode(6NSCSDVPRVUD6):schema.yaml -> Inode(F6G2WMFXHEPC4)"];
n_9805824_29->n_9805824_30[color="blue"];
n_9805824_30[label="30: Inode(6NSCSDVPRVUD6):schema_tree.yaml -> Inode(NMHG2L57ESID2)"];
n_9805824_30->n_9805824_31[color="blue"];
n_9805824_31[label="31: Inode(6NSCSDVPRVUD6):system.yaml -> Inode(3LPVG62PMSPBQ)"];
n_9805824_31->n_9805824_32[color="blue"];
n_9805824_32[label="32: Inode(6TMTDO45E3YQY): -> Inode(6TMTDO45E3YQY)"];
n_9805824_32->n_9805824_33[color="blue"];
n_9805824_33[label="33: Inode(6TMTDO45E3YQY):api.js -> Inode(FUTFDFPW44524)"];
n_9805824_33->n_9805824_34[color="blue"];
n_9805824_34[label="34: Inode(6TMTDO45E3YQY):base.js -> Inode(Y3UJQKQS4MP2K)"];
n_9805824_34->n_9805824_35[color="blue"];
n_9805824_35[label="35: Inode(6TMTDO45E3YQY):convenience.js -> Inode(AUMQ4GZOGACUO)"];
n_9805824_35->n_9805824_36[color="blue"];
n_9805824_36[label="36: Inode(6TMTDO45E3YQY):event.js -> Inode(FBA35XTQ2ZG3G)"];
n_9805824_36->n_9805824_37[color="blue"];
n_9805824_37[label="37: Inode(6TMTDO45E3YQY):helpers -> Inode(VDXMC7T3ZYEYG)"];
n_9805824_37->n_9805824_38[color="blue"];
n_9805824_38[label="38: Inode(6TMTDO45E3YQY):parser.js -> Inode(2USC4IYBTLXAQ)"];
n_9805824_38->n_9805824_39[color="blue"];
n_9805824_39[label="39: Inode(6TMTDO45E3YQY):pointer.js -> Inode(6LIZKWXUAC2XI)"];
n_9805824_39->n_9805824_40[color="blue"];
n_9805824_40[label="40: Inode(6TMTDO45E3YQY):svelte.js -> Inode(VKY4R6MG2WJRM)"];
n_9805824_40->n_9805824_41[color="blue"];
n_9805824_41[label="41: Inode(6TMTDO45E3YQY):topic.js -> Inode(7JIIEKIZZVY32)"];
n_9805824_41->n_9805824_42[color="blue"];
n_9805824_42[label="42: Inode(6TMTDO45E3YQY):tree.js -> Inode(5NGRCXLCVIDBS)"];
n_9805824_42->n_9805824_43[color="blue"];
n_9805824_43[label="43: Inode(6XVIYWEUJNQTS): -> Inode(6XVIYWEUJNQTS)"];
n_9805824_43->n_9805824_44[color="blue"];
n_9805824_44[label="44: Inode(6XVIYWEUJNQTS):lint.sh -> Inode(XKNWE7VUUVIM6)"];
n_9805824_44->n_9805824_45[color="blue"];
n_9805824_45[label="45: Inode(6X3XBPT7AZ35O): -> Inode(6X3XBPT7AZ35O)"];
n_9805824_45->n_9805824_46[color="blue"];
n_9805824_46[label="46: Inode(6X3XBPT7AZ35O):spec.js -> Inode(5IJ7O2D3AHJ6Q)"];
n_9805824_46->n_9805824_47[color="blue"];
n_9805824_47[label="47: Inode(65TLXQ7KZYYJ4): -> Inode(65TLXQ7KZYYJ4)"];
n_9805824_47->n_9805824_48[color="blue"];
n_9805824_48[label="48: Inode(65TLXQ7KZYYJ4):.autoenv.zsh -> Inode(6NPIRHGIBQ6ME)"];
n_9805824_48->n_9805824_49[color="blue"];
n_9805824_49[label="49: Inode(65TLXQ7KZYYJ4):.bin -> Inode(6XVIYWEUJNQTS)"];
n_9805824_49->n_9805824_50[color="blue"];
n_9805824_50[label="50: Inode(65TLXQ7KZYYJ4):admin -> Inode(GHPSHGSWVRTC2)"];
n_9805824_50->n_9805824_51[color="blue"];
n_9805824_51[label="51: Inode(65TLXQ7KZYYJ4):djiny -> Inode(73KSRBC4ZOXC2)"];
n_9805824_51->n_9805824_52[color="blue"];
n_9805824_52[label="52: Inode(65TLXQ7KZYYJ4):package-lock.json -> Inode(XXOOLDCHDBDHG)"];
n_9805824_52->n_9805824_53[color="blue"];
n_9805824_53[label="53: Inode(65TLXQ7KZYYJ4):pnpm-lock.yaml -> Inode(W3Q6QN5724GXE)"];
n_9805824_53->n_9805824_54[color="blue"];
n_9805824_54[label="54: Inode(65TLXQ7KZYYJ4):pnpm-workspace.yaml -> Inode(GM72ONXBVS64G)"];
n_9805824_54->n_9805824_55[color="blue"];
n_9805824_55[label="55: Inode(7BD75A5OV3RRU): -> Inode(7BD75A5OV3RRU)"];
n_9805824_55->n_9805824_56[color="blue"];
n_9805824_56[label="56: Inode(7BD75A5OV3RRU):fixtures -> Inode(FNO44B3I6OJIG)"];
n_9805824_56->n_9805824_57[color="blue"];
n_9805824_57[label="57: Inode(7BD75A5OV3RRU):integration -> Inode(6X3XBPT7AZ35O)"];
n_9805824_57->n_9805824_58[color="blue"];
n_9805824_58[label="58: Inode(7BD75A5OV3RRU):plugins -> Inode(ESKHFJ2QX7P5C)"];
n_9805824_58->n_9805824_59[color="blue"];
n_9805824_59[label="59: Inode(7BD75A5OV3RRU):support -> Inode(3DJEOGW5L5HCE)"];
n_9805824_59->n_9805824_60[color="blue"];
n_9805824_60[label="60: Inode(7GVTKXM3HLWNQ): -> Inode(7GVTKXM3HLWNQ)"];
n_9805824_60->n_9805824_61[color="blue"];
n_9805824_61[label="61: Inode(7GVTKXM3HLWNQ):api.js -> Inode(5Q4L6H5VPXEKO)"];
n_9805824_61->n_9805824_62[color="blue"];
n_9805824_62[label="62: Inode(7GVTKXM3HLWNQ):authorization.js -> Inode(3V5UJCRQVV52C)"];
n_9805824_62->n_9805824_63[color="blue"];
n_9805824_63[label="63: Inode(7GVTKXM3HLWNQ):components -> Inode(UFNCC47RMZ2R6)"];
n_9805824_63->n_9805824_64[color="blue"];
n_9805824_64[label="64: Inode(7GVTKXM3HLWNQ):drivers -> Inode(4BEYCSKD6NO7G)"];
n_9805824_64->n_9805824_65[color="blue"];
n_9805824_65[label="65: Inode(7GVTKXM3HLWNQ):models -> Inode(KILB4TFGRUWYO)"];
n_9805824_65->n_9805824_66[color="blue"];
n_9805824_66[label="66: Inode(7GVTKXM3HLWNQ):send.js -> Inode(ZEKLTYQNM5DBO)"];
n_9805824_66->n_9805824_67[color="blue"];
n_9805824_67[label="67: Inode(7GVTKXM3HLWNQ):subscribe.js -> Inode(FRR3RYWBJ66RO)"];
n_9805824_67->n_9805824_68[color="blue"];
n_9805824_68[label="68: Inode(7GVTKXM3HLWNQ):theme.js -> Inode(37C3XDBYLHIOS)"];
n_9805824_68->n_9805824_69[color="blue"];
n_9805824_69[label="69: Inode(7GVTKXM3HLWNQ):unsubscribe.js -> Inode(42T3SEQPCQ2NQ)"];
n_9805824_69->n_9805824_70[color="blue"];
n_9805824_70[label="70: Inode(7GVTKXM3HLWNQ):websocket -> Inode(FWTL334DDZDFG)"];
n_9805824_70->n_9805824_71[color="blue"];
n_9805824_71[label="71: Inode(73KSRBC4ZOXC2): -> Inode(73KSRBC4ZOXC2)"];
n_9805824_71->n_9805824_72[color="blue"];
n_9805824_72[label="72: Inode(73KSRBC4ZOXC2):.autoenv.zsh -> Inode(QOWPYVSW7ZQE6)"];
n_9805824_72->n_9805824_73[color="blue"];
n_9805824_73[label="73: Inode(73KSRBC4ZOXC2):.bin -> Inode(X7MBB5IZL2DBA)"];
n_9805824_73->n_9805824_74[color="blue"];
n_9805824_74[label="74: Inode(73KSRBC4ZOXC2):.eslintrc.cjs -> Inode(V5WFDITISXTCE)"];
n_9805824_74->n_9805824_75[color="blue"];
n_9805824_75[label="75: Inode(73KSRBC4ZOXC2):.gitignore -> Inode(RETQ6VPCV3KZW)"];
n_9805824_75->n_9805824_76[color="blue"];
n_9805824_76[label="76: Inode(73KSRBC4ZOXC2):.npmrc -> Inode(RYEFEZ45PN4MA)"];
n_9805824_76->n_9805824_77[color="blue"];
n_9805824_77[label="77: Inode(73KSRBC4ZOXC2):.prettierignore -> Inode(XVUKOURL7SMHU)"];
n_9805824_77->n_9805824_78[color="blue"];
n_9805824_78[label="78: Inode(73KSRBC4ZOXC2):.prettierrc -> Inode(33H5CQLJPJTXG)"];
n_9805824_78->n_9805824_79[color="blue"];
n_9805824_79[label="79: Inode(73KSRBC4ZOXC2):README.md -> Inode(PA6SDKU4QOTA2)"];
n_9805824_79->n_9805824_80[color="blue"];
n_9805824_80[label="80: Inode(73KSRBC4ZOXC2):package-lock.json -> Inode(6HBI4WGQ3OVCW)"];
n_9805824_80->n_9805824_81[color="blue"];
n_9805824_81[label="81: Inode(73KSRBC4ZOXC2):package.json -> Inode(FW7A5FRRVA4F6)"];
n_9805824_81->n_9805824_82[color="blue"];
n_9805824_82[label="82: Inode(73KSRBC4ZOXC2):rollup.config.js.noop -> Inode(2SBU4JA2BYW5G)"];
n_9805824_82->n_9805824_83[color="blue"];
n_9805824_83[label="83: Inode(73KSRBC4ZOXC2):src -> Inode(COXOPYDYKF3XG)"];
n_9805824_83->n_9805824_84[color="blue"];
n_9805824_84[label="84: Inode(73KSRBC4ZOXC2):static -> Inode(HDJNI6MNYEHJQ)"];
n_9805824_84->n_9805824_85[color="blue"];
n_9805824_85[label="85: Inode(73KSRBC4ZOXC2):svelte.config.js -> Inode(4LQOKCLW6CKCC)"];
n_9805824_85->n_9805824_86[color="blue"];
n_9805824_86[label="86: Inode(73KSRBC4ZOXC2):vite.config.js.noop -> Inode(5AIPYQMZK4QXC)"];
}
}
@import 'sanitize.css';
@mixin inset {
background: var(--shadow-tertiary)!important;
box-shadow: inset 0 0.25rem 0.5rem var(--shadow), inset 0 0.125rem 0.25rem var(--shadow-tertiary), 0 0.0375rem 0 var(--highlight-secondary);
}
.inset {
@include inset;
}
@mixin zoom_on_hover {
transition: all .2s ease-in-out;
&:active {
transform: scale(0.9) !important;
}
&:hover {
cursor: pointer;
transform: scale(1.1);
}
}
.zoom_on_hover {
@include zoom_on_hover;
}
button {
border: none;
background: none;
@include zoom_on_hover;
svg {
color: var(--text-primary);
}
.circle {
color: var(--text-primary);
text-align: center;
border: none;
width: 3em;
height: 3em;
border-radius: 50%;
}
&.primary {
background: hsla(240, 100%, 20%, 0.9);
}
&.secondary {
border: 3px solid hsla(240, 100%, 20%, 0.9);
}
&.big {
border-radius: 50vh;
text-align: center;
color: hsla(100, 25%, 100%, 0.8);
margin: 0.375em auto;
h2, h3 {
font-weight: bold;
text-align: center;
margin: 0.5em 1em;
}
}
}
@mixin text_inputs {
background: hsla(50, 50%, 100%, 0.3);
border: none;
border-radius: 2px;
height: 2em;
font-family: Montserrat, Tahoma, 'sans-serif';
&:focus-visible {
outline: none;
box-shadow: 0px 0px 3px 3px hsla(240, 100%, 20%, 0.9);
}
}
input {
@include text_inputs;
}
textarea {
@include text_inputs;
width: 100%;
min-height: 3em;
}
h1 {
input {
border: none;
box-shadow: none;
}
}
main {
box-sizing: border-box;
width: 100vw;
height: calc(100vh - 5rem);
margin: 0;
padding: 2em;
overflow-y: scroll;
font-family: Montserrat, Tahoma, 'sans-serif';
}
nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 200;
padding: 0 2em;
background: hsl(0, 100%, 0%);
ul {
margin: 0 auto;
padding: 0;
width: fit-content;
display: grid;
grid-auto-flow: column;
gap: 1em;
h3 {
color: hsla(50, 50%, 100%, 0.3) !important;
align-self: center;
text-align: center;
margin: 0 1em;
}
li {
span {
margin: 0;
max-height: 5em;
display: grid;
grid-auto-flow: row;
grid-auto-rows: max-content;
align-items: center;
justify-items: center;
color: hsla(50, 50%, 100%, 0.3);
text-align: center;
text-decoration: none;
padding: 1em 0.5em;
float: left;
}
&:hover {
svg {
fill: hsla(50, 50%, 100%, 1) !important;
}
span, a {
color: hsla(50, 50%, 100%, 1);
}
}
}
[aria-current] {
position: relative;
}
[aria-current]::before {
position: absolute;
content: '';
width: calc(100% - 1em);
height: 2px;
background-color: rgb(255,62,0);
display: block;
top: 1px;
}
}
}
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;
}
.grid-1 {
display: grid;
grid-template: 1 1fr / 1 1fr;
align-items: center;
justify-items: center;
}
.flex-row {
display: flex;
flex-direction: row;
align-content: center;
justify-content: space-around;
}
.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;
}
}
.side-scroll-child {
width: 80vw;
justify-self: center;
scroll-snap-align: center;
align-content: center;
display: grid;
}
.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) {
.side-scroll-panel {
transition: opacity 0.5s ease, transform 0.5s ease;
}
}
li {
color: var(--text-primary);
}
.side-scroll-panel-transition {
opacity: 1;
transform: none;
}
.fill-w {
width: 100%;
}
.fill-h {
height: 100%;
}
.fill {
width: 100%;
height: 100%;
}
.title-big {
font-weight: 700;
font-size: 2.8em;
text-transform: uppercase;
}
.lgt-txt {
color: hsla(100, 25%, 80%);
}
.drk-txt {
color: hsla(100, 0%, 40%);
}
h1, h2, h3, p, label {
color: var(--text-primary);
}
p {
margin-block-start: 0.25em;
margin-block-end: 0.25em;
}
.panel {
padding: 1rem;
background: var(--bg-shade);
border-radius: 0.5rem;
}
svg {
width: auto;
height: 2rem;
fill: var(--text-primary);
}
.opaque-8 {
opacity: 0.8;
}
.panel-opaque {
padding: 2rem;
background: var(--bg-shade);
border-radius: 0.5rem;
opacity: 0.8;
}
.pad-top-05em {
padding-top: 0.5em;
}
.pad-btm-05em {
padding-bottom: 0.5em;
}
.pad-top-btm-05em {
padding-top: 0.5em;
padding-bottom: 0.5em;
}
.ctr {
margin: auto auto;
text-align: center;
}
.grid-flow-column {
display: grid;
grid-auto-flow: column;
}
.height-min-content {
height: min-content;
}
.grid-align-center {
align-items: center;
}
.grid-justify-center {
justify-items: center;
}
.width-100 {
width: 100%;
}
.width-80 {
width: 80%;
}
.width-60 {
width: 60%;
}
.width-40 {
width: 40%;
}
.text-ctr {
text-align: center;
}
.text-rgt {
text-align: right;
}
.text-lft {
text-align: left;
}
.grid {
display: grid;
}
.grid-responsive-columns-1fr {
grid-template-columns: repeat(auto-fit, minmax(1rem, 1fr));
}
.button-circle {
display: grid;
justify-content: center;
text-align: center;
width: max-content;
grid-template-rows: min-content max-content;
border-radius: 50%;
}
.panel_horizontal {
display: grid;
grid-auto-flow: column;
gap: 3rem;
grid-auto-columns: min-content;
}
.title-row {
display: grid;
grid-template: 4rem / 4rem auto;
align-items: center;
svg {
justify-self: center;
z-index: 200;
color:hsla(50, 50%, 100%, 1);
}
h1 {
margin: 0;
z-index: 200;
width: max-content;
}
}
<tr>
<td>
<label for="email">Email:</label>
</td>
<td>
<input class="width-100" type="text" id="email" bind:value={email} />
</td>
</tr>
<tr>
<td>
<label for="email">Confirm Email:</label>
</td>
<td>
<input class="width-100" type="text" id="email-confirm" bind:value={email_confirm} />
</td>
</tr>
{#if typeof message === 'string' && message !== ''}
<p>{message}</p>
{:else if message === true}
<p>OK</p>
{/if}
<script>
let message = ''
let email_confirm = ''
let email = ''
$: checkEmail(email, email_confirm)
async function checkEmail() {
if (email !== '') {
if (email !== email_confirm) {
message = 'Emails must match.'
}
message = 'Checking for existing accounts using that email.'
if (await checkServerForExistingEmail(email)) {
message = true
} else {
message = 'That email is already in use, please log in.'
}
} else {
message = ''
}
}
</script>
<div calss="overlay">
<Token></Token>
</div>
<script>
import Token from './_notifications/token.svelte'
</script>
<script>
$: user_id = datastore.svelte('session/user_id')
$: store = datastore.svelte(`state/users/${$user_id}/services/+/+/token`, [], pointer => [pointer.steps[4], pointer.steps[5], pointer.value])
$: console.log({S: $store, u: $user_id})
$: [service, type, user_token] = $store.length > 0 ? $store[0] : [undefined, undefined, undefined]
</script>
<label>
<slot name="label"></slot>
<textarea type="text" placeholder="{placeholder}" value={_value} on:keyup={onChange} on:focus={onFocus} on:blur={onBlur}/>
</label>
<script>
export let event_path
export let attribute
export let placeholder
$: user_id = datastore.svelte('session/user_id')
$: store = datastore.svelte(`setup${event_path}/${attribute}`)
function onChange({ target }) {
const { value } = target
datastore.queue(`/setup/users/${$user_id}${event_path}/${attribute}`, value)
}
let _value
$: if (!focused) { _value = $store }
console.log({_value, placeholder})
let focused = false
const onFocus = () => focused = true
const onBlur = () => {
focused = false
_value = $store
}
</script>
<label>
<slot name="label"></slot>
<input type="text" placeholder="{placeholder}" value={_value} on:keyup={onChange} on:focus={onFocus} on:blur={onBlur}/>
</label>
<script>
export let event_path
export let attribute
export let placeholder
$: user_id = datastore.svelte('session/user_id')
$: store = datastore.svelte(`setup${event_path}/${attribute}`)
function onChange({ target }) {
const { value } = target
datastore.queue(`/setup/users/${$user_id}${event_path}/${attribute}`, value)
}
let _value = ''
$: if (!focused) {
_value = $store
}
let focused = false
const onFocus = () => focused = true
const onBlur = () => {
focused = false
_value = $store
}
</script>
<div class="tab">
<h3>show:</h3>
<label>
<input type="checkbox" bind:group="{include}" value="owned"/>
<h3>Owned</h3>
</label>
<label>
<input type="checkbox" bind:group="{include}" value="followed"/>
<h3>Followed</h3>
</label>
</div>
{#each events as event}
<Event path={event}></Event>
{/each}
<script>
import Event from './owned.svelte'
$: user_id = datastore.svelte('session/user_id')
$: user_owned_events = datastore.svelte(`state/users/${$user_id}/owned`, [])
$: followed_events = datastore.svelte(`state/users/${$user_id}/followed`, [])
let include = ['owned', 'followed']
let events = []
$: {
let temp = []
if (include.includes('owned')) {
temp.push(...$user_owned_events)
}
if (include.includes('followed')) {
temp.push(...$followed_events)
}
events = temp
}
$: show = datastore.svelte(`session/show_owned_events`, false)
</script>
<style lang="scss">
.panel {
display: grid;
grid-template-columns: 1fr max-content;
border-radius: 0;
}
div {
display: grid;
grid-auto-flow: column;
justify-items: center;
}
h3 {
margin: 0;
}
.tab {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
justify-items: center;
align-content: center;
label {
padding-top: 0;
padding-bottom: 0;
display: inline-flex;
gap: 0.5em;
}
}
</style>
{#if $user_owned_events.includes(path)}
<Owned path="{path}"></Owned>
{:else}
<Followed path="{path}"></Followed>
{/if}
<script>
export let path
import Owned from './owned.svelte'
import Followed from './followed.svelte'
$: user_id = datastore.svelte('session/user_id')
$: user_owned_events = datastore.svelte(`state/users/${$user_id}/owned`, [])
</script>
<p style="grid-area: limit;">
You own {$user_owned_events.length} Events
</p>
{#if can_create}
<button style="grid-area: add;" on:click|preventDefault={onCreate}>Create</button>
{:else}
<p>User event limit reached <a>Extend your limit</a></p>
{/if}
<script>
$: user_id = datastore.svelte('session/user_id')
$: user_owned_events = datastore.svelte(`state/users/${$user_id}/owned`, [])
$: number_of_user_owned_events = $user_owned_events.length
$: user_type = datastore.svelte(`state/users/${$user_id}/type`, 'free')
$: event_limit = {
free: 1,
basic: 3,
business: 5
}[$user_type]
$: can_create = number_of_user_owned_events <= event_limit
function onCreate() {
datastore.queue(`/action/users/${$user_id}/events/new`, +new Date)
}
$: show = datastore.svelte(`session/show_owned_events`, false)
function toggle() {
datastore.set("/session/show_owned_events", !$show)
}
</script>
<style lang="scss">
label {
justify-content: right;
display: grid;
grid-template-columns: max-content max-content;
border-radius: 0;
gap: 2em;
}
p {
text-align: center;
}
</style>
<div class="panel" on:focus|preventDefault>
<p>{$name}</p>
<button on:click={onClick}><Icon name="faPlusSquare" ></Icon></button>
</div>
<script>
export let event_path
import Icon from '$lib/components/Icon.svelte'
$: name = datastore.svelte(`setup${event_path}/name`)
$: user_id = datastore.svelte(`session/user_id`)
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
function onClick() {
datastore.push(`/state/users/${$user_id}/followed`, event_path)
datastore.push(`/setup${event_path}/users`, event_path)
dispatch('close')
}
</script>
<style lang="scss">
div {
display: grid;
grid-template-columns: max-content 1fr;
button {
justify-self: end;
margin: 0;
}
}
</style>
<script>
import Global from '$lib/components/global.svelte'
import Notifications from './_notifications.svelte'
</script>
<main id="main-scroll">
<Global>
<Notifications></Notifications>
<slot></slot>
</Global>
</main>
/**
* @type {import('@sveltejs/kit').RequestHandler}
*/
export async function get({ params }) {
const { root, icon_name } = params
let icon
try {
switch (root) {
case 'solid': {
({ [icon_name]: icon } = await import('@fortawesome/free-solid-svg-icons'))
break
}
case 'brands': {
({ [icon_name]: icon } = await import('@fortawesome/free-brands-svg-icons'))
break
}
default: {
return { status: 404 }
}
}
}
catch (error) {
return { status: 404 }
}
if (icon) {
return {
headers: {
'content-type': 'application/javascript'
},
body: JSON.stringify(icon)
}
}
}
let global_window
if (typeof window !== 'undefined') {
global_window = window
} else {
global_window = global
}
export const unsubscribe = (topic) => {
// console.log('subscribe()', 'topic:', topic)
let operation = 'u'
let payload
payload = {
o: operation,
t: topic
}
const ws = global_window.connections['node']
if (ws) {
ws.send(payload)
}
}
<section class="modal_wrapper" in:fade="{{ duration: 300 }}" out:fade|local="{{ delay: 300, duration: 300 }}">
<div class="modal_bg_blur" on:click|preventDefault="{close}"></div>
<div class="card modal" in:fly="{{ delay: 300, y: 300, duration: 300 }}" out:fly="{{ y: 300, duration: 300 }}">
<div class="modal_header">
<slot name="header"></slot>
<button class="close" on:click|preventDefault="{close}">
<Icon name="faTimes" />
</button>
</div>
<div class="modal_body">
<slot></slot>
</div>
</div>
</section>
<script>
import { fade, fly } from 'svelte/transition'
import Icon from '$lib/components/Icon.svelte'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
const close = () => {
dispatch('close')
}
</script>
<style lang="scss">
.modal_wrapper {
position: fixed;
width: 100%;
height: 90%;
z-index: 1000;
display: grid;
grid-template:
'main' 1fr / 1fr;
margin-top: -2rem;
margin-left: -2rem;
place-items: center;
@media (min-width: 769px) {
// Breakpoint: tablet to desktop
width: 100vw;
}
.modal_bg_blur {
width: 100%;
height: 100%;
display: grid;
grid-area: main;
z-index: 2;
backdrop-filter: blur(3px);
background: var(--bg-darken);
}
.modal {
width: 60%;
height: max-content;
min-height: 25vh;
grid-area: main;
z-index: 3;
overflow: hidden;
padding: 1rem;
grid-template-rows: max-content 1fr;
background: var(--bg-button);
.modal_header {
padding: 0rem 0.5rem;
grid-column: 1;
grid-column-end: -1;
grid-template-columns: 1fr max-content;
display: grid;
align-items: center;
button {
justify-self: end;
place-items: center;
display: grid;
}
}
.modal_body {
display: grid;
grid-auto-rows: max-content;
gap: 1rem;
padding: 0.5rem;
overflow-y: scroll;
overflow-x: hidden;
}
}
}
button {
background: none;
border: none;
}
.modal_body::-webkit-scrollbar {
width: 0.375rem;
display: block;
background: transparent;
border-radius: 50px;
}
.modal_body::-webkit-scrollbar-thumb {
background: linear-gradient(0deg, var(--accent), var(--accent-secondary));
border-radius: 50px;
}
</style>
<script>
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 = '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
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]
}
} 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
}
if (value === true){
result.push('fa-' + key)
} else if (value !== false) {
result.push('fa-' + key +'-' + value)
}
return result
}, []).join(' ')
}
let props = propEval($$props)
$: props = propEval($$props)
$: {
const [_width, _height /* _ligatures */ /* _unicode */, , , _svgPathData] = data.icon
width = _width
height = _height
path = _svgPathData
label = data.iconName
box = `0 0 ${width} ${height}`
style = `font-size: ${scale}em`
}
</script>
<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>
<style type="text/scss">
svg.fa-spin {
-webkit-animation-name: spin;
-moz-animation-name: spin;
-ms-animation-name: spin;
animation-name: spin;
-webkit-animation-duration: 4000ms;
-moz-animation-duration: 4000ms;
-ms-animation-duration: 4000ms;
animation-duration: 4000ms;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@-moz-keyframes spin {
from {
-moz-transform: rotate(0deg);
}
to {
-moz-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
}
</style>
server {
server_name processor.djinmusic.ca www.processor.djinmusic.ca;
location / {
proxy_pass http://127.0.0.1:25706;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=480m use_temp_path=off;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name djinmusic.ca www.djinmusic.ca;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name djinmusic.ca www.djinmusic.ca;
location / {
return 301 https://app.$host$request_uri;
}
}
server {
server_name app.djinmusic.ca www.app.djinmusic.ca;
location / {
proxy_cache my_cache;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_revalidate on;
proxy_pass http://localhost:5443;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
server {
server_name admin.djinmusic.ca www.admin.djinmusic.ca;
location / {
proxy_cache my_cache;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_revalidate on;
proxy_pass http://localhost:6443;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
module.exports = {
parser: 'babel-eslint',
parserOptions: {
ecmaVersion: 2019,
sourceType: 'module',
ecmaFeatures: {
modules: true
}
},
env: {
es2020: true,
browser: true,
mocha: true,
node: true
},
extends: ['eslint:recommended', 'prettier'],
plugins: ['svelte3', 'html', 'optimize-regex', 'prettier'],
overrides: [
{
files: ['*.svelte'],
processor: 'svelte3/svelte3'
}
],
rules: {
'id-blacklist': ['warn', 'cb', 'err', 'req', 'res'],
'optimize-regex/optimize-regex': 'off',
'prettier/prettier': ['off', { singleQuote: true, semi: false, printWidth: 256 }],
'no-console': 'off',
'no-debugger': 'warn',
'no-fallthrough': 'off',
'no-undef': 'warn',
'no-unused-vars': ['error', { args: 'none' }]
},
settings: {
'svelte3/ignore-warnings': warning => {
let ignore_warning = false
switch (warning.code) {
case 'unused-export-let':
ignore_warning = true
break
default:
break
}
return ignore_warning
},
'svelte3/ignore-styles': () => true
},
globals: {
CONFIG: true,
_: true,
__$$self: true,
connections: true,
datastore: true,
Pointer: true,
theme: true,
moment: true,
DateTime: true,
Duration: true,
Interval: true,
requireYAML: true,
system_uuid: true,
system_path: true,
system_topic: true
}
}
.reify-cache/
dist/
node_modules/
tmp/
.git
*.log
node_modules
app/djiny/src
app/djiny/cypress
app/djiny/cypress.json
.nginx
.bin
__sapper__/dev
#!/usr/bin/env bash
. $BIN_DIR/_lib.sh
echo '🔎 Linting ...'
# Lint JavaScript
JS_ERROR_CODE=0
FILES=$(find $WORKING_DIR \( -name '*.js' -o -name '*.html' \) -not -path "$WORKING_DIR/.reify-cache/*" -not -path "$WORKING_DIR/node_modules/*" -not -path "$WORKING_DIR/dist/*" -not -path "$WORKING_DIR/tmp/*")
if [[ "${FILES:-x}" != "x" ]]; then
echo -e " $(echo "$FILES" | wc -l | awk '{print $1}') JavaScript, HTML, and Svelte files"
set +e
eslint -c "$MONO_DIR/.eslintrc.js" --ignore-path "$MONO_DIR/.eslintignore" $FILES
JS_ERROR_CODE=$?
set -e
fi
# Lint SCSS
CSS_ERROR_CODE=0
FILES=$(find $WORKING_DIR \( -name '*.scss' -o -name '*.scss' -o -name '*.html' \) -not -path "$WORKING_DIR/.reify-cache/*" -not -path "$WORKING_DIR/node_modules/*" -not -path "$WORKING_DIR/dist/*" -not -path "$WORKING_DIR/tmp/*")
if [[ "${FILES:-x}" != "x" ]]; then
echo -e " $(echo "$FILES" | wc -l | awk '{print $1}') CSS files"
set +e
stylelint --fix --ignore-path "$MONO_DIR/.stylelintignore" $FILES
CSS_ERROR_CODE=$?
set -e
fi
if [[ $JS_ERROR_CODE -gt 0 ]] || [[ $CSS_ERROR_CODE -gt 0 ]]; then
exit 1
fi
#!/usr/bin/env bash
. $BIN_DIR/_lib.sh
gecho 'Installing repo-level dependencies...'
cd $MONO_DIR
pnpm install -r
gecho 'Installing app dependencies...'
cd $MONO_DIR/app
pnpm install -r
gecho 'Installing channel dependencies...'
cd $MONO_DIR/services/channel
pnpm install -r
gecho 'Installing datastore dependencies...'
cd $MONO_DIR/services/datastore
pnpm install -r
#!/usr/bin/env bash
. $BIN_DIR/_lib.sh
echo '🗄 Formatting ...'
# Format *.js files
format-js() {
FILES=""
if [[ ! -z ${@+x} ]]; then
_IFS=$IFS
IFS=" "
FILES=${@}
else
FILES=$(find $WORKING_DIR -name '*.js' -not -path "$WORKING_DIR/.reify-cache/*" -not -path "$WORKING_DIR/node_modules/*" -not -path "$WORKING_DIR/dist/*" -not -path "$WORKING_DIR/tmp/*")
fi
[[ "${FILES:-x}" = "x" ]] && return
echo -e " $(echo "$FILES" | wc -l | awk '{print $1}') JavaScript files"
prettier --config "$MONO_DIR/.prettierrc" --ignore-path "$MONO_DIR/.prettierignore" --write $FILES >/dev/null
if [[ ! -z ${_IFS+x} ]]; then
IFS=$_IFS
fi
}
# Format *.html <script> and <style> elements
format-html() {
echo "Formatting html and svelte has been disabled."
return 0
FILES=""
if [[ ! -z ${@+x} ]]; then
_IFS=$IFS
IFS=" "
FILES=${@}
else
FILES=$(find $WORKING_DIR -name '*.svelte' -not -path "$WORKING_DIR/node_modules/*" -not -path "$WORKING_DIR/dist/*" -not -path "$WORKING_DIR/tmp/*")
fi
[[ "${FILES:-x}" = "x" ]] && return
echo -e " $(echo "$FILES" | wc -l | awk '{print $1}') Svelte files"
prettier --config "$MONO_DIR/.prettierrc" --ignore-path "$MONO_DIR/.prettierignore" --write $FILES >/dev/null
if [[ ! -z ${_IFS+x} ]]; then
IFS=$_IFS
fi
}
format-all() {
format-js
format-html
}
ARGC=$#
if [[ $ARGC -gt 1 ]]; then
ARGV=( $@ )
COMMAND=${ARGV[0]}
FILES=${ARGV[@]:1}
case $COMMAND in
"js" )
format-js $FILES
;;
"html" )
format-html $FILES
;;
* )
format-all
;;
esac
else
format-all
fi
#!/usr/bin/env bash
. $BIN_DIR/_lib.sh
#npm run build
printf "Building Public App\n\n"
cd $MONO_DIR/app/djiny
npm run build
cd $MONO_DIR/app/admin
cd $MONO_DIR
printf "Using $HOME/.ssh/id_rsa_corda_digital_ocean\n\n"
printf "\n1. Deploying updated app changes\n\n"
rsync --progress -Pavuz --exclude-from="$BIN_DIR/rsync-deploy.ignore" -e "ssh -i $HOME/.ssh/id_rsa_corda_digital_ocean" "${MONO_DIR}/." "tpcowan@djinmusic.ca:/home/tpcowan/djinmusic"
printf "\n2. Deploying updated app changes\n\n"
rsync --progress -Pavuz -e "ssh -i $HOME/.ssh/id_rsa_corda_digital_ocean" "${MONO_DIR}/.nginx/sites-available/" "tpcowan@djinmusic.ca:/etc/nginx/sites-available"
printf "\n3. Updating remote deploy script\n$BIN_DIR/deploy-remote.sh\n\n"
rsync --progress -Pavuz -e "ssh -i $HOME/.ssh/id_rsa_corda_digital_ocean" "${BIN_DIR}/deploy-remote.sh" "tpcowan@djinmusic.ca:/home/tpcowan/djinmusic/deploy-remote.sh"
printf "\n4. Triggering remote deploy script\n\n"
ssh -i $HOME/.ssh/id_rsa_corda_digital_ocean -t tpcowan@djinmusic.ca < $BIN_DIR/deploy-remote.sh
#!/bin/bash
PATH=/home/tpcowan/.nvm/versions/node/v12.18.3/bin:$PATH
if [ "$(uname -s)" == "Darwin" ]; then
echo "Don't run this on your local computer!"
exit 1
fi
echo "[remote] linking nginx configuration"
sudo ln -s /etc/nginx/sites-available/djinlist/* /etc/nginx/sites-enabled/
sudo nginx -s reload
echo "[remote] Updating processor"
cd djinmusic
pnpm install -r
echo "[remote] Checking for djinlist-server"
server_check=$(pm2 pid djinlist-server)
echo "${server_check}"
echo "${#server_check}"
if [ ${#server_check} -eq 0 ]; then
echo "[remote] Starting djinlist-server"
cd services/channel
pm2 start node --name "djinlist-server" -l "../../djinlist-server.log" -- --experimental-modules --experimental-json-modules --es-module-specifier-resolution=node src/index.js
cd ../..
else
echo "[remote] Restarting djinlist-server"
pm2 stop djinlist-server
pm2 start djinlist-server
fi
echo "[remote] Checking for djinlist-app"
djiny_check=$(pm2 pid djinlist-app)
echo "${djiny_check}"
echo "${#djiny_check}"
if [ ${#djiny_check} -eq 0 ]; then
echo "[remote] Starting djinlist-app"
cd app/djiny
pm2 start npm --name "djinlist-app" -l "../../djinlist-app.log" -- start
cd ../..
else
echo "[remote] Restarting djinlist-app"
pm2 stop djinlist-app
pm2 start djinlist-app
fi
echo "[remote] Checking for djinlist-admin"
admin_check=$(pm2 pid djinlist-admin)
echo "${admin_check}"
echo "${#admin_check}"
if [ ${#admin_check} -eq 0 ]; then
echo "[remote] Starting djinlist-admin"
cd app/admin
pm2 start npm --name "djinlist-admin" -l "../../djinlist-admin.log" -- start
cd ../..
else
echo "[remote] Restarting djinlist-admin"
pm2 stop djinlist-admin
pm2 start djinlist-admin
fi
echo "[remote] Installed"
set -euo pipefail
IFS=$'\n\t'
gecho() {
local msg=$@
echo -e "\033[0;32m${msg}\033[0m"
}
recho() {
local msg=$@
echo -e "\033[0;31m${msg}\033[0m"
}
yecho() {
local msg=$@
echo -e "\033[0;93m${msg}\033[0m"
}
node_modules/