Merge branch 'graffiti-garden:main' into main
This commit is contained in:
commit
fb3c976380
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
189
graffiti.js
189
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
|
||||
}
|
||||
|
||||
// Return the original in case of failure
|
||||
return originalObject
|
||||
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)
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ export default {
|
|||
|
||||
computed: {
|
||||
objects() {
|
||||
return graffiti.objectsByTags(...this.tags)
|
||||
return graffiti.objects(...this.tags)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue