summaryrefslogtreecommitdiff
path: root/graffiti.js
diff options
context:
space:
mode:
Diffstat (limited to 'graffiti.js')
-rw-r--r--graffiti.js351
1 files changed, 0 insertions, 351 deletions
diff --git a/graffiti.js b/graffiti.js
deleted file mode 100644
index 65c6ece..0000000
--- a/graffiti.js
+++ /dev/null
@@ -1,351 +0,0 @@
-import Auth from './src/auth.js'
-import GraffitiArray from './src/array.js'
-
-export default class {
-
- // There needs to be a new object map for each tag
- constructor(
- graffitiURL="https://graffiti.garden",
- objectConstructor=()=>({})) {
-
- this.graffitiURL = graffitiURL
- this.open = false
- this.eventTarget = new EventTarget()
- this.tagMap = objectConstructor() // tag->{count, Set(uuid)}
- this.objectMap = objectConstructor() // uuid->object
- this.GraffitiArray = GraffitiArray(this)
-
- this.#initialize()
- }
-
- async #initialize() {
- // Perform authorization
- this.authParams = await Auth.connect(this.graffitiURL)
-
- // Rewrite the URL
- this.wsURL = new URL(this.graffitiURL)
- this.wsURL.host = "app." + this.wsURL.host
- if (this.wsURL.protocol == 'https:') {
- this.wsURL.protocol = 'wss:'
- } else {
- this.wsURL.protocol = 'ws:'
- }
- if (this.authParams.token) {
- this.wsURL.searchParams.set("token", this.authParams.token)
- }
-
- // Commence connection
- this.#connect()
- }
-
- // Wait for the connection to be
- // open (state=true) or closed (state=false)
- async connectionState(state) {
- if (this.open != state) {
- await new Promise(resolve => {
- this.eventTarget.addEventListener(
- state? "open": "closed", ()=> resolve())
- })
- }
- }
-
- #connect() {
- this.ws = new WebSocket(this.wsURL)
- this.ws.onmessage = this.#onMessage.bind(this)
- this.ws.onclose = this.#onClose.bind(this)
- this.ws.onopen = this.#onOpen.bind(this)
- }
-
- // authorization functions
- get myID() { return this.authParams.myID }
- toggleLogIn() {
- this.myID? Auth.logOut() : Auth.logIn(this.graffitiURL)
- }
-
- async #onClose() {
- console.error("lost connection to graffiti server, attemping reconnect soon...")
- this.open = false
- this.eventTarget.dispatchEvent(new Event("closed"))
- await new Promise(resolve => setTimeout(resolve, 2000))
- this.#connect()
- }
-
- async #request(msg) {
- if (!this.open) {
- throw "Can't make request! Not connected to graffiti server"
- }
-
- // Create a random message ID
- const messageID = crypto.randomUUID()
-
- // Create a listener for the reply
- const dataPromise = new Promise(resolve => {
- this.eventTarget.addEventListener('$'+messageID, (e) => {
- resolve(e.data)
- })
- })
-
- // Send the request
- msg.messageID = messageID
- this.ws.send(JSON.stringify(msg))
-
- // Await the reply
- const data = await dataPromise
- delete data.messageID
-
- if ('error' in data) {
- throw data
- } else {
- return data.reply
- }
- }
-
- #onMessage(event) {
- const data = JSON.parse(event.data)
-
- if ('messageID' in data) {
- // It's a reply
- // Forward it back to the sender
- const messageEvent = new Event('$'+data.messageID)
- messageEvent.data = data
- this.eventTarget.dispatchEvent(messageEvent)
-
- } else if ('update' in data) {
- this.#updateCallback(data['update'])
-
- } else if ('remove' in data) {
- this.#removeCallback(data['remove'])
-
- } else if (data.type == 'error') {
- if (data.reason == 'authorization') {
- Auth.logOut()
- }
- throw data
- }
- }
-
- #updateCallback(object) {
- const uuid = this.#objectUUID(object)
-
- // Add the UUID to the tag map
- let subscribed = false
- for (const tag of object._tags) {
- if (!(tag in this.tagMap)) continue
- this.tagMap[tag].uuids.add(uuid)
- subscribed = true
- }
-
- if (!subscribed) return
-
- // Define object specific properties
- if (!('_id' in object)) {
- // Assign the object UUID
- Object.defineProperty(object, '_id', { value: uuid })
-
- // Add proxy functions so object modifications
- // sync with the server
- object = new Proxy(object, this.#objectHandler(object, true))
- }
-
- this.objectMap[uuid] = object
- }
-
- #removeCallback(object) {
- const uuid = this.#objectUUID(object)
-
- // Remove the UUID from all relevant tag maps
- let supported = false
- for (const tag in this.tagMap) {
- if (this.tagMap[tag].uuids.has(uuid)) {
- if (object._tags.includes(tag)) {
- this.tagMap[tag].uuids.delete(uuid)
- } else {
- supported = true
- }
- }
- }
-
- // If all tags have been removed, delete entirely
- if (!supported && uuid in this.objectMap) {
- delete this.objectMap[uuid]
- }
- }
-
- async update(object) {
- object._by = this.myID
- if (!object._key) object._key = crypto.randomUUID()
-
- // Immediately replace the object
- this.#updateCallback(object)
-
- // Send it to the server
- try {
- await this.#request({ update: object })
- } catch(e) {
- // Delete the temp object
- this.#removeCallback(object)
- throw e
- }
- }
-
- #objectHandler(object, root) {
- return {
- get: (target, prop, receiver)=>
- this.#getObjectProperty(object, target, prop, receiver),
- set: (target, prop, val, receiver)=>
- this.#setObjectProperty(object, root, target, prop, val, receiver),
- deleteProperty: (target, prop)=>
- this.#deleteObjectProperty(object, root, target, prop)
- }
- }
-
- #getObjectProperty(object, target, prop, receiver) {
- if (typeof target[prop] === 'object' && target[prop] !== null) {
- return new Proxy(Reflect.get(target, prop, receiver), this.#objectHandler(object, false))
- } else {
- return Reflect.get(target, prop, receiver)
- }
- }
-
- #setObjectProperty(object, root, target, prop, val, receiver) {
- // Store the original, perform the update,
- // sync with server and restore original if error
- const originalObject = Object.assign({}, object)
- if (Reflect.set(target, prop, val, receiver)) {
- this.#removeCallback(originalObject)
- this.#updateCallback(object)
- this.#request({ update: object }).catch(e=> {
- this.#removeCallback(object)
- this.#updateCallback(originalObject)
- throw e
- })
- return true
- } else { return false }
- }
-
- #deleteObjectProperty(object, root, target, prop) {
- const originalObject = Object.assign({}, object)
- if (root && ['_key', '_by', '_tags'].includes(prop)) {
- // This is a deletion of the whole object
- this.#removeCallback(object)
- this.#request({ remove: object._key }).catch(e=> {
- this.#updateCallback(originalObject)
- throw e
- })
- return true
- } else {
- if (Reflect.deleteProperty(target, prop)) {
- this.#request({ update: object }).catch(e=> {
- this.#updateCallback(originalObject)
- throw e
- })
- return true
- } else { return false }
- }
- }
-
- async myTags() {
- return await this.#request({ ls: null })
- }
-
- async objectByKey(userID, objectKey) {
- return await this.#request({ get: {
- _by: userID,
- _key: objectKey
- }})
- }
-
- objects(...tags) {
- tags = tags.filter(tag=> tag!=null)
- for (const tag of tags) {
- if (!(tag in this.tagMap)) {
- throw `You are not subscribed to '${tag}'`
- }
- }
-
- // Merge by UUIDs from all tags and
- // convert to relevant objects
- const uuids = new Set(tags.map(tag=>[...this.tagMap[tag].uuids]).flat())
- const objects = [...uuids].map(uuid=> this.objectMap[uuid])
-
- // Return an array wrapped with graffiti functions
- return new this.GraffitiArray(...objects)
- }
-
- async subscribe(...tags) {
- tags = tags.filter(tag=> tag!=null)
- // Look at what is already subscribed to
- const subscribingTags = []
- for (const tag of tags) {
- if (tag in this.tagMap) {
- // Increase the count
- this.tagMap[tag].count++
- } else {
- // Create a new slot
- this.tagMap[tag] = {
- uuids: new Set(),
- count: 1
- }
- subscribingTags.push(tag)
- }
- }
-
- // Try subscribing in the background
- // but don't raise an error since
- // the subscriptions will happen once connected
- if (subscribingTags.length)
- try {
- await this.#request({ subscribe: subscribingTags })
- } catch {}
- }
-
- async unsubscribe(...tags) {
- tags = tags.filter(tag=> tag!=null)
- // Decrease the count of each tag,
- // removing and marking if necessary
- const unsubscribingTags = []
- for (const tag of tags) {
- this.tagMap[tag].count--
-
- if (!this.tagMap[tag].count) {
- unsubscribingTags.push(tag)
- delete this.tagMap[tag]
- }
- }
-
- // Unsubscribe from all remaining tags
- if (unsubscribingTags.length)
- try {
- await this.#request({ unsubscribe: unsubscribingTags })
- } catch {}
- }
-
- async #onOpen() {
- console.log("connected to the graffiti socket")
- this.open = true
- this.eventTarget.dispatchEvent(new Event("open"))
-
- // Clear data
- for (let tag in this.tagMap) {
- this.tagMap[tag].uuids = new Set()
- }
- for (let uuid in this.objectMap) delete this.objectMap[uuid]
-
- // Resubscribe
- const tags = Object.keys(this.tagMap)
- if (tags.length) await this.#request({ subscribe: tags })
- }
-
- // Utility function to get a universally unique string
- // that represents a particular object
- #objectUUID(object) {
- if (!object._by || !object._key) {
- throw {
- type: 'error',
- content: 'the object you are trying to identify does not have an owner or key',
- object
- }
- }
- return object._by + object._key
- }
-}