diff options
author | Anthony Wang | 2023-02-09 19:10:01 -0500 |
---|---|---|
committer | GitHub | 2023-02-09 19:10:01 -0500 |
commit | fb3c976380e704a1c129f141f0bdb7f5f4576611 (patch) | |
tree | 961ca925762b2953ad0b0d69374a0d5d2721915e | |
parent | 53b54eb076246ffeb6c5c4e8b52a643696195333 (diff) | |
parent | 7413cb9d937986fd11936730d6773f1a270eba48 (diff) |
Merge branch 'graffiti-garden:main' into main
-rw-r--r-- | demo/components/chat.js | 4 | ||||
-rw-r--r-- | demo/components/moderation.js | 2 | ||||
-rw-r--r-- | demo/index.html | 1 | ||||
-rw-r--r-- | graffiti.js | 189 | ||||
-rw-r--r-- | plugins/vue/plugin.js | 2 | ||||
-rw-r--r-- | src/array.js | 5 |
6 files changed, 102 insertions, 101 deletions
diff --git a/demo/components/chat.js b/demo/components/chat.js index bffde26..5095dcd 100644 --- a/demo/components/chat.js +++ b/demo/components/chat.js @@ -45,11 +45,11 @@ export default { <Comments :messageID="object._id" /> <template v-if="object._by==$graffitiMyID"> - <button @click="object.message+='!!';object._update()"> + <button @click="object.message+='!!'"> ‼️ </button> - <button @click="object._remove()"> + <button @click="delete object._key"> ❌ </button> </template> diff --git a/demo/components/moderation.js b/demo/components/moderation.js index 4cf8071..5e12fb0 100644 --- a/demo/components/moderation.js +++ b/demo/components/moderation.js @@ -51,7 +51,7 @@ export default { <ul v-for="object in messageObjects(objects)"> <graffiti-objects :tags="[object._id]" v-slot="{objects: responses}"> - <li v-if="likeObjects(responses, object._id).filter(o=> o._by=admin).length"> + <li v-if="likeObjects(responses, object._id).filter(o=> o._by==admin).length"> <em><Name :of="object._by"/></em>: {{ object.message }} </li> diff --git a/demo/index.html b/demo/index.html index 1f05b25..d1b5991 100644 --- a/demo/index.html +++ b/demo/index.html @@ -11,7 +11,6 @@ "vue": "https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.45/vue.esm-browser.prod.min.js", "graffiti-vue": "../plugins/vue/plugin.js" }}</script> - <!--"graffiti-vue": "https://graffiti.garden/graffiti-js/plugins/vue/plugin.js"--> <script type="module"> import { createApp } from 'vue' import { Name, SetMyName } from './components/name.js' diff --git a/graffiti.js b/graffiti.js index 824fdf6..65c6ece 100644 --- a/graffiti.js +++ b/graffiti.js @@ -6,13 +6,13 @@ export default class { // There needs to be a new object map for each tag constructor( graffitiURL="https://graffiti.garden", - objectMapConstructor=()=>({})) { + objectConstructor=()=>({})) { this.graffitiURL = graffitiURL - this.objectMapConstructor = objectMapConstructor this.open = false this.eventTarget = new EventTarget() - this.tagMap = {} + this.tagMap = objectConstructor() // tag->{count, Set(uuid)} + this.objectMap = objectConstructor() // uuid->object this.GraffitiArray = GraffitiArray(this) this.#initialize() @@ -93,10 +93,10 @@ export default class { const data = await dataPromise delete data.messageID - if (data.type == 'error') { + if ('error' in data) { throw data } else { - return data['reply'] + return data.reply } } @@ -127,101 +127,120 @@ export default class { #updateCallback(object) { const uuid = this.#objectUUID(object) - let originalObject = null + // Add the UUID to the tag map + let subscribed = false for (const tag of object._tags) { if (!(tag in this.tagMap)) continue - const objectMap = this.tagMap[tag].objectMap - - if (uuid in objectMap) { - // Copy the original object if - // one exists, in case of failure - originalObject = Object.assign({},objectMap[uuid]) - - // Replace the object by copying - // so references to it don't break - this.#recursiveCopy(objectMap[uuid], object) - } else if (!('_id' in object)) { - - // Add properties to the object - // so it can be updated and removed - // without the collection - Object.defineProperty(object, '_id', { value: this.#objectUUID(object) }) - Object.defineProperty(object, '_update', { value: ()=>this.update(object) }) - Object.defineProperty(object, '_remove', { value: ()=>this.remove(object) }) - - objectMap[uuid] = object - } + 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)) } - // Return the original in case of failure - return originalObject + this.objectMap[uuid] = object } #removeCallback(object) { const uuid = this.#objectUUID(object) - let originalObject = null - for (const tag of object._tags) { - if (!(tag in this.tagMap)) continue - const objectMap = this.tagMap[tag].objectMap + // 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 (!(uuid in objectMap)) return - originalObject = Object.assign({},objectMap[uuid]) - delete objectMap[uuid] + // If all tags have been removed, delete entirely + if (!supported && uuid in this.objectMap) { + delete this.objectMap[uuid] } } async update(object) { - if (!this.myID) { - throw 'you can\'t update objects without logging in!' - } - - // Add by/to fields object._by = this.myID - if ('_to' in object && !Array.isArray(object._to)) { - throw new Error("_to must be an array") - } - - // Pre-generate the object's ID if it does not already exist if (!object._key) object._key = crypto.randomUUID() // Immediately replace the object - const originalObject = this.#updateCallback(object) + this.#updateCallback(object) // Send it to the server try { await this.#request({ update: object }) } catch(e) { - if (originalObject) { - // Restore the original object - this.#updateCallback(originalObject) - } else { - // Delete the temp object - this.#removeCallback(object) - } + // Delete the temp object + this.#removeCallback(object) throw e } } - async remove(object) { - if (!this.myID) { - throw 'you can\'t remove objects without logging in!' + #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) } + } - if (this.myID != object._by) { - throw 'you can\'t remove an object that isn\'t yours!' + #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) } + } - // Immediately remove the object - // but store it in case there is an error - const originalObject = this.#removeCallback(object) + #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 } + } - try { - return await this.#request({ remove: object._key }) - } catch(e) { - // Delete failed, restore the object - if (originalObject) this.#updateCallback(originalObject) - throw e + #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 } } } @@ -236,7 +255,7 @@ export default class { }}) } - objectsByTags(...tags) { + objects(...tags) { tags = tags.filter(tag=> tag!=null) for (const tag of tags) { if (!(tag in this.tagMap)) { @@ -244,12 +263,13 @@ export default class { } } - // Merge by UUID to combine all the maps - const combinedMaps = Object.assign({}, - ...tags.map(tag=> this.tagMap[tag].objectMap)) + // 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(...Object.values(combinedMaps)) + return new this.GraffitiArray(...objects) } async subscribe(...tags) { @@ -263,7 +283,7 @@ export default class { } else { // Create a new slot this.tagMap[tag] = { - objectMap: this.objectMapConstructor(), + uuids: new Set(), count: 1 } subscribingTags.push(tag) @@ -307,9 +327,9 @@ export default class { // Clear data for (let tag in this.tagMap) { - const objectMap = this.tagMap[tag].objectMap - for (let uuid in objectMap) delete objectMap[uuid] + this.tagMap[tag].uuids = new Set() } + for (let uuid in this.objectMap) delete this.objectMap[uuid] // Resubscribe const tags = Object.keys(this.tagMap) @@ -328,21 +348,4 @@ export default class { } return object._by + object._key } - - #recursiveCopy(target, source) { - for (const field in target) { - if (!(field in source)) { - delete target[field] - } - } - - for (const field in source) { - if (field in target && typeof target[field] == 'object' && typeof source[field] == 'object') { - this.#recursiveCopy(target[field], source[field]) - } else { - target[field] = source[field] - } - } - } } - diff --git a/plugins/vue/plugin.js b/plugins/vue/plugin.js index f789449..f04098e 100644 --- a/plugins/vue/plugin.js +++ b/plugins/vue/plugin.js @@ -65,7 +65,7 @@ export default { computed: { objects() { - return graffiti.objectsByTags(...this.tags) + return graffiti.objects(...this.tags) } }, diff --git a/src/array.js b/src/array.js index c53113e..ae30012 100644 --- a/src/array.js +++ b/src/array.js @@ -17,9 +17,8 @@ export default function(graffiti) { return [...new Set(this.map(o=> o._by))] } - async removeMine() { - await Promise.all( - this.mine.map(async o=> await o._remove())) + removeMine() { + this.mine.map(o=> delete o._key) } #getProperty(obj, propertyPath) { |