Last active
February 27, 2020 18:12
-
-
Save kenmueller/ebe54cebcd6d5785bc28a95e77a62153 to your computer and use it in GitHub Desktop.
Persistent store on the web, modeled after Firebase Firestore
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* - 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