diff options
-rw-r--r-- | graffiti.js | 180 | ||||
-rw-r--r-- | test.html | 73 |
2 files changed, 140 insertions, 113 deletions
diff --git a/graffiti.js b/graffiti.js index fe7c2d8..f0a087e 100644 --- a/graffiti.js +++ b/graffiti.js @@ -2,11 +2,16 @@ import Auth from './auth.js' export default class { - constructor(graffitiURL="https://graffiti.garden") { + // There needs to be a new object map for each tag + constructor( + graffitiURL="https://graffiti.garden", + objectMapConstructor=()=>({})) { + this.graffitiURL = graffitiURL + this.objectMapConstructor = objectMapConstructor this.open = false - this.subscriptionData = {} this.eventTarget = new EventTarget() + this.tagMap = {} } // CALL THIS BEFORE DOING ANYTHING ELSE @@ -26,8 +31,13 @@ export default class { this.wsURL.searchParams.set("token", this.authParams.token) } - // And commence connection + // Commence connection this.connect() + + // Wait until open + await new Promise(resolve => { + this.eventTarget.addEventListener("graffitiOpen", () => resolve() ) + }) } connect() { @@ -51,6 +61,10 @@ export default class { } async request(msg) { + if (!this.open) { + throw { 'error': 'Not connected!' } + } + // Create a random message ID const messageID = crypto.randomUUID() @@ -61,13 +75,6 @@ export default class { }) }) - // Wait for the socket to open - if (!this.open) { - await new Promise(resolve => { - this.eventTarget.addEventListener("graffitiOpen", () => resolve() ) - }) - } - // Send the request msg.messageID = messageID this.ws.send(JSON.stringify(msg)) @@ -79,7 +86,7 @@ export default class { if (data.type == 'error') { throw data } else { - return data + return data['reply'] } } @@ -93,30 +100,23 @@ export default class { messageEvent.data = data this.eventTarget.dispatchEvent(messageEvent) - } else if (['updates', 'removes'].includes(data.type)) { - // Subscription data - if (data.queryID in this.subscriptionData) { - const sd = this.subscriptionData[data.queryID] + } else if ('update' in data || 'remove' in data) { - // For each data point, either add or remove it - for (const r of data.results) { - if (data.type == 'updates') { - sd.updateCallback(r) - } else { - sd.removeCallback(r) - } - } + const object = 'update' in data? data['update'] : data['remove'] + const uuid = this.objectUUID(object) - // And update this query's notion of "now" - if (data.complete) { - if (data.historical) { - sd.historyComplete = true - } - if (sd.historyComplete) { - sd.since = data.now + for (const tag of object._tags) { + if (tag in this.tagMap) { + const om = this.tagMap[tag].objectMap + + if ('remove' in data) { + delete om[uuid] + } else { + om[uuid] = object } } } + } else if (data.type == 'error') { if (data.reason == 'authorization') { Auth.logOut() @@ -125,61 +125,98 @@ export default class { } } - async update(object, query) { - const data = await this.request({ object, query }) - return data.objectID + async update(object) { + // TODO + // Add the logic in vue to here + return await this.request({ update: object }) } - async remove(objectID) { - await this.request({ objectID }) + async remove(objectKey) { + // TODO + // same + return await this.request({ remove: objectKey }) } - async subscribe( - query, - updateCallback, - removeCallback, - flags={}, - since=null, - queryID=null) { + async lsTags() { + return await this.request({ ls: null }) + } - // Create a random query ID - if (!queryID) queryID = crypto.randomUUID() + async objectByKey(userID, objectKey) { + return await this.request({ get: { + _by: userID, + _key: objectKey + }}) + } - // Send the request - await this.request({ queryID, query, since, ...flags }) + objectsByTags(...tags) { + for (const tag of tags) { + if (!(tag in this.tagMap)) { + throw `You are not subscribed to '${tag}'` + } + } - // Store the subscription in case of disconnections - this.subscriptionData[queryID] = { - query, since, flags, updateCallback, removeCallback, - historyComplete: false + // Merge by UUID to combine all the maps + const combinedMaps = Object.assign({}, + ...tags.map(tag=> this.tagMap[tag].objectMap)) + + // Return just the array + return Object.values(combinedMaps) + } + + async subscribe(...tags) { + // 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] = { + objectMap: this.objectMapConstructor(), + count: 1 + } + subscribingTags.push(tag) + } } - return queryID + // Begin subscribing in the background + if (subscribingTags.length) + await this.request({ subscribe: subscribingTags }) } - async unsubscribe(queryID) { - // Remove allocated space - delete this.subscriptionData[queryID] + async unsubscribe(...tags) { + // 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] + } + } - // And unsubscribe - const data = await this.request({ queryID }) + // Unsubscribe from all remaining tags + if (unsubscribingTags.length) + await this.request({ unsubscribe: unsubscribingTags }) } async onOpen() { console.log("connected to the graffiti socket") this.open = true this.eventTarget.dispatchEvent(new Event("graffitiOpen")) - // Resubscribe to hanging queries - for (const queryID in this.subscriptionData) { - const sd = this.subscriptionData[queryID] - await this.subscribe( - sd.query, - sd.updateCallback, - sd.removeCallback, - sd.flags, - sd.since, - queryID) + + // Clear data + for (let tag in this.tagMap) { + const objectMap = this.tagMap[tag].objectMap + for (let uuid in objectMap) delete objectMap[uuid] } + + // Resubscribe + const tags = Object.keys(this.tagMap) + if (tags.length) await this.request({ subscribe: tags }) } // Adds required fields to an object. @@ -192,20 +229,21 @@ export default class { } // Pre-generate the object's ID if it does not already exist - if (!object._id) object._id = crypto.randomUUID() + if (!object._key) object._key = crypto.randomUUID() + + return object } // Utility function to get a universally unique string // that represents a particular object objectUUID(object) { - if (!object._id || !object._by) { + if (!object._by || !object._key) { throw { type: 'error', - content: 'the object you are trying to identify does not have an ID or owner', + content: 'the object you are trying to identify does not have an owner or key', object } } - return object._id + object._by + return object._by + object._key } - } @@ -5,7 +5,7 @@ <p id="ID"></p> - <h2 id="status">Unsubscribed</h2> + <h2 id="status"></h2> <button onclick="Subscribe()"> Subscribe @@ -31,7 +31,7 @@ import Graffiti from "./graffiti.js" // Connect to a local Graffiti instance - // (see the server README for how to run locally) + // (see the server README for how to n locally) const graffiti = new Graffiti("http://localhost:5001") await graffiti.initialize() @@ -41,57 +41,46 @@ window.LogOut = ()=> graffiti.toggleLogIn() document.getElementById('ID').innerHTML = `Your Graffiti ID is: ${graffiti.myID}` - // Create a display counter - let count = 0 - function displayCount() { - document.getElementById('status').innerHTML = `Subscribed: ${count} Objects` + const myTag = "asdf" + + // Make a display + async function displayObjects() { + await new Promise(r => setTimeout(r, 1000)); + + let display = 'not subscribed' + try { + const objects = graffiti.objectsByTags(myTag) + display = `objects: ${JSON.stringify(objects)}` + } catch {} + + document.getElementById('status').innerHTML = display + } + + // Create an object containing a special string + window.Subscribe = async function() { + await graffiti.subscribe(myTag) + displayObjects() } - // From here to below we're going to - // define functions that can be activated - // with button presses, corresponding to - // each of the four Graffiti primitives. + window.Unsubscribe = async function() { + await graffiti.unsubscribe(myTag) + displayObjects() + } // Create an object containing a special string - const special = crypto.randomUUID() - const usedIDs = [] window.Update = async function() { - usedIDs.unshift(crypto.randomUUID()) - await graffiti.update({ - _id: usedIDs[0], - _by: graffiti.myID, - special - }, {}) + console.log(await graffiti.update( + graffiti.completeObject({_tags: [myTag]}))) + displayObjects() } // Remove an existing object window.Remove = async function() { - if ( usedIDs.length ) { - await graffiti.remove( usedIDs.pop() ) - } + const objects = graffiti.objectsByTags(myTag) + console.log(await graffiti.remove(objects[0]._key)) + displayObjects() } - // Subscribe to objects containing the special string - let queryID = null - window.Subscribe = async function() { - if (queryID) return - count = 0 - queryID = await graffiti.subscribe( - { special }, - (obj)=> { count++; displayCount() }, - (obj)=> { count--; displayCount() } - ) - displayCount() - } - - // Unsubscribe to the existing query - window.Unsubscribe = async function() { - if (queryID) { - await graffiti.unsubscribe(queryID) - queryID = null - document.getElementById('status').innerHTML = "Unsubscribed" - } - } </script> </body> </html> |