Skip to content

Commit

Permalink
feat(storage): compatible with unstorage
Browse files Browse the repository at this point in the history
  • Loading branch information
tmkx committed Jan 23, 2024
1 parent 37d7250 commit 32a407d
Show file tree
Hide file tree
Showing 17 changed files with 704 additions and 111 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-crabs-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@webx-kit/storage": patch
---

feat(storage): compatible with unstorage
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ jobs:
- uses: actions/cache@v3
with:
path: node_modules/.cache/turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
turbo-${{ runner.os }}-
- uses: pnpm/action-setup@v2

Expand Down
2 changes: 1 addition & 1 deletion packages/storage/demo/content-scripts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createRoot } from 'react-dom/client';
import { useAtomValue } from 'jotai/react';
import { apiKeyAtom } from '../hooks/atoms';
import { apiKeyAtom } from '../shared/atoms';

const App = () => {
const apiKey = useAtomValue(apiKeyAtom);
Expand Down
4 changes: 0 additions & 4 deletions packages/storage/demo/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ const manifest: chrome.runtime.ManifestV3 = {
service_worker: 'static/js/background.js',
type: 'module',
},
options_ui: {
page: 'options.html',
open_in_tab: true,
},
content_scripts: [
{
matches: ['<all_urls>'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createRoot } from 'react-dom/client';
import { useAtomValue } from 'jotai/react';
import { apiKeyAtom } from '../../hooks/atoms';
import { apiKeyAtom } from '../../shared/atoms';

const App = () => {
const apiKey = useAtomValue(apiKeyAtom);
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/demo/pages/jotai/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createRoot } from 'react-dom/client';
import { useAtom } from 'jotai/react';
import { apiKeyAtom } from '../../hooks/atoms';
import { apiKeyAtom } from '../../shared/atoms';

const App = () => {
const [apiKey, setAPIKey] = useAtom(apiKeyAtom);
Expand Down
13 changes: 13 additions & 0 deletions packages/storage/demo/pages/unstorage-2/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createRoot } from 'react-dom/client';
import { useUnstorage } from '../../shared/unstorage';

const App = () => {
const [apiKey] = useUnstorage('apiKey', 'DEFAULT');
return (
<div>
<div data-testid="apiKey">{apiKey}</div>
</div>
);
};

createRoot(document.getElementById('root')!).render(<App />);
21 changes: 21 additions & 0 deletions packages/storage/demo/pages/unstorage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createRoot } from 'react-dom/client';
import { useUnstorage } from '../../shared/unstorage';

const App = () => {
const [apiKey, setAPIKey] = useUnstorage('apiKey', 'DEFAULT');
return (
<div>
<div data-testid="apiKey">{apiKey}</div>
<button
data-testid="change"
onClick={() => {
setAPIKey('Changed');
}}
>
Change
</button>
</div>
);
};

createRoot(document.getElementById('root')!).render(<App />);
File renamed without changes.
29 changes: 29 additions & 0 deletions packages/storage/demo/shared/unstorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useCallback, useEffect, useState } from 'react';
import { createStorage } from 'unstorage';
import { createDriver } from '@/unstorage';

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

export function useUnstorage(key: string, defaultValue?: any) {
const [value, setValue] = useState(defaultValue);

useEffect(() => {
const refresh = () => storage.getItem(key).then((v) => setValue(v ?? defaultValue));
refresh();
const unwatch = storage.watch((_type, changedKey) => changedKey === key && refresh());
return () => void unwatch.then((unwatch) => unwatch());
}, [key]);

const handleSetValue = useCallback(
(value: any) => {
storage.setItem(key, value).then(() => {
setValue(value);
});
},
[key]
);

return [value, handleSetValue] as const;
}
12 changes: 6 additions & 6 deletions packages/storage/e2e/jotai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,24 @@ test('Single page', async ({ getURL, page }) => {

test('Cross pages', async ({ getURL, context }) => {
const jotaiPage = await context.newPage();
const optionsPage = await context.newPage();
const jotai2Page = await context.newPage();
const webPage = await context.newPage();

await jotaiPage.goto(await getURL('jotai.html'));
await optionsPage.goto(await getURL('options.html'));
await jotai2Page.goto(await getURL('jotai-2.html'));
await webPage.goto(getWebpageURL());

await expect(jotaiPage.getByTestId('apiKey')).toHaveText('DEFAULT');
await expect(optionsPage.getByTestId('apiKey')).toHaveText('DEFAULT');
await expect(jotai2Page.getByTestId('apiKey')).toHaveText('DEFAULT');
await expect(webPage.getByTestId('apiKey')).toHaveText('DEFAULT');

await jotaiPage.getByTestId('change').click();
await expect(jotaiPage.getByTestId('apiKey')).toHaveText('Changed');
await expect(optionsPage.getByTestId('apiKey')).toHaveText('Changed');
await expect(jotai2Page.getByTestId('apiKey')).toHaveText('Changed');
await expect(webPage.getByTestId('apiKey')).toHaveText('Changed');

await Promise.all([jotaiPage.reload(), optionsPage.reload(), webPage.reload()]);
await Promise.all([jotaiPage.reload(), jotai2Page.reload(), webPage.reload()]);
await expect(jotaiPage.getByTestId('apiKey')).toHaveText('Changed');
await expect(optionsPage.getByTestId('apiKey')).toHaveText('Changed');
await expect(jotai2Page.getByTestId('apiKey')).toHaveText('Changed');
await expect(webPage.getByTestId('apiKey')).toHaveText('Changed');
});
31 changes: 31 additions & 0 deletions packages/storage/e2e/unstorage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test } from './context';

test('Single page', async ({ getURL, page }) => {
await page.goto(await getURL('unstorage.html'));

await expect(page.getByTestId('apiKey')).toHaveText('DEFAULT');
await page.getByTestId('change').click();
await expect(page.getByTestId('apiKey')).toHaveText('Changed');

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

test('Cross pages', async ({ getURL, context }) => {
const jotaiPage = await context.newPage();
const jotai2Page = await context.newPage();

await jotaiPage.goto(await getURL('unstorage.html'));
await jotai2Page.goto(await getURL('unstorage-2.html'));

await expect(jotaiPage.getByTestId('apiKey')).toHaveText('DEFAULT');
await expect(jotai2Page.getByTestId('apiKey')).toHaveText('DEFAULT');

await jotaiPage.getByTestId('change').click();
await expect(jotaiPage.getByTestId('apiKey')).toHaveText('Changed');
await expect(jotai2Page.getByTestId('apiKey')).toHaveText('Changed');

await Promise.all([jotaiPage.reload(), jotai2Page.reload()]);
await expect(jotaiPage.getByTestId('apiKey')).toHaveText('Changed');
await expect(jotai2Page.getByTestId('apiKey')).toHaveText('Changed');
});
11 changes: 10 additions & 1 deletion packages/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./unstorage": {
"types": "./src/unstorage.ts",
"default": "./src/unstorage.ts"
}
},
"types": "./src/index.ts",
Expand All @@ -13,6 +17,10 @@
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./unstorage": {
"types": "./dist/unstorage.d.mts",
"default": "./dist/unstorage.mjs"
}
},
"types": "./dist/index.d.mts"
Expand Down Expand Up @@ -40,7 +48,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tsup": "^8.0.1",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"unstorage": "^1.10.1"
},
"peerDependencies": {
"@types/chrome": "^0.0.258"
Expand Down
35 changes: 33 additions & 2 deletions packages/storage/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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>;
hasItem: (key: string) => Promise<boolean>;
getKeys: () => Promise<string[]>;
removeItem: (key: keyof T | (keyof T)[]) => Promise<void>;
clear: () => Promise<void>;
subscribe: <K extends keyof T>(key: K, callback: (value: T[K] | null) => void) => VoidFunction;
watch: (callback: (event: 'update' | 'remove', key: string) => void) => VoidFunction;
}

export function createStorage<T extends Record<string, any> = Record<string, any>>(
Expand All @@ -17,7 +20,17 @@ export function createStorage<T extends Record<string, any> = Record<string, any

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 result[key as string] ?? (arguments.length === 2 ? defaultValue : null);
return key in result ? result[key as string] : arguments.length === 2 ? defaultValue : null;
}

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

async function getKeys(): Promise<string[]> {
const result = await storage.get();
return Object.keys(result);
}

async function removeItem(key: keyof T | (keyof T)[]): Promise<void> {
Expand All @@ -28,9 +41,11 @@ export function createStorage<T extends Record<string, any> = Record<string, any
return storage.clear();
}

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

let onChangedActive = false;
const onChangedCallbacks = new Map<keyof T, Set<(value: any) => void>>();
const onChangedListener: Parameters<(typeof storage)['onChanged']['addListener']>[0] = (changes) => {
const onChangedListener: OnChangedListener = (changes) => {
for (const [key, value] of Object.entries(changes)) {
const changedCallback = onChangedCallbacks.get(key);
if (!changedCallback) continue;
Expand Down Expand Up @@ -66,11 +81,27 @@ export function createStorage<T extends Record<string, any> = Record<string, any
};
}

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);
}
};
storage.onChanged.addListener(listener);
return () => {
storage.onChanged.removeListener(listener);
};
}

return {
setItem,
getItem,
hasItem,
getKeys,
removeItem,
clear,
subscribe,
watch,
};
}
9 changes: 9 additions & 0 deletions packages/storage/src/unstorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChromeStorage, createStorage } from './index';

export function createDriver(): ChromeStorage {
const storage = createStorage();
return {
...storage,
getItem: (key: string) => storage.getItem(key),
};
}
2 changes: 1 addition & 1 deletion packages/storage/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from 'tsup';

export default defineConfig({
entry: ['./src/index.ts'],
entry: ['./src/*.ts'],
outDir: './dist',
format: 'esm',
clean: true,
Expand Down
Loading

0 comments on commit 32a407d

Please sign in to comment.