Skip to content

Commit

Permalink
Bulk operations (#58)
Browse files Browse the repository at this point in the history
* Bulk operations

* Upstream merges and more tests
  • Loading branch information
sleekslush committed Aug 30, 2024
1 parent bcd968b commit cc7eb27
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 62 deletions.
112 changes: 70 additions & 42 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,26 @@ const mockStorage = {
[key]: this.data[key]
}
},
getMany(keys?: string[]) {
if (!keys) {
return { ...this.data }
}
return keys.reduce((acc, key) => {
acc[key] = this.data[key]
return acc
}, {})
},
set(key = "", value = "") {
this.data[key] = value
},
remove(key: string) {
delete this.data[key]
},
removeMany(keys: string[]) {
keys.forEach((key) => {
delete this.data[key]
})
},
clear() {
this.data = {}
}
Expand Down Expand Up @@ -69,8 +83,8 @@ export const createStorageMock = (): {
sync: {
// Needed because react hook tries to directly read the value
//@ts-ignore
get: mockOutput.getTriggers.mockImplementation((key: any) =>
mockStorage.get(key)
get: mockOutput.getTriggers.mockImplementation((keys: any) =>
mockStorage.getMany(keys)
),
//@ts-ignore
set: mockOutput.setTriggers.mockImplementation(
Expand All @@ -92,10 +106,10 @@ export const createStorageMock = (): {
}
),
//@ts-ignore
remove: mockOutput.removeTriggers.mockImplementation((key: string) => {
mockStorage.remove(key)
remove: mockOutput.removeTriggers.mockImplementation((keys: string[]) => {
mockStorage.removeMany(keys)

onChangedCallback &&
onChangedCallback && keys.forEach((key) =>
onChangedCallback(
{
[key]: {
Expand All @@ -105,6 +119,7 @@ export const createStorageMock = (): {
},
"sync"
)
)
})
}
}
Expand Down Expand Up @@ -211,7 +226,7 @@ describe("react hook", () => {
await act(async () => {
rerender({ key: key2 })
})
expect(getTriggers).toHaveBeenCalledWith(key2)
expect(getTriggers).toHaveBeenCalledWith([key2])
await waitFor(() => expect(result.current[0]).toBe(initValue))

// set new key to new value
Expand All @@ -231,36 +246,6 @@ describe("react hook", () => {

unmount()
})

test('isLoading is true until value is fetched', async () => {
const { getTriggers } = createStorageMock()

const key = 'key'
const value = 'hello'

const { result, unmount } = renderHook(() => useStorage(key))

expect(result.current[2].isLoading).toBe(true)

unmount()
})

test('isLoading is false after value is fetched', async () => {
const { getTriggers } = createStorageMock()

const key = 'key'
const value = 'hello'

const { result, unmount } = renderHook(() => useStorage(key))

await act(async () => {
await result.current[1](value)
})

expect(result.current[2].isLoading).toBe(false)

unmount()
})
})

describe("watch/unwatch", () => {
Expand Down Expand Up @@ -360,6 +345,20 @@ describe("Storage - Basic CRUD operations with namespace", () => {
})
})

test("setMany operation", async () => {
const testData = {
"key1": "value1",
"key2": "value2"
}

await storage.setMany(testData)

expect(storageMock.setTriggers).toHaveBeenCalledWith({
[`${namespace}key1`]: JSON.stringify("value1"),
[`${namespace}key2`]: JSON.stringify("value2")
})
})

// Test get operation with namespace
test("get operation", async () => {
// Test data
Expand All @@ -374,13 +373,30 @@ describe("Storage - Basic CRUD operations with namespace", () => {

// Check if storageMock.getTriggers is called with the correct parameter
expect(storageMock.getTriggers).toHaveBeenCalledWith(
`${namespace}${testKey}`
[`${namespace}${testKey}`]
)

// Check if the returned value is correct
expect(getValue).toEqual(testValue)
})

test("getMany operation", async () => {
const testData = {
"key1": "value1",
"key2": "value2"
}

await storage.setMany(testData)

const getValue = await storage.getMany(Object.keys(testData))

expect(storageMock.getTriggers).toHaveBeenCalledWith(
[`${namespace}key1`, `${namespace}key2`]
)

expect(getValue).toMatchObject(testData)
})

// Test getAll operation with namespace
test("getAll operation", async () => {
// Test data
Expand Down Expand Up @@ -415,7 +431,22 @@ describe("Storage - Basic CRUD operations with namespace", () => {

// Check if storageMock.removeListener is called with the correct parameter
expect(storageMock.removeTriggers).toHaveBeenCalledWith(
`${namespace}${testKey}`
[`${namespace}${testKey}`]
)
})

test("removeMany operation", async () => {
const testData = {
"key1": "value1",
"key2": "value2"
}

await storage.setMany(testData)

await storage.removeMany(Object.keys(testData))

expect(storageMock.removeTriggers).toHaveBeenCalledWith(
[`${namespace}key1`, `${namespace}key2`]
)
})

Expand All @@ -435,10 +466,7 @@ describe("Storage - Basic CRUD operations with namespace", () => {
await storage.removeAll()

expect(storageMock.removeTriggers).toHaveBeenCalledWith(
`${namespace}${testKey1}`
)
expect(storageMock.removeTriggers).toHaveBeenCalledWith(
`${namespace}${testKey2}`
[`${namespace}${testKey1}`, `${namespace}${testKey2}`]
)
})
})
90 changes: 70 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,29 +197,36 @@ export abstract class BaseStorage {
protected rawGet = async (
key: string
): Promise<string | null | undefined> => {
if (this.hasExtensionApi) {
const dataMap = await this.#primaryClient.get(key)
const results = await this.rawGetMany([key])
return results[key]
}

return dataMap[key]
protected rawGetMany = async (
keys: string[]
): Promise<Record<string, string | null | undefined>> => {
if (this.hasExtensionApi) {
return await this.#primaryClient.get(keys)
}

// If chrome storage is not available, use localStorage
// TODO: TRY asking for storage permission and retry?
if (this.isCopied(key)) {
return this.#secondaryClient?.getItem(key)
}
return keys.filter(this.isCopied).reduce((dataMap, copiedKey) => {
dataMap[copiedKey] = this.#secondaryClient?.getItem(copiedKey)
return dataMap
}, {})
}

return null
protected rawSet = async (key: string, value: string): Promise<null> => {
return await this.rawSetMany({ [key]: value })
}

protected rawSet = async (key: string, value: string) => {
// If not a secret, we set it in localstorage as well
if (this.isCopied(key)) {
this.#secondaryClient?.setItem(key, value)
protected rawSetMany = async (items: Record<string, string>): Promise<null> => {
if (this.#secondaryClient) {
Object.entries(items)
.filter(([key]) => this.isCopied(key))
.forEach(([key, value]) => this.#secondaryClient.setItem(key, value))
}

if (this.hasExtensionApi) {
await this.#primaryClient.set({ [key]: value })
await this.#primaryClient.set(items)
}

return null
Expand All @@ -237,20 +244,23 @@ export abstract class BaseStorage {
}

protected rawRemove = async (key: string) => {
if (this.isCopied(key)) {
this.#secondaryClient?.removeItem(key)
await this.rawRemoveMany([key])
}

protected rawRemoveMany = async (keys: string[]) => {
if (this.#secondaryClient) {
keys.filter(this.isCopied).forEach((key) => this.#secondaryClient.removeItem(key))
}

if (this.hasExtensionApi) {
await this.#primaryClient.remove(key)
await this.#primaryClient.remove(keys)
}
}

removeAll = async () => {
const allData = await this.getAll()
const keyList = Object.keys(allData)

await Promise.all(keyList.map(this.remove))
await this.removeMany(keyList)
}

watch = (callbackMap: StorageCallbackMap) => {
Expand Down Expand Up @@ -344,15 +354,18 @@ export abstract class BaseStorage {
* Get value from either local storage or chrome storage.
*/
abstract get: <T = string>(key: string) => Promise<T | undefined>
abstract getMany: <T = any>(keys: string[]) => Promise<Record<string, T | undefined>>

/**
* Set the value. If it is a secret, it will only be set in extension storage.
* Returns a warning if storage capacity is almost full.
* Throws error if the new item will make storage full
*/
abstract set: (key: string, rawValue: any) => Promise<null>
abstract setMany: (items: Record<string, any>) => Promise<null>

abstract remove: (key: string) => Promise<void>
abstract removeMany: (keys: string[]) => Promise<void>

/**
* Parse the value into its original form from storage raw value.
Expand All @@ -366,19 +379,31 @@ export abstract class BaseStorage {
return this.get<T>(key)
}

async getItems<T = string>(keys: string[]) {
return await this.getMany<T>(keys)
}

/**
* Alias for set, but returns void instead
*/
async setItem(key: string, rawValue: any) {
await this.set(key, rawValue)
}

async setItems(items: Record<string, any>) {
await await this.setMany(items)
}

/**
* Alias for remove
*/
async removeItem(key: string) {
return this.remove(key)
}

async removeItems(keys: string[]) {
return await this.removeMany(keys)
}
}

export type StorageOptions = ConstructorParameters<typeof BaseStorage>[0]
Expand All @@ -393,22 +418,47 @@ export class Storage extends BaseStorage {
return this.parseValue<T>(rawValue)
}

getMany = async <T = any>(keys: string[]) => {
const nsKeys = keys.map(this.getNamespacedKey)
const rawValues = await this.rawGetMany(nsKeys)
const parsedValues = await Promise.all(
Object.values(rawValues).map(this.parseValue<T>)
)
return Object.keys(rawValues).reduce((results, key, i) => {
results[this.getUnnamespacedKey(key)] = parsedValues[i]
return results
}, {} as Record<string, T | undefined>)
}

set = async (key: string, rawValue: any) => {
const nsKey = this.getNamespacedKey(key)
const value = this.serde.serializer(rawValue)
return this.rawSet(nsKey, value)
}

setMany = async (items: Record<string, any>) => {
const nsItems = Object.entries(items).reduce((nsItems, [key, value]) => {
nsItems[this.getNamespacedKey(key)] = this.serde.serializer(value)
return nsItems
}, {});
return await this.rawSetMany(nsItems)
}

remove = async (key: string) => {
const nsKey = this.getNamespacedKey(key)
return this.rawRemove(nsKey)
}

removeMany = async (keys: string[]) => {
const nsKeys = keys.map(this.getNamespacedKey)
return await this.rawRemoveMany(nsKeys)
}

setNamespace = (namespace: string) => {
this.keyNamespace = namespace
}

protected parseValue = async <T>(rawValue: any) => {
protected parseValue = async <T>(rawValue: any): Promise<T | undefined> => {
try {
if (rawValue !== undefined) {
return this.serde.deserializer<T>(rawValue)
Expand Down
Loading

0 comments on commit cc7eb27

Please sign in to comment.