Skip to content

Instantly share code, notes, and snippets.

@kenmueller
Last active February 27, 2020 18:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kenmueller/ebe54cebcd6d5785bc28a95e77a62153 to your computer and use it in GitHub Desktop.
Save kenmueller/ebe54cebcd6d5785bc28a95e77a62153 to your computer and use it in GitHub Desktop.
Persistent store on the web, modeled after Firebase Firestore
/*
* - Access a collection
* store.collection('users')
*
* - Access a document
* store.collection('users').document('abc')
* store.document('users/abc')
*
* - Get all the documents in a collection
* store.collection('users').documents() // Forced to reload documents
* store.collection('users').documents(true) // Retrieved from cache
*
* - Access the data of a document
* store.document('users/abc').data() // Forced to reload data
* store.document('users/def').data(true) // Retrieved from cache
*/
const __pathCount = paths =>
[].concat(...paths.map(path => path.split('/'))).length
const __randomDocumentId = () =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0
return (c === 'x' ? r : r & 0x3 | 0x8).toString(16)
})
const __collectionSnapshotListeners = {}
const __onCollectionSnapshot = snapshot => {
for (const callback of __collectionSnapshotListeners[snapshot.collection._path] || [])
callback(snapshot)
}
const __documentSnapshotListeners = {}
const __onDocumentSnapshot = snapshot => {
for (const callback of __documentSnapshotListeners[snapshot.document._path] || [])
callback(snapshot)
}
class Store {
collection = (...paths) =>
new Collection(...paths)
document = (...paths) =>
new Document(null, ...paths)
}
class Collection {
constructor(...paths) {
paths = paths.filter(path => path)
if (!(__pathCount(paths) % 2))
throw new Error('The number of components in a collection path must be odd')
this._path = paths.join('/')
}
get id() {
return this._path.substring(this._path.lastIndexOf('/') + 1, this._path.length)
}
documents = (fromCache = false) => {
if (fromCache && this._documents)
return this._documents
const rawDocuments = localStorage.getItem(`${this._path}__DOCUMENTS__`)
return this._documents = rawDocuments
? JSON.parse(rawDocuments).map(this.document)
: []
}
addSnapshotListener = callback => {
__collectionSnapshotListeners[this._path] = [
...__collectionSnapshotListeners[this._path] || [],
callback
]
const documents = this.documents()
for (const document of documents)
callback({
documents,
document,
type: 'added'
})
}
_addToRawDocuments = id => {
const rawDocuments = localStorage.getItem(`${this._path}__DOCUMENTS__`)
localStorage.setItem(
`${this._path}__DOCUMENTS__`,
JSON.stringify([
...new Set([
...rawDocuments ? JSON.parse(rawDocuments) : [],
id
])
])
)
return (rawDocuments || []).includes(id) ? 'modified' : 'added'
}
_removeFromRawDocuments = id => {
const rawDocuments = localStorage.getItem(`${this._path}__DOCUMENTS__`)
if (!rawDocuments)
return false
localStorage.setItem(
`${this._path}__DOCUMENTS__`,
JSON.stringify(
JSON.parse(rawDocuments).filter(documentId =>
documentId !== id
)
)
)
return rawDocuments.includes(id)
}
document = id =>
new Document(this, this._path, id || __randomDocumentId())
add = data =>
this.document().set(data)
delete = () => {
for (const document of this.documents())
document.delete()
return this
}
}
class Document {
constructor(parent, ...paths) {
paths = paths.filter(path => path)
if (__pathCount(paths) % 2)
throw new Error('The number of components in a document path must be even')
this._parent = parent || new Collection(...paths.slice(0, -1))
this._path = paths.join('/')
}
get id() {
return this._path.substring(this._path.lastIndexOf('/') + 1, this._path.length)
}
collection = (...paths) =>
new Collection(this._path, ...paths)
addSnapshotListener = callback => {
__documentSnapshotListeners[this._path] = [
...__documentSnapshotListeners[this._path] || [],
callback
]
if (this.data())
callback({
parent: this._parent,
document: this,
type: 'added'
})
}
data = (fromCache = false) => {
if (fromCache && this._data !== undefined)
return this._data
const rawData = localStorage.getItem(this._path)
return this._data = rawData && JSON.parse(rawData)
}
get = (field, fromCache = false) => {
const data = this.data(fromCache)
return data && data[field]
}
set = data => {
localStorage.setItem(this._path, JSON.stringify(data))
this._data = data
const type = this._parent._addToRawDocuments(this.id)
__onCollectionSnapshot({
collection: this._parent,
documents: this._parent.documents(),
document: this,
type
})
__onDocumentSnapshot({
parent: this._parent,
document: this,
type
})
return this
}
update = (data, fromCache = false) => {
localStorage.setItem(
this._path,
JSON.stringify({ ...this.data(fromCache) || {}, ...data })
)
this._data = data
const type = this._parent._addToRawDocuments(this.id)
__onCollectionSnapshot({
collection: this._parent,
documents: this._parent.documents(),
document: this,
type
})
__onDocumentSnapshot({
parent: this._parent,
document: this,
type
})
return this
}
delete = () => {
localStorage.removeItem(this._path)
this._data = null
if (this._parent._removeFromRawDocuments(this.id)) {
__onCollectionSnapshot({
collection: this._parent,
documents: this._parent.documents(),
document: this,
type: 'removed'
})
__onDocumentSnapshot({
parent: this._parent,
document: this,
type: 'removed'
})
}
return this
}
}
const store = new Store
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment