Delete all files except for demo
This commit is contained in:
parent
fb3c976380
commit
091ff04c24
|
@ -1,5 +0,0 @@
|
|||
# Graffiti Javascript Library
|
||||
|
||||
This library enables any webpage to interface 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/).
|
||||
|
||||
Check out the live [demo](https://graffiti.garden/graffiti-js/demo) of the library and plugin in action. The demo's source code is in the [`/demo`](https://github.com/graffiti-garden/graffiti-js/tree/main/demo) folder.
|
351
graffiti.js
351
graffiti.js
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
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 with reactive entries
|
||||
const graffiti = new Graffiti(graffitiURL, ()=>reactive({}))
|
||||
|
||||
// Create a reactive variable that
|
||||
// tracks connection state
|
||||
const connectionState = ref(false)
|
||||
;(function waitForState(state) {
|
||||
graffiti.connectionState(state).then(()=> {
|
||||
connectionState.value = state
|
||||
waitForState(!state)
|
||||
})})(true)
|
||||
Object.defineProperty(app.config.globalProperties, "$graffitiConnected", {
|
||||
get: ()=> connectionState.value
|
||||
})
|
||||
|
||||
// Latch on to the graffiti ID
|
||||
// when the connection state first becomes true
|
||||
let myID = null
|
||||
Object.defineProperty(app.config.globalProperties, "$graffitiMyID", {
|
||||
get: ()=> {
|
||||
if (connectionState.value) myID = graffiti.myID
|
||||
return myID
|
||||
}
|
||||
})
|
||||
|
||||
// Add static functions
|
||||
for (const key of ['toggleLogIn', 'update', 'myTags', 'objectByKey']) {
|
||||
const vueKey = '$graffiti' + key.charAt(0).toUpperCase() + key.slice(1)
|
||||
app.config.globalProperties[vueKey] = graffiti[key].bind(graffiti)
|
||||
}
|
||||
|
||||
// A component 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.objects(...this.tags)
|
||||
}
|
||||
},
|
||||
|
||||
template: '<slot :objects="objects"></slot>'
|
||||
})
|
||||
|
||||
}
|
||||
}
|
61
src/array.js
61
src/array.js
|
@ -1,61 +0,0 @@
|
|||
// Extend the array class to expose update
|
||||
// functionality, plus provide some
|
||||
// useful helper methods
|
||||
export default function(graffiti) {
|
||||
|
||||
return class GraffitiArray extends Array {
|
||||
|
||||
get mine() {
|
||||
return this.filter(o=> o._by==graffiti.myID)
|
||||
}
|
||||
|
||||
get notMine() {
|
||||
return this.filter(o=> o._by!=graffiti.myID)
|
||||
}
|
||||
|
||||
get authors() {
|
||||
return [...new Set(this.map(o=> o._by))]
|
||||
}
|
||||
|
||||
removeMine() {
|
||||
this.mine.map(o=> delete o._key)
|
||||
}
|
||||
|
||||
#getProperty(obj, propertyPath) {
|
||||
// Split it up by periods
|
||||
propertyPath = propertyPath.match(/([^\.]+)/g)
|
||||
// Traverse down the path tree
|
||||
for (const property of propertyPath) {
|
||||
obj = obj[property]
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
sortBy(propertyPath) {
|
||||
|
||||
const sortOrder = propertyPath[0] == '-'? -1 : 1
|
||||
if (sortOrder < 0) propertyPath = propertyPath.substring(1)
|
||||
|
||||
return this.sort((a, b)=> {
|
||||
const propertyA = this.#getProperty(a, propertyPath)
|
||||
const propertyB = this.#getProperty(b, propertyPath)
|
||||
return sortOrder * (
|
||||
propertyA < propertyB? -1 :
|
||||
propertyA > propertyB? 1 : 0 )
|
||||
})
|
||||
}
|
||||
|
||||
groupBy(propertyPath) {
|
||||
return this.reduce((chain, obj)=> {
|
||||
const property = this.#getProperty(obj, propertyPath)
|
||||
if (property in chain) {
|
||||
chain[property].push(obj)
|
||||
} else {
|
||||
chain[property] = new GraffitiArray(obj)
|
||||
}
|
||||
return chain
|
||||
}, {})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
125
src/auth.js
125
src/auth.js
|
@ -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('')
|
||||
}
|
||||
|
||||
}
|
153
src/logoot.js
153
src/logoot.js
|
@ -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,
|
||||
}
|
Loading…
Reference in a new issue