Skip to content

Instantly share code, notes, and snippets.

@zerobias
Created November 27, 2019 12:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save zerobias/db9f433ddd6cc8e30d008ab63d0d8d9a to your computer and use it in GitHub Desktop.
Save zerobias/db9f433ddd6cc8e30d008ab63d0d8d9a to your computer and use it in GitHub Desktop.
Reactive database with localStorage and effector

Reactive database with localStorage and effector

Try it

Usage

import {createDB} from './lsdb'

const db = createDB('LSTestDB', {
  columns: {
    foo: '',
    listeners: 0
  }
})
db.columns.foo.watch(val => {
  console.log('foo column state', val)
})

db.state.watch(state => {
  console.log('db %s state', db.name, state)
})

await db.start()

db.updates.foo('bar')
// => foo column state bar
// => db LSTestDB state {foo: bar, listeners: 0}
import {
createEvent,
createStore,
createEffect,
combine,
forward,
Effect,
Store,
Event,
step
} from 'effector'
type DBStatus = 'stop' | 'start' | 'idle' | 'error' | 'sync'
export type DB<T extends {[column: string]: any}> = {
name: string
status: Store<DBStatus>
columns: {
[K in keyof T]: Store<T[K]>
}
updates: {
[K in keyof T]: Event<T[K]>
}
state: Store<T>
error: Store<any>
onError: Event<{
params: {
db: string
column: string
value: any
}
error: Error
}>
start: Effect<void, T>
remove: {
db: Effect<void, void>
column: Effect<keyof T, void>
}
}
type Column<T> = {
defaultValue: T
}
export const onStorage = createEvent<StorageEvent>()
window.addEventListener('storage', onStorage)
const updateDB = createEffect({
handler({db, column, value}: {db: string; column: string; value: any}) {
setItem(value, getDBPath(column, db))
}
})
const onSerializedValue = onStorage.map(ev => {
const status: 'add' | 'delete' | 'change' =
ev.oldValue === null ? 'add' : ev.newValue === null ? 'delete' : 'change'
let newValue = null
try {
newValue = JSON.parse(ev.newValue)
} catch(err) {}
return {
event: ev,
value: newValue,
status
}
})
const setItem = (value: any, keys: (string | number)[], sep: string = '/') => {
const fullPath = keys.join(sep)
const serialized = JSON.stringify(value)
localStorage.setItem(fullPath, serialized)
}
const upsertItem = (
value: any,
keys: (string | number)[],
sep: string = '/'
) => {
const currentValue = getItem(keys, sep)
if (currentValue === undefined) {
setItem(value, keys, sep)
return value
}
return currentValue
}
const deleteItem = (keys: (string | number)[], sep: string = '/') => {
const fullPath = keys.join(sep)
localStorage.removeItem(fullPath)
}
const getItem = (keys: (string | number)[], sep: string = '/') => {
const fullPath = keys.join(sep)
const serialized = localStorage.getItem(fullPath)
if (serialized == null) return
return JSON.parse(serialized)
}
function createColumn<T>(value: T): Column<T> {
return {defaultValue: value}
}
export function createDB<T extends {[column: string]: any}>(
name: string,
schema: {
columns: {[K in keyof T]: T[K]}
}
): DB<T> {
const normalizedColumns = {} as {
[K in keyof T]: Column<T[K]>
}
for (const key in schema.columns) {
normalizedColumns[key] = createColumn(schema.columns[key])
}
const start = createEffect<void, T>({
handler() {
const result: T = {} as any
for (const column in normalizedColumns) {
result[column] = upsertItem(
normalizedColumns[column].defaultValue,
getDBPath(column, name)
)
}
return result
}
})
const removeDB = createEffect({
handler() {
for (const column in normalizedColumns) {
deleteItem(getDBPath(column, name))
}
}
})
const removeColumn = createEffect<keyof T, void>({
handler(column: any) {
deleteItem(getDBPath(column, name))
}
})
const columns = {} as {
[K in keyof T]: Store<T[K]>
}
const updates = {} as {
[K in keyof T]: Event<T[K]>
}
for (const column in normalizedColumns) {
const fullPathSeq = getDBPath(column, name)
const fullPath = fullPathSeq.join('/')
const update = createEvent<T[typeof column]>()
//@ts-ignore
update.graphite.seq.push(
step.compute({
fn: upd => (upd === undefined ? null : upd)
}),
//@ts-ignore
step.barrier({
priority: 'sampler'
})
)
updates[column] = update
columns[column] = createStore(normalizedColumns[column].defaultValue)
columns[column]
.on(start.done, (_, {result}) => result[column])
.on(update, (_, upd) => (upd === undefined ? null : upd))
.on(onSerializedValue, (_, {event, value, status}) => {
if (event.key !== fullPath) return
if (status === 'delete') return null
return value
})
forward({
from: update.map(value => ({
db: name,
column,
value
})),
to: updateDB
})
}
const error = createStore(null)
const state: Store<T> = combine(columns) as any
const status = createStore<DBStatus>('stop')
const onDBUpdate = updateDB.finally.filter({
fn: ({params: {db}}) => db === name
})
const onError = onDBUpdate.filterMap(result => {
if (result.status === 'fail') return result
})
error
.on(start.fail, (_, {error}) => error)
.on(onError, (error, result) => {
if (error !== null) return
return result.error
})
status.on(start.fail, () => 'error')
status.on(start.done, () => 'idle')
return {
name,
columns,
updates,
status,
state,
error,
onError,
start,
remove: {
db: removeDB,
column: removeColumn
}
}
}
function getDBPath(columnName: string, dbName: string) {
return ['db', dbName, columnName]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment