diff --git a/README.md b/README.md index 78d2cff2..144943e2 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/drivers/overlay.ts b/src/drivers/overlay.ts new file mode 100644 index 00000000..4916b52b --- /dev/null +++ b/src/drivers/overlay.ts @@ -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() + } + })) + } + } +}) diff --git a/src/drivers/utils/index.ts b/src/drivers/utils/index.ts index 4a8b9a2b..fc2f9e9f 100644 --- a/src/drivers/utils/index.ts +++ b/src/drivers/utils/index.ts @@ -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, '') +} diff --git a/test/drivers/overlay.test.ts b/test/drivers/overlay.test.ts new file mode 100644 index 00000000..fe830e0e --- /dev/null +++ b/test/drivers/overlay.test.ts @@ -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] + }) + }) +})