Skip to content

Commit

Permalink
feat: watcher
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Mar 11, 2021
1 parent 9059e96 commit ebcf1f1
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 130 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
- Native aware value serialization and deserialization
- Restore initial state (hydration)
- State snapshot
- Abstract watcher

WIP:

- State watching
- Key expiration
- Storage server

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"release": "yarn test && standard-version && git push --follow-tags && npm publish",
"test": "yarn lint && jest"
},
"dependencies": {
"chokidar": "^3.5.1"
},
"devDependencies": {
"@nuxtjs/eslint-config-typescript": "latest",
"@types/jest": "latest",
Expand Down
44 changes: 41 additions & 3 deletions src/drivers/fs.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { existsSync } from 'fs'
import { resolve } from 'path'
import { resolve, relative, join } from 'path'
import { FSWatcher, WatchOptions, watch } from 'chokidar'
import type { DriverFactory } from '../types'
import { readFile, writeFile, readdirRecursive, rmRecursive, unlink } from './utils/node-fs'

export interface FSStorageOptions {
dir: string
ingore: string[]
watchOptions: WatchOptions
}

export default <DriverFactory> function (opts: FSStorageOptions) {
if (!opts.dir) {
throw new Error('dir is required')
}

const r = (key: string) => resolve(opts.dir, key.replace(/:/g, '/'))
if (!opts.ingore) {
opts.ingore = [
'node_modules'
]
}

opts.dir = resolve(opts.dir)
const r = (key: string) => join(opts.dir, key.replace(/:/g, '/'))

let _watcher: FSWatcher

return {
hasItem (key) {
Expand All @@ -33,6 +45,32 @@ export default <DriverFactory> function (opts: FSStorageOptions) {
async clear () {
await rmRecursive(r('.'))
},
dispose () {}
async dispose() {
if (_watcher) {
await _watcher.close()
}
},
watch(callback) {
if (_watcher) {
return
}
return new Promise((resolve, reject) => {
_watcher = watch(opts.dir, {
ignoreInitial: true,
ignored: opts.ingore,
...opts.watchOptions
})
.on('ready', resolve)
.on('error', reject)
.on('all', (eventName, path) => {
path = relative(opts.dir, path)
if (eventName === 'change' || eventName === 'add') {
callback('update', path)
} else if (eventName === 'unlink') {
callback('remove', path)
}
})
})
}
}
}
39 changes: 30 additions & 9 deletions src/drivers/localstorage.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import type { DriverFactory } from '../types'

export interface LocalStorageOptions {
window?: typeof window
localStorage?: typeof window.localStorage
}

export default <DriverFactory> function (opts?: LocalStorageOptions) {
const _localStorage = opts?.localStorage || (globalThis.localStorage as typeof window.localStorage)
if (!_localStorage) {
export default <DriverFactory>function (opts: LocalStorageOptions = {}) {
if (!opts.window) {
opts.window = typeof window !== 'undefined' ? window : undefined
}
if (!opts.localStorage) {
opts.localStorage = opts.window?.localStorage
}
if (!opts.localStorage) {
throw new Error('localStorage not available')
}

let _storageListener: (ev: StorageEvent) => void

return {
hasItem (key) {
return Object.prototype.hasOwnProperty.call(_localStorage, key)
return Object.prototype.hasOwnProperty.call(opts.localStorage!, key)
},
getItem (key) {
return _localStorage.getItem(key)
return opts.localStorage!.getItem(key)
},
setItem (key, value) {
return _localStorage.setItem(key, value)
return opts.localStorage!.setItem(key, value)
},
removeItem (key) {
return _localStorage.removeItem(key)
return opts.localStorage!.removeItem(key)
},
getKeys () {
return Object.keys(_localStorage)
return Object.keys(opts.localStorage!)
},
clear () {
_localStorage.clear()
opts.localStorage!.clear()
if (opts.window && _storageListener) {
opts.window.removeEventListener('storage', _storageListener)
}
},
watch(callback) {
if (opts.window) {
_storageListener = (ev: StorageEvent) => {
if (ev.key) {
callback(ev.newValue ? 'update' : 'remove', ev.key)
}
}
opts.window.addEventListener('storage', _storageListener)
}
}
}
}
58 changes: 48 additions & 10 deletions src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import destr from 'destr'
import type { Storage, Driver } from './types'
import type { Storage, Driver, WatchCallback } from './types'
import memory from './drivers/memory'
import { normalizeKey, normalizeBase, asyncCall, stringify } from './utils'

interface StorageCTX {
mounts: Record<string, Driver>
mountpoints: string[]
watching: boolean
watchListeners: Function[]
}

export function createStorage (): Storage {
const ctx: StorageCTX = {
mounts: { '': memory() },
mountpoints: ['']
mountpoints: [''],
watching: false,
watchListeners: []
}

const getMount = (key?: string) => {
key = normalizeKey(key)
const getMount = (key: string) => {
for (const base of ctx.mountpoints) {
if (key.startsWith(base)) {
return {
Expand All @@ -30,8 +33,7 @@ export function createStorage (): Storage {
}
}

const getMounts = (base?: string) => {
base = normalizeBase(base)
const getMounts = (base: string) => {
return ctx.mountpoints
.filter(mountpoint => base!.startsWith(mountpoint))
.map(mountpoint => ({
Expand All @@ -40,29 +42,57 @@ export function createStorage (): Storage {
}))
}

const onChange: WatchCallback = (event, key) => {
if (!ctx.watching) { return }
key = normalizeKey(key)
for (const listener of ctx.watchListeners) {
listener(event, key)
}
}

const startWatch = async () => {
if (ctx.watching) { return }
ctx.watching = true
for (const storage of Object.values(ctx.mounts)) {
if (storage.watch) {
await storage.watch(onChange)
}
}
}

const storage: Storage = {
hasItem (key) {
key = normalizeKey(key)
const { relativeKey, driver } = getMount(key)
return asyncCall(driver.hasItem, relativeKey)
},
getItem (key) {
key = normalizeKey(key)
const { relativeKey, driver } = getMount(key)
return asyncCall(driver.getItem, relativeKey).then(val => destr(val))
},
setItem (key, value) {
async setItem (key, value) {
if (value === undefined) {
return storage.removeItem(key)
}
key = normalizeKey(key)
const { relativeKey, driver } = getMount(key)
return asyncCall(driver.setItem, relativeKey, stringify(value))
await asyncCall(driver.setItem, relativeKey, stringify(value))
if (!driver.watch) {
onChange('update', key)
}
},
async setItems (base, items) {
base = normalizeBase(base)
await Promise.all(Object.entries(items).map(e => storage.setItem(base + e[0], e[1])))
},
removeItem (key) {
async removeItem (key) {
key = normalizeKey(key)
const { relativeKey, driver } = getMount(key)
return asyncCall(driver.removeItem, relativeKey)
await asyncCall(driver.removeItem, relativeKey)
if (!driver.watch) {
onChange('remove', key)
}
},
async getKeys (base) {
base = normalizeBase(base)
Expand All @@ -74,6 +104,7 @@ export function createStorage (): Storage {
return base ? keys.filter(key => key.startsWith(base!)) : keys
},
async clear (base) {
base = normalizeBase(base)
await Promise.all(getMounts(base).map(m => asyncCall(m.driver.clear)))
},
async dispose () {
Expand All @@ -93,6 +124,9 @@ export function createStorage (): Storage {
delete ctx.mounts[base]
}
ctx.mounts[base] = driver
if (ctx.watching && driver.watch) {
driver.watch(onChange)
}
if (initialState) {
await storage.setItems(base, initialState)
}
Expand All @@ -107,6 +141,10 @@ export function createStorage (): Storage {
}
ctx.mountpoints = ctx.mountpoints.filter(key => key !== base)
delete ctx.mounts[base]
},
async watch (callback) {
await startWatch()
ctx.watchListeners.push(callback)
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export type StorageValue = null | string | String | number | Number | boolean | Boolean | object

export type WatchEvent = 'update' | 'remove'
export type WatchCallback = (event: WatchEvent, key: string) => any

export interface Driver {
hasItem: (key: string) => boolean | Promise<boolean>
getItem: (key: string) => string | Promise<string>
Expand All @@ -8,6 +11,7 @@ export interface Driver {
getKeys: () => string[] | Promise<string[]>
clear: () => void | Promise<void>
dispose?: () => void | Promise<void>
watch?: (callback: WatchCallback) => void | Promise<void>
}

export type DriverFactory<OptsT = any> = (opts?: OptsT) => Driver
Expand All @@ -20,7 +24,8 @@ export interface Storage {
removeItem: (key: string) => Promise<void>
getKeys: (base?: string) => Promise<string[]>
clear: (base?: string) => Promise<void>
mount: (base: string, driver: Driver, initialState?: Record<string, string>) => Promise<void>
mount: (base: string, driver: Driver, initialState?: Record<string, StorageValue>) => Promise<void>
unmount: (base: string, dispose?: boolean) => Promise<void>
dispose: () => Promise<void>
watch: (callback: WatchCallback) => Promise<void>
}
Loading

0 comments on commit ebcf1f1

Please sign in to comment.