Skip to content

Commit

Permalink
feat: overlay driver
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed May 2, 2022
1 parent 9985cda commit 588881e
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 0 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,32 @@ const storage = createStorage({
})
```

### `overlay` (universal)

This is a special driver that creates a multi-layer overlay driver.

All write operations happen on the top level layer while values are read from all layers.

When removing a key, a special value `__OVERLAY_REMOVED__` will be set on the top level layer internally.

In the example below, we create an in-memory overlay on top of fs. No changes will be actually written to the disk.

```js
import { createStorage } from 'unstorage'
import overlay from 'unstorage/drivers/memory'
import memory from 'unstorage/drivers/memory'
import fs from 'unstorage/drivers/fs'

const storage = createStorage({
driver: overlay({
layers: [
memory(),
fs({ base: './data' })
]
})
})
```

### `http` (universal)

Use a remote HTTP/HTTPS endpoint as data storage. Supports built-in [http server](#storage-server) methods.
Expand Down
69 changes: 69 additions & 0 deletions src/drivers/overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { defineDriver } from './utils'
import type { Driver, StorageValue } from '../types'
import { normalizeKey } from './utils'

export interface OverlayStorageOptions {
layers: Driver[]
}

const OVERLAY_REMOVED = '__OVERLAY_REMOVED__'

export default defineDriver((options: OverlayStorageOptions) => {
return {
async hasItem (key) {
for (const layer of options.layers) {
if (await layer.hasItem(key)) {
if (layer === options.layers[0]) {
if (await options.layers[0]?.getItem(key) === OVERLAY_REMOVED) {
return false
}
}
return true
}
}
return false
},
async getItem (key) {
for (const layer of options.layers) {
const value = await layer.getItem(key)
if (value === OVERLAY_REMOVED) {
return null
}
if (value !== null) {
return value
}
}
return null
},
// TODO: Support native meta
// async getMeta (key) {},
async setItem(key, value) {
await options.layers[0]?.setItem(key, value)
},
async removeItem (key) {
await options.layers[0]?.setItem(key, OVERLAY_REMOVED)
},
async getKeys(base) {
const allKeys = await Promise.all(options.layers.map(async layer => {
const keys = await layer.getKeys(base)
return keys.map(key => normalizeKey(key))
}))
const uniqueKeys = Array.from(new Set(allKeys.flat()))
const existingKeys = await Promise.all(uniqueKeys.map(async key => {
if (await options.layers[0]?.getItem(key) === OVERLAY_REMOVED) {
return false
}
return key
}))
return existingKeys.filter(Boolean) as string[]
},
async dispose() {
// TODO: Graceful error handling
await Promise.all(options.layers.map(async layer => {
if (layer.dispose) {
await layer.dispose()
}
}))
}
}
})
5 changes: 5 additions & 0 deletions src/drivers/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ export function isPrimitive (arg: any) {
export function stringify (arg: any) {
return isPrimitive(arg) ? (arg + '') : JSON.stringify(arg)
}

export function normalizeKey (key: string | undefined): string {
if (!key) { return '' }
return key.replace(/[/\\]/g, ':').replace(/^:|:$/g, '')
}
13 changes: 13 additions & 0 deletions test/drivers/overlay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe } from 'vitest'
import driver from '../../src/drivers/overlay'
import memory from '../../src/drivers/memory'
import { testDriver } from './utils'

describe('drivers: overlay', () => {
const [s1, s2] = [memory(), memory()]
testDriver({
driver: driver({
layers: [s1, s2]
})
})
})

0 comments on commit 588881e

Please sign in to comment.