From 74af0e0176b1194a3abc2a2a8586ebc163fdd024 Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 1 Feb 2023 16:38:10 -0500 Subject: works with new backend --- graffiti.js | 180 ++++++++++++++++++++++++++++++++++++------------------------ 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 } - } diff --git a/test.html b/test.html index 3a0d853..3623578 100644 --- a/test.html +++ b/test.html @@ -5,7 +5,7 @@

-

Unsubscribed

+

+ + +
  • + {{object._tags}} + +
  • +
    + + + diff --git a/plugins/vue/plugin.js b/plugins/vue/plugin.js new file mode 100644 index 0000000..e5483c6 --- /dev/null +++ b/plugins/vue/plugin.js @@ -0,0 +1,58 @@ +import { ref, reactive } from 'vue' +import Graffiti from '../../graffiti.js' + +export default { + install(app, options) { + + const graffitiURL = options && 'url' in options? + options.url : 'https://graffiti.garden' + + // Initialize graffiti + const graffiti = new Graffiti(graffitiURL, ()=>reactive({})) + + // These ID need to change after opening + app.config.globalProperties.$graffitiID = ref(null) + graffiti.waitTilOpen().then(()=> { + app.config.globalProperties.$graffitiID.value = graffiti.myID + }) + + // Add logging in and out + app.config.globalProperties.$graffitiToggleLogIn = + graffiti.toggleLogIn.bind(graffiti) + + // A composable for subscribing and + // unsubscribing to tags that returns + // a reactive array of the results + app.component('GraffitiObjects', { + + props: ['tags'], + + watch: { + tags: { + async handler(newTags, oldTags=[]) { + // Subscribe to the new tags + await graffiti.subscribe(...newTags) + // Unsubscribe to the existing tags + await graffiti.unsubscribe(...oldTags) + }, + immediate: true, + deep: true + } + }, + + // Handle unmounting too + unmount() { + graffiti.unsubscribe(this.tags) + }, + + computed: { + objects() { + return graffiti.objectsByTags(...this.tags) + } + }, + + template: '' + }) + + } +} diff --git a/test.html b/test.html index 400b0bb..3fb639f 100644 --- a/test.html +++ b/test.html @@ -32,8 +32,8 @@ // Connect to a local Graffiti instance // (see the server README for how to n locally) - const graffiti = new Graffiti("http://localhost:5001") - await graffiti.initialize() + window.graffiti = new Graffiti("http://localhost:5001") + await graffiti.waitTilOpen() // Log in automatically if not already // and supply a log out button diff --git a/utils/logoot.js b/utils/logoot.js new file mode 100644 index 0000000..c8c7c02 --- /dev/null +++ b/utils/logoot.js @@ -0,0 +1,153 @@ +export default { + + query(property) { + return { + [property]: { + $type: 'array', + $type: ['int', 'long'], + }, + $nor: [ + { [property]: { $gt: this.maxInt } }, + { [property]: { $lt: 0 } }, + ] + } + }, + + get before() { + return [] + }, + + get after() { + return [this.maxInt+1] + }, + + between(a, b, scale=100) { + // Strip zeros and find common length + const aLength = this.lengthWithoutZeros(a) + const bLength = this.lengthWithoutZeros(b) + const minLength = Math.min(aLength, bLength) + + // Initialize output + const out = [] + + // Find the break point where a[i] != b[i] + let i = 0 + while (i < minLength && a[i] == b[i]) { + out.push(a[i]) + i++ + } + + // Initialize upper and lower bounds for + // sampling the last digit + let lowerBound = 1 + let upperBound = this.maxInt + + if (i < minLength) { + // If the break happened before we hit + // the end of one of the arrays + + if (Math.abs(a[i] - b[i]) > 1) { + // If a[i] and b[i] are more than one + // away from each other, just sample + // between them + lowerBound = Math.min(a[i], b[i]) + 1 + upperBound = Math.max(a[i], b[i]) - 1 + } else { + // If they are one away no integers + // will fit in between, so add new layer + const lesser = (a[i] < b[i])? a : b + out.push(lesser[i]) + i++ + + while (i < lesser.length && lesser[i] >= this.maxInt) { + // If the lesser is at it's limit, + // we will need to add even more layers + out.push(lesser[i]) + i++ + } + + if (i < lesser.length) { + // Sample something greater than + // the lesser digit + lowerBound = lesser[i] + 1 + } + } + } else { + // The break happened because we hit + // the end of one of the arrays. + + if (aLength == bLength) { + // If they are entirely equal, + // there is nothing in between + // just return what we have + return out + } + + const longerLength = Math.max(aLength, bLength) + const longer = (a.length == longerLength)? a : b + while (i < longerLength && longer[i] == 0) { + // Skip past the zeros because we can't sample + // for digits less than zero + out.push(0) + i++ + } + + if (i < longerLength) { + if (longer[i] == 1) { + // If longer is at it's limit, + // we still need to add another layer + out.push(0) + } else { + upperBound = longer[i] - 1 + } + } + } + + // Create a random number in [0,1] but bias it to be small, + // so that numbers tend to increase by a small amount. + let random = Math.random() + random = -Math.log(1-random)/scale + random = Math.min(random, 1) + + // Finally, sample between the upper and lower bounds + out.push(Math.floor(random * (upperBound + 1 - lowerBound)) + lowerBound) + return out + }, + + compare(a, b) { + // Strip zeros and find common length + const aLength = this.lengthWithoutZeros(a) + const bLength = this.lengthWithoutZeros(b) + const minLength = Math.min(aLength, bLength) + + // See if there are any differences + for (let i = 0; i < minLength; i++) { + if (a[i] > b[i]) { + return 1 + } else if (a[i] < b[i]) { + return -1 + } + } + + // If they are all the same up til now, + // the longer one is bigger + if (aLength > bLength) { + return 1 + } else if (aLength < bLength) { + return -1 + } else { + return 0 + } + }, + + + lengthWithoutZeros(a) { + let length = a.length + while (length > 0 && a[length - 1] == 0) { + length-- + } + return length + }, + + maxInt: 9007199254740991, +} -- cgit v1.2.3-70-g09d2 From a2cc33b1de416bef592ecf25816f17017e62fe8e Mon Sep 17 00:00:00 2001 From: anonymous Date: Wed, 1 Feb 2023 19:59:23 -0500 Subject: minor changes --- README.md | 8 +-- auth.js | 125 ----------------------------------------- graffiti.js | 13 +++-- index.html | 84 +++++++++++++++++++++++++++ plugins/vue/plugin.js | 2 +- src/auth.js | 125 +++++++++++++++++++++++++++++++++++++++++ src/logoot.js | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++ test.html | 84 --------------------------- utils/logoot.js | 153 -------------------------------------------------- 9 files changed, 374 insertions(+), 373 deletions(-) delete mode 100644 auth.js create mode 100644 index.html create mode 100644 src/auth.js create mode 100644 src/logoot.js delete mode 100644 test.html delete mode 100644 utils/logoot.js diff --git a/README.md b/README.md index cf97d02..8c06ad0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# Graffiti for Vanilla Javascript +# Graffiti Javascript Library -This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/graffiti-garden/server). -We recommend not using this vanilla library itself but instead using framework plugins that are built on top of it like the [Graffiti plugin for Vue.JS](https://github.com/graffiti-garden/graffiti-x-vue). - -If you create a [local Graffiti instance](https://github.com/graffiti-garden/server#local-usage) and a local webserver in this directory (*e.g.* `python3 -m http.server`) and navigate to `test.html` (*e.g.* [http://localhost:8000/test.html](http://localhost:8000/test.html)) you should be able to log in and test each of the Graffiti primitives: `subscribe`, `unsubscribe`, `update`, `remove`. These primitives will be robust to spamming and network interruptions. +This is the base Javascript library that interfaces with the [Graffiti server](https://github.com/graffiti-garden/graffiti-server). +It also includes a plugin that extends it to operate with the [Vue.js framework](https://vuejs.org/). diff --git a/auth.js b/auth.js deleted file mode 100644 index 8ee803d..0000000 --- a/auth.js +++ /dev/null @@ -1,125 +0,0 @@ -export default { - - async logIn(graffitiURL) { - // Generate a random client secret and state - const clientSecret = crypto.randomUUID() - const state = crypto.randomUUID() - - // The client ID is the secret's hex hash - const clientID = await this.sha256(clientSecret) - - // Store the client secret as a local variable - window.localStorage.setItem('graffitiClientSecret', clientSecret) - window.localStorage.setItem('graffitiClientID', clientID) - window.localStorage.setItem('graffitiAuthState', state) - - // Redirect to the login window - const loginURL = this.authURL(graffitiURL) - loginURL.searchParams.set('client_id', clientID) - loginURL.searchParams.set('redirect_uri', window.location.href) - loginURL.searchParams.set('state', state) - window.location.href = loginURL - }, - - async connect(graffitiURL) { - - // Check to see if we are already logged in - let token = window.localStorage.getItem('graffitiToken') - let myID = window.localStorage.getItem('graffitiID') - - if (!token || !myID) { - // Remove them both in case one exists - // and the other does not - token = myID = null - - // Check to see if we are redirecting back - const url = new URL(window.location) - if (url.searchParams.has('code')) { - - // Extract the code and state from the URL and strip it from the history - const code = url.searchParams.get('code') - const state = url.searchParams.get('state') - url.searchParams.delete('code') - url.searchParams.delete('state') - window.history.replaceState({}, '', url) - - // Get stored variables and remove them - const clientSecret = window.localStorage.getItem('graffitiClientSecret') - const clientID = window.localStorage.getItem('graffitiClientID') - const storedState = window.localStorage.getItem('graffitiAuthState') - window.localStorage.removeItem('graffitiClientSecret') - window.localStorage.removeItem('graffitiClientID') - window.localStorage.removeItem('graffitiAuthState') - - // Make sure state has been preserved - if (state != storedState) { - throw new Error("The state in local storage does not match the state sent by the server") - } - - // Construct the body of the POST - let form = new FormData() - form.append('client_id', clientID) - form.append('client_secret', clientSecret) - form.append('code', code) - - // Ask to exchange the code for a token - const tokenURL = this.authURL(graffitiURL) - tokenURL.pathname = '/token' - const response = await fetch(tokenURL, { - method: 'post', - body: form - }) - - // Make sure the response is OK - if (!response.ok) { - let reason = response.status + ": " - try { - reason += (await response.json()).detail - } catch (e) { - reason += response.statusText - } - - throw new Error(`The authorization code could not be exchanged for a token.\n\n${reason}`) - } - - // Parse out the token - const data = await response.json() - token = data.access_token - myID = data.owner_id - - // And make sure that the token is valid - if (!token || !myID) { - throw new Error(`The authorization token could not be parsed from the response.\n\n${data}`) - } - - // Store the token and ID - window.localStorage.setItem('graffitiToken', token) - window.localStorage.setItem('graffitiID', myID) - } - } - - return { myID, token } - - }, - - logOut() { - window.localStorage.removeItem('graffitiToken') - window.localStorage.removeItem('graffitiID') - window.location.reload() - }, - - authURL(graffitiURL) { - const url = new URL(graffitiURL) - url.host = "auth." + url.host - return url - }, - - async sha256(input) { - const encoder = new TextEncoder() - const inputBytes = encoder.encode(input) - const outputBuffer = await crypto.subtle.digest('SHA-256', inputBytes) - const outputArray = Array.from(new Uint8Array(outputBuffer)) - return outputArray.map(b => b.toString(16).padStart(2, '0')).join('') - } - -} diff --git a/graffiti.js b/graffiti.js index d28b64d..9b92b4f 100644 --- a/graffiti.js +++ b/graffiti.js @@ -1,4 +1,5 @@ -import Auth from './auth.js' +import Auth from './src/auth.js' +//import Collection from './src/collection.js' export default class { @@ -36,10 +37,12 @@ export default class { this.#connect() } - async waitTilOpen() { - await new Promise(resolve => { - this.eventTarget.addEventListener("graffitiOpen", () => resolve() ) - }) + async opened() { + if (!this.open) { + await new Promise(resolve => { + this.eventTarget.addEventListener("graffitiOpen", () => resolve() ) + }) + } } #connect() { diff --git a/index.html b/index.html new file mode 100644 index 0000000..e3c2dc0 --- /dev/null +++ b/index.html @@ -0,0 +1,84 @@ + + + +

    Graffiti x JS

    + +

    + +

    + + + + + + + + + + + + + + diff --git a/plugins/vue/plugin.js b/plugins/vue/plugin.js index e5483c6..2c61f40 100644 --- a/plugins/vue/plugin.js +++ b/plugins/vue/plugin.js @@ -12,7 +12,7 @@ export default { // These ID need to change after opening app.config.globalProperties.$graffitiID = ref(null) - graffiti.waitTilOpen().then(()=> { + graffiti.opened().then(()=> { app.config.globalProperties.$graffitiID.value = graffiti.myID }) diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 0000000..8ee803d --- /dev/null +++ b/src/auth.js @@ -0,0 +1,125 @@ +export default { + + async logIn(graffitiURL) { + // Generate a random client secret and state + const clientSecret = crypto.randomUUID() + const state = crypto.randomUUID() + + // The client ID is the secret's hex hash + const clientID = await this.sha256(clientSecret) + + // Store the client secret as a local variable + window.localStorage.setItem('graffitiClientSecret', clientSecret) + window.localStorage.setItem('graffitiClientID', clientID) + window.localStorage.setItem('graffitiAuthState', state) + + // Redirect to the login window + const loginURL = this.authURL(graffitiURL) + loginURL.searchParams.set('client_id', clientID) + loginURL.searchParams.set('redirect_uri', window.location.href) + loginURL.searchParams.set('state', state) + window.location.href = loginURL + }, + + async connect(graffitiURL) { + + // Check to see if we are already logged in + let token = window.localStorage.getItem('graffitiToken') + let myID = window.localStorage.getItem('graffitiID') + + if (!token || !myID) { + // Remove them both in case one exists + // and the other does not + token = myID = null + + // Check to see if we are redirecting back + const url = new URL(window.location) + if (url.searchParams.has('code')) { + + // Extract the code and state from the URL and strip it from the history + const code = url.searchParams.get('code') + const state = url.searchParams.get('state') + url.searchParams.delete('code') + url.searchParams.delete('state') + window.history.replaceState({}, '', url) + + // Get stored variables and remove them + const clientSecret = window.localStorage.getItem('graffitiClientSecret') + const clientID = window.localStorage.getItem('graffitiClientID') + const storedState = window.localStorage.getItem('graffitiAuthState') + window.localStorage.removeItem('graffitiClientSecret') + window.localStorage.removeItem('graffitiClientID') + window.localStorage.removeItem('graffitiAuthState') + + // Make sure state has been preserved + if (state != storedState) { + throw new Error("The state in local storage does not match the state sent by the server") + } + + // Construct the body of the POST + let form = new FormData() + form.append('client_id', clientID) + form.append('client_secret', clientSecret) + form.append('code', code) + + // Ask to exchange the code for a token + const tokenURL = this.authURL(graffitiURL) + tokenURL.pathname = '/token' + const response = await fetch(tokenURL, { + method: 'post', + body: form + }) + + // Make sure the response is OK + if (!response.ok) { + let reason = response.status + ": " + try { + reason += (await response.json()).detail + } catch (e) { + reason += response.statusText + } + + throw new Error(`The authorization code could not be exchanged for a token.\n\n${reason}`) + } + + // Parse out the token + const data = await response.json() + token = data.access_token + myID = data.owner_id + + // And make sure that the token is valid + if (!token || !myID) { + throw new Error(`The authorization token could not be parsed from the response.\n\n${data}`) + } + + // Store the token and ID + window.localStorage.setItem('graffitiToken', token) + window.localStorage.setItem('graffitiID', myID) + } + } + + return { myID, token } + + }, + + logOut() { + window.localStorage.removeItem('graffitiToken') + window.localStorage.removeItem('graffitiID') + window.location.reload() + }, + + authURL(graffitiURL) { + const url = new URL(graffitiURL) + url.host = "auth." + url.host + return url + }, + + async sha256(input) { + const encoder = new TextEncoder() + const inputBytes = encoder.encode(input) + const outputBuffer = await crypto.subtle.digest('SHA-256', inputBytes) + const outputArray = Array.from(new Uint8Array(outputBuffer)) + return outputArray.map(b => b.toString(16).padStart(2, '0')).join('') + } + +} diff --git a/src/logoot.js b/src/logoot.js new file mode 100644 index 0000000..c8c7c02 --- /dev/null +++ b/src/logoot.js @@ -0,0 +1,153 @@ +export default { + + query(property) { + return { + [property]: { + $type: 'array', + $type: ['int', 'long'], + }, + $nor: [ + { [property]: { $gt: this.maxInt } }, + { [property]: { $lt: 0 } }, + ] + } + }, + + get before() { + return [] + }, + + get after() { + return [this.maxInt+1] + }, + + between(a, b, scale=100) { + // Strip zeros and find common length + const aLength = this.lengthWithoutZeros(a) + const bLength = this.lengthWithoutZeros(b) + const minLength = Math.min(aLength, bLength) + + // Initialize output + const out = [] + + // Find the break point where a[i] != b[i] + let i = 0 + while (i < minLength && a[i] == b[i]) { + out.push(a[i]) + i++ + } + + // Initialize upper and lower bounds for + // sampling the last digit + let lowerBound = 1 + let upperBound = this.maxInt + + if (i < minLength) { + // If the break happened before we hit + // the end of one of the arrays + + if (Math.abs(a[i] - b[i]) > 1) { + // If a[i] and b[i] are more than one + // away from each other, just sample + // between them + lowerBound = Math.min(a[i], b[i]) + 1 + upperBound = Math.max(a[i], b[i]) - 1 + } else { + // If they are one away no integers + // will fit in between, so add new layer + const lesser = (a[i] < b[i])? a : b + out.push(lesser[i]) + i++ + + while (i < lesser.length && lesser[i] >= this.maxInt) { + // If the lesser is at it's limit, + // we will need to add even more layers + out.push(lesser[i]) + i++ + } + + if (i < lesser.length) { + // Sample something greater than + // the lesser digit + lowerBound = lesser[i] + 1 + } + } + } else { + // The break happened because we hit + // the end of one of the arrays. + + if (aLength == bLength) { + // If they are entirely equal, + // there is nothing in between + // just return what we have + return out + } + + const longerLength = Math.max(aLength, bLength) + const longer = (a.length == longerLength)? a : b + while (i < longerLength && longer[i] == 0) { + // Skip past the zeros because we can't sample + // for digits less than zero + out.push(0) + i++ + } + + if (i < longerLength) { + if (longer[i] == 1) { + // If longer is at it's limit, + // we still need to add another layer + out.push(0) + } else { + upperBound = longer[i] - 1 + } + } + } + + // Create a random number in [0,1] but bias it to be small, + // so that numbers tend to increase by a small amount. + let random = Math.random() + random = -Math.log(1-random)/scale + random = Math.min(random, 1) + + // Finally, sample between the upper and lower bounds + out.push(Math.floor(random * (upperBound + 1 - lowerBound)) + lowerBound) + return out + }, + + compare(a, b) { + // Strip zeros and find common length + const aLength = this.lengthWithoutZeros(a) + const bLength = this.lengthWithoutZeros(b) + const minLength = Math.min(aLength, bLength) + + // See if there are any differences + for (let i = 0; i < minLength; i++) { + if (a[i] > b[i]) { + return 1 + } else if (a[i] < b[i]) { + return -1 + } + } + + // If they are all the same up til now, + // the longer one is bigger + if (aLength > bLength) { + return 1 + } else if (aLength < bLength) { + return -1 + } else { + return 0 + } + }, + + + lengthWithoutZeros(a) { + let length = a.length + while (length > 0 && a[length - 1] == 0) { + length-- + } + return length + }, + + maxInt: 9007199254740991, +} diff --git a/test.html b/test.html deleted file mode 100644 index 3fb639f..0000000 --- a/test.html +++ /dev/null @@ -1,84 +0,0 @@ - - - -

    Graffiti x JS

    - -

    - -

    - - - - - - - - - - - - - - diff --git a/utils/logoot.js b/utils/logoot.js deleted file mode 100644 index c8c7c02..0000000 --- a/utils/logoot.js +++ /dev/null @@ -1,153 +0,0 @@ -export default { - - query(property) { - return { - [property]: { - $type: 'array', - $type: ['int', 'long'], - }, - $nor: [ - { [property]: { $gt: this.maxInt } }, - { [property]: { $lt: 0 } }, - ] - } - }, - - get before() { - return [] - }, - - get after() { - return [this.maxInt+1] - }, - - between(a, b, scale=100) { - // Strip zeros and find common length - const aLength = this.lengthWithoutZeros(a) - const bLength = this.lengthWithoutZeros(b) - const minLength = Math.min(aLength, bLength) - - // Initialize output - const out = [] - - // Find the break point where a[i] != b[i] - let i = 0 - while (i < minLength && a[i] == b[i]) { - out.push(a[i]) - i++ - } - - // Initialize upper and lower bounds for - // sampling the last digit - let lowerBound = 1 - let upperBound = this.maxInt - - if (i < minLength) { - // If the break happened before we hit - // the end of one of the arrays - - if (Math.abs(a[i] - b[i]) > 1) { - // If a[i] and b[i] are more than one - // away from each other, just sample - // between them - lowerBound = Math.min(a[i], b[i]) + 1 - upperBound = Math.max(a[i], b[i]) - 1 - } else { - // If they are one away no integers - // will fit in between, so add new layer - const lesser = (a[i] < b[i])? a : b - out.push(lesser[i]) - i++ - - while (i < lesser.length && lesser[i] >= this.maxInt) { - // If the lesser is at it's limit, - // we will need to add even more layers - out.push(lesser[i]) - i++ - } - - if (i < lesser.length) { - // Sample something greater than - // the lesser digit - lowerBound = lesser[i] + 1 - } - } - } else { - // The break happened because we hit - // the end of one of the arrays. - - if (aLength == bLength) { - // If they are entirely equal, - // there is nothing in between - // just return what we have - return out - } - - const longerLength = Math.max(aLength, bLength) - const longer = (a.length == longerLength)? a : b - while (i < longerLength && longer[i] == 0) { - // Skip past the zeros because we can't sample - // for digits less than zero - out.push(0) - i++ - } - - if (i < longerLength) { - if (longer[i] == 1) { - // If longer is at it's limit, - // we still need to add another layer - out.push(0) - } else { - upperBound = longer[i] - 1 - } - } - } - - // Create a random number in [0,1] but bias it to be small, - // so that numbers tend to increase by a small amount. - let random = Math.random() - random = -Math.log(1-random)/scale - random = Math.min(random, 1) - - // Finally, sample between the upper and lower bounds - out.push(Math.floor(random * (upperBound + 1 - lowerBound)) + lowerBound) - return out - }, - - compare(a, b) { - // Strip zeros and find common length - const aLength = this.lengthWithoutZeros(a) - const bLength = this.lengthWithoutZeros(b) - const minLength = Math.min(aLength, bLength) - - // See if there are any differences - for (let i = 0; i < minLength; i++) { - if (a[i] > b[i]) { - return 1 - } else if (a[i] < b[i]) { - return -1 - } - } - - // If they are all the same up til now, - // the longer one is bigger - if (aLength > bLength) { - return 1 - } else if (aLength < bLength) { - return -1 - } else { - return 0 - } - }, - - - lengthWithoutZeros(a) { - let length = a.length - while (length > 0 && a[length - 1] == 0) { - length-- - } - return length - }, - - maxInt: 9007199254740991, -} -- cgit v1.2.3-70-g09d2