Skip to content
Open
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
17 changes: 17 additions & 0 deletions docs/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ await storage.watch<number>(
},
);
await storage.getMeta<{ v: number }>('local:installDate');
await storage.watchMeta<{ v: number }>('local:installDate', (newMeta) => {
// ...
});
```

> This approach is fine for one-off storage fields or generic helpers, but [defining storage items](#defining-storage-items) is the recommended way to add type-safety.
Expand Down Expand Up @@ -117,6 +120,17 @@ await storage.setMeta('local:preference', { v: 2 });
await storage.getMeta('local:preference'); // { v: 2, lastModified: 1703690746007 }
```

To listen for metadata changes, use `watchMeta`:

```ts
const unwatch = storage.watchMeta<{ lastModified: number }>(
'local:preference',
(newMeta, oldMeta) => {
console.log('Preference metadata changed:', { newMeta, oldMeta });
},
);
```

You can remove all metadata associated with a key, or just specific properties:

```ts
Expand Down Expand Up @@ -155,6 +169,9 @@ await showChangelogOnUpdate.removeValue();
const unwatch = showChangelogOnUpdate.watch((newValue) => {
// ...
});
const unwatchMeta = showChangelogOnUpdate.watchMeta((newMeta) => {
// ...
});
```

### Versioning
Expand Down
125 changes: 125 additions & 0 deletions packages/storage/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,63 @@ describe('Storage Utils', () => {
});
});

describe('watchMeta', () => {
it('should not trigger if the changed key is different from the metadata key', async () => {
const cb = vi.fn();

storage.watchMeta(`${storageArea}:key`, cb);
await storage.setMeta(`${storageArea}:not-the-key`, { v: 2 });

expect(cb).not.toBeCalled();
});

it("should not trigger if the metadata doesn't change", async () => {
const cb = vi.fn();
const meta = { syncedAt: 123 };

await storage.setMeta(`${storageArea}:key`, meta);
storage.watchMeta(`${storageArea}:key`, cb);
await storage.setMeta(`${storageArea}:key`, meta);

expect(cb).not.toBeCalled();
});

it('should call the callback when metadata changes', async () => {
const cb = vi.fn();
const oldMeta = { syncedAt: 123 };
const newMeta = { syncedAt: 456 };

await storage.setMeta(`${storageArea}:key`, oldMeta);
storage.watchMeta(`${storageArea}:key`, cb);
await storage.setMeta(`${storageArea}:key`, newMeta);

expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith(newMeta, oldMeta);
});

it('should use an empty object when metadata is removed', async () => {
const cb = vi.fn();
const oldMeta = { syncedAt: 123 };

await storage.setMeta(`${storageArea}:key`, oldMeta);
storage.watchMeta(`${storageArea}:key`, cb);
await storage.removeMeta(`${storageArea}:key`);

expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith({}, oldMeta);
});

it('should remove the listener when calling the returned function', async () => {
const cb = vi.fn();

const unwatch = storage.watchMeta(`${storageArea}:key`, cb);
unwatch();
await storage.setMeta(`${storageArea}:key`, { syncedAt: 123 });

expect(cb).not.toBeCalled();
});
});

describe('unwatch', () => {
it('should remove all watch listeners', async () => {
const cb = vi.fn();
Expand Down Expand Up @@ -1295,6 +1352,74 @@ describe('Storage Utils', () => {
});
});

describe('watchMeta', () => {
it("should not trigger if the changed key is different from the item's metadata key", async () => {
const item = storage.defineItem(`local:key`);
const cb = vi.fn();

item.watchMeta(cb);
await storage.setMeta(`local:not-the-key`, { v: 2 });

expect(cb).not.toBeCalled();
});

it("should not trigger if the metadata doesn't change", async () => {
const item = storage.defineItem<number, { syncedAt: number }>(
`local:key`,
);
const cb = vi.fn();
const meta = { syncedAt: 123 };

await item.setMeta(meta);
item.watchMeta(cb);
await item.setMeta(meta);

expect(cb).not.toBeCalled();
});

it('should call the callback when metadata changes', async () => {
const item = storage.defineItem<number, { syncedAt: number }>(
`local:key`,
);
const cb = vi.fn();
const oldMeta = { syncedAt: 123 };
const newMeta = { syncedAt: 456 };

await item.setMeta(oldMeta);
item.watchMeta(cb);
await item.setMeta(newMeta);

expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith(newMeta, oldMeta);
});

it('should use an empty object when metadata is removed', async () => {
const item = storage.defineItem<number, { syncedAt: number }>(
`local:key`,
);
const cb = vi.fn();
const oldMeta = { syncedAt: 123 };

await item.setMeta(oldMeta);
item.watchMeta(cb);
await item.removeMeta();

expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith({}, oldMeta);
});

it('should remove the listener when calling the returned function', async () => {
const item = storage.defineItem(`local:key`);
const cb = vi.fn();

const unwatch = item.watchMeta(cb);
unwatch();
await item.setMeta({ v: 2 });

expect(cb).not.toBeCalled();
});
});

describe('unwatch', () => {
it('should remove all watch listeners', async () => {
const item = storage.defineItem(`local:key`);
Expand Down
33 changes: 32 additions & 1 deletion packages/storage/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ function createStorage(): WxtStorage {
value ?? fallback ?? null;

const getMetaValue = (properties: any) =>
typeof properties === 'object' && !Array.isArray(properties)
properties != null &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This a breaking change? We used to return null when meta was empty, but now we don't?

typeof properties === 'object' &&
!Array.isArray(properties)
? properties
: {};

Expand Down Expand Up @@ -136,6 +138,19 @@ function createStorage(): WxtStorage {
cb: WatchCallback<any>,
) => driver.watch(driverKey, cb);

const watchMeta = (
driver: WxtStorageDriver,
driverKey: string,
cb: WatchCallback<any>,
) =>
watch(driver, getMetaKey(driverKey), (newValue, oldValue) => {
const newMeta = getMetaValue(newValue);
const oldMeta = getMetaValue(oldValue);
if (dequal(newMeta, oldMeta)) return;

cb(newMeta, oldMeta);
});

return {
getItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
Expand Down Expand Up @@ -389,6 +404,11 @@ function createStorage(): WxtStorage {
return watch(driver, driverKey, cb);
},

watchMeta: (key, cb) => {
const { driver, driverKey } = resolveKey(key);
return watchMeta(driver, driverKey, cb);
},

unwatch() {
Object.values(drivers).forEach((driver) => {
driver.unwatch();
Expand Down Expand Up @@ -574,6 +594,8 @@ function createStorage(): WxtStorage {
cb(newValue ?? getFallback(), oldValue ?? getFallback()),
),

watchMeta: (cb) => watchMeta(driver, driverKey, cb),

migrate,
};
},
Expand Down Expand Up @@ -839,6 +861,12 @@ export interface WxtStorage {
/** Watch for changes to a specific key in storage. */
watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch;

/** Watch for changes to metadata for a specific key in storage. */
watchMeta<T extends Record<string, unknown>>(
key: StorageItemKey,
cb: WatchCallback<NullablePartial<T>>,
): Unwatch;

/** Remove all watch listeners. */
unwatch(): void;

Expand Down Expand Up @@ -918,6 +946,9 @@ export interface WxtStorageItem<
/** Listen for changes to the value in storage. */
watch(cb: WatchCallback<TValue>): Unwatch;

/** Listen for changes to metadata in storage. */
watchMeta(cb: WatchCallback<NullablePartial<TMetadata>>): Unwatch;

/**
* If there are migrations defined on the storage item, migrate to the latest
* version.
Expand Down