Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storage): support storage prefix #8

Merged
merged 1 commit into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-pens-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@webx-kit/storage": patch
---

feat(storage): support storage prefix
4 changes: 3 additions & 1 deletion packages/storage/demo/shared/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { atomWithStorage } from 'jotai/utils';
import { createStorage } from '@/index';

const chromeStorage = createStorage();
const chromeStorage = createStorage({
prefix: 'jotai:',
});

export const apiKeyAtom = atomWithStorage<string>('apiKey', 'DEFAULT', chromeStorage);
2 changes: 1 addition & 1 deletion packages/storage/demo/shared/unstorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createStorage } from 'unstorage';
import { createDriver } from '@/unstorage';

export const storage = createStorage({
driver: createDriver(),
driver: createDriver({ prefix: 'unstorage:' }),
});

export function useUnstorage(key: string, defaultValue?: any) {
Expand Down
4 changes: 4 additions & 0 deletions packages/storage/e2e/jotai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ test('Single page', async ({ getURL, page }) => {

await page.reload();
await expect(page.getByTestId('apiKey')).toHaveText('Changed');

await expect(page.evaluate(() => chrome.storage.local.get())).resolves.toEqual({
'jotai:apiKey': 'Changed',
});
});

test('Cross pages', async ({ getURL, context }) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/storage/e2e/unstorage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ test('Single page', async ({ getURL, page }) => {

await page.reload();
await expect(page.getByTestId('apiKey')).toHaveText('Changed');

await expect(page.evaluate(() => chrome.storage.local.get())).resolves.toEqual({
'unstorage:apiKey': 'Changed',
});
});

test('Cross pages', async ({ getURL, context }) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"test": "playwright test",
"lint:type": "tsc --noEmit"
},
"dependencies": {
"type-fest": "^4.10.0"
},
"devDependencies": {
"@modern-js/app-tools": "^2.46.1",
"@playwright/test": "^1.41.0",
Expand Down
75 changes: 54 additions & 21 deletions packages/storage/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,68 @@
import type { StringKeyOf } from 'type-fest';

export interface ChromeStorage<T extends Record<string, any> = Record<string, any>> {
setItem: <K extends keyof T>(key: K, value: T[K]) => Promise<void>;
getItem: <K extends keyof T>(key: K, defaultValue?: T[K]) => Promise<T[K] | null>;
setItem: <K extends StringKeyOf<T>>(key: K, value: T[K]) => Promise<void>;
getItem: <K extends StringKeyOf<T>>(key: K, defaultValue?: T[K]) => Promise<T[K] | null>;
hasItem: (key: string) => Promise<boolean>;
getKeys: () => Promise<string[]>;
removeItem: (key: keyof T | (keyof T)[]) => Promise<void>;
removeItem: (key: StringKeyOf<T> | StringKeyOf<T>[]) => Promise<void>;
clear: () => Promise<void>;
subscribe: <K extends keyof T>(key: K, callback: (value: T[K] | null) => void) => VoidFunction;
subscribe: <K extends StringKeyOf<T>>(key: K, callback: (value: T[K] | null) => void) => VoidFunction;
watch: (callback: (event: 'update' | 'remove', key: string) => void) => VoidFunction;
}

export interface CreateStorageOptions {
/**
* @default "local"
*/
area?: chrome.storage.AreaName;
prefix?: string;
}

export function createStorage<T extends Record<string, any> = Record<string, any>>(
area: chrome.storage.AreaName = 'local'
options?: CreateStorageOptions
): ChromeStorage<T> {
const { area = 'local', prefix } = options || {};
const storage = chrome.storage[area];
const [addPrefix, removePrefix] = createPrefixHelpers(prefix);

async function setItem<K extends keyof T>(key: K, value: T[K]): Promise<void> {
return storage.set({ [key]: value });
async function setItem<K extends StringKeyOf<T>>(key: K, value: T[K]): Promise<void> {
return storage.set({ [addPrefix(key)]: value });
}

async function getItem<K extends keyof T>(key: K, defaultValue?: T[K]): Promise<T[K] | null> {
const result = await storage.get(key as string);
return key in result ? result[key as string] : arguments.length === 2 ? defaultValue : null;
async function getItem<K extends StringKeyOf<T>>(key: K, defaultValue?: T[K]): Promise<T[K] | null> {
const prefixedKey = addPrefix(key);
const result = await storage.get(prefixedKey);
return prefixedKey in result ? result[prefixedKey] : arguments.length === 2 ? defaultValue : null;
}

async function hasItem(key: string): Promise<boolean> {
const result = await storage.get(key as string);
return key in result;
const prefixedKey = addPrefix(key);
const result = await storage.get(prefixedKey);
return prefixedKey in result;
}

async function getKeys(): Promise<string[]> {
const result = await storage.get();
return Object.keys(result);
const prefixedKeys = Object.keys(result);
if (!prefix) return prefixedKeys;
return prefixedKeys.filter((key) => key.startsWith(prefix)).map(removePrefix);
}

async function removeItem(key: keyof T | (keyof T)[]): Promise<void> {
return storage.remove(key as string | string[]);
async function removeItem(key: StringKeyOf<T> | StringKeyOf<T>[]): Promise<void> {
return storage.remove(Array.isArray(key) ? key.map(addPrefix) : addPrefix(key));
}

async function clear(): Promise<void> {
return storage.clear();
if (!prefix) return storage.clear();
const keys = await getKeys();
return storage.remove(keys.map(addPrefix));
}

type OnChangedListener = Parameters<(typeof storage)['onChanged']['addListener']>[0];

let onChangedActive = false;
const onChangedCallbacks = new Map<keyof T, Set<(value: any) => void>>();
const onChangedCallbacks = new Map<string, Set<(value: any) => void>>();
const onChangedListener: OnChangedListener = (changes) => {
for (const [key, value] of Object.entries(changes)) {
const changedCallback = onChangedCallbacks.get(key);
Expand Down Expand Up @@ -74,18 +92,19 @@ export function createStorage<T extends Record<string, any> = Record<string, any
}
}

function subscribe<K extends keyof T>(key: K, callback: (value: T[K] | null) => void): VoidFunction {
addCallback(key as string, callback);
function subscribe<K extends StringKeyOf<T>>(key: K, callback: (value: T[K] | null) => void): VoidFunction {
const prefixedKey = addPrefix(key);
addCallback(prefixedKey, callback);
return () => {
removeCallback(key as string, callback);
removeCallback(prefixedKey, callback);
};
}

function watch(callback: (type: 'update' | 'remove', key: string) => void): VoidFunction {
const listener: OnChangedListener = (changes) => {
for (const [key, value] of Object.entries(changes)) {
const type = 'newValue' in value ? 'update' : 'remove';
callback(type, key);
callback(type, removePrefix(key));
}
};
storage.onChanged.addListener(listener);
Expand All @@ -105,3 +124,17 @@ export function createStorage<T extends Record<string, any> = Record<string, any
watch,
};
}

function identity<T>(value: T): T {
return value;
}

type StringTransformer = (key: string) => string;

function createPrefixHelpers(prefix?: string): [StringTransformer, StringTransformer] {
if (!prefix) return [identity, identity];
const len = prefix.length;
const add: StringTransformer = (key) => prefix + key;
const remove: StringTransformer = (key) => (key.startsWith(prefix) ? key.slice(len) : key);
return [add, remove];
}
6 changes: 3 additions & 3 deletions packages/storage/src/unstorage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChromeStorage, createStorage } from './index';
import { ChromeStorage, CreateStorageOptions, createStorage } from './index';

export function createDriver(): ChromeStorage {
const storage = createStorage();
export function createDriver(options?: CreateStorageOptions): ChromeStorage {
const storage = createStorage(options);
return {
...storage,
getItem: (key: string) => storage.getItem(key),
Expand Down
27 changes: 18 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.