Skip to content

Commit

Permalink
Merge pull request #1952 from dusk-network/feature-1946
Browse files Browse the repository at this point in the history
explorer: Save market data in local storage
  • Loading branch information
ascartabelli authored Jul 9, 2024
2 parents 2133332 + 34e9489 commit a2ea6cd
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 7 deletions.
80 changes: 80 additions & 0 deletions explorer/src/lib/dusk/storage/__tests__/createStorage.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { createStorage } from "..";

describe("createStorage", () => {
const serializer = vi.fn(JSON.stringify);
const deserializer = vi.fn(JSON.parse);

afterEach(() => {
serializer.mockClear();
deserializer.mockClear();
});

for (const type of /** @type {StorageType[]} */ (["local", "session"])) {
const storage = createStorage(type, serializer, deserializer);
const systemStorage = globalThis[`${type}Storage`];
const valueA = { a: 1, b: 2 };
const serializedA = JSON.stringify(valueA);
const valueB = ["a", "b", "c", "d"];
const serializedB = JSON.stringify(valueB);

beforeEach(() => {
systemStorage.setItem("some-key", serializedA);
});

it("should expose a method to clear the created storage", async () => {
await expect(storage.clear()).resolves.toBe(undefined);

expect(systemStorage.length).toBe(0);
});

it("should expose a method to retrieve a value from the created storage", async () => {
await expect(storage.getItem("some-key")).resolves.toStrictEqual(valueA);

expect(deserializer).toHaveBeenCalledTimes(1);
expect(deserializer).toHaveBeenCalledWith(serializedA);

await expect(storage.getItem("some-other-key")).resolves.toBe(null);

expect(deserializer).toHaveBeenCalledTimes(1);
});

it("should expose a method to remove a value from the created storage", async () => {
await expect(storage.removeItem("some-key")).resolves.toBe(undefined);

expect(systemStorage.getItem("some-key")).toBe(null);
});

it("should expose a method to set a value in the selected storage", async () => {
await expect(storage.setItem("some-key", valueB)).resolves.toBe(
undefined
);

expect(serializer).toHaveBeenCalledTimes(1);
expect(serializer).toHaveBeenCalledWith(valueB);
expect(systemStorage.getItem("some-key")).toBe(serializedB);
});

it("should return a rejected promise if any of the underlying storage method fails", async () => {
/** @type {Array<keyof DuskStorage & keyof Storage>} */
const methods = ["clear", "getItem", "removeItem", "setItem"];
const error = new Error("some error message");

for (const method of methods) {
const methodSpy = vi
.spyOn(Storage.prototype, method)
.mockImplementation(() => {
throw error;
});

// we don't care for correct parameters here
await expect(
storage[method]("some-key", "some value")
).rejects.toStrictEqual(error);

methodSpy.mockRestore();
}
});
}
});
31 changes: 31 additions & 0 deletions explorer/src/lib/dusk/storage/createStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @param {StorageType} type
* @param {StorageSerializer} serializer
* @param {StorageDeserializer} deserializer
* @returns {DuskStorage}
*/
function createStorage(type, serializer, deserializer) {
const storage = type === "local" ? localStorage : sessionStorage;

return {
async clear() {
return storage.clear();
},

async getItem(key) {
const value = storage.getItem(key);

return value === null ? null : deserializer(value);
},

async removeItem(key) {
return storage.removeItem(key);
},

async setItem(key, value) {
return storage.setItem(key, serializer(value));
},
};
}

export default createStorage;
1 change: 1 addition & 0 deletions explorer/src/lib/dusk/storage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as createStorage } from "./createStorage";
12 changes: 12 additions & 0 deletions explorer/src/lib/dusk/storage/storage.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type StorageType = "local" | "session";

type StorageSerializer = (value: any) => string;

type StorageDeserializer = (value: string) => any;

type DuskStorage = {
clear: () => Promise<void>;
getItem: (key: string) => Promise<any>;
removeItem: (key: string) => Promise<void>;
setItem: (key: string, value: any) => Promise<void>;
};
57 changes: 57 additions & 0 deletions explorer/src/lib/services/__tests__/marketDataStorage.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent } from "@testing-library/svelte";

import { marketDataStorage } from "..";

describe("marketDataStorage", () => {
const marketData = { data: { a: 1 }, lastUpdate: new Date() };

beforeEach(() => {
localStorage.setItem("market-data", JSON.stringify(marketData));
});

it("should expose a method to clear the market data storage", async () => {
localStorage.setItem("some-key", "some value");

await expect(marketDataStorage.clear()).resolves.toBe(undefined);

expect(localStorage.getItem("market-data")).toBeNull();
expect(localStorage.getItem("some-key")).toBe("some value");
});

it("should expose a method to retrieve market data from the storage", async () => {
await expect(marketDataStorage.get()).resolves.toStrictEqual(marketData);
});

it("should expose a method to set the data in the storage", async () => {
const newData = { data: { b: 2 }, lastUpdate: new Date() };

// @ts-expect-error
await expect(marketDataStorage.set(newData)).resolves.toBe(undefined);
await expect(marketDataStorage.get()).resolves.toStrictEqual(newData);

expect(localStorage.getItem("market-data")).toBe(JSON.stringify(newData));
});

it("should expose a method that allows to set a listener for storage events and returns a function to remove the listener", async () => {
const eventA = new StorageEvent("storage", { key: "market-data" });
const eventB = new StorageEvent("storage", { key: "some-other-key" });
const listener = vi.fn();
const removeListener = marketDataStorage.onChange(listener);

await fireEvent(window, eventA);

expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(eventA);

await fireEvent(window, eventB);

expect(listener).toHaveBeenCalledTimes(1);

removeListener();

await fireEvent(window, eventA);

expect(listener).toHaveBeenCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions explorer/src/lib/services/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as duskAPI } from "./duskAPI";
export { default as marketDataStorage } from "./marketDataStorage";
46 changes: 46 additions & 0 deletions explorer/src/lib/services/marketDataStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createStorage } from "$lib/dusk/storage";

/**
* @param {string} key
* @param {any} value
*/
const reviver = (key, value) =>
key === "lastUpdate" ? new Date(value) : value;
const storage = createStorage("local", JSON.stringify, (value) =>
JSON.parse(value, reviver)
);
const key = "market-data";

export default {
clear() {
return storage.removeItem(key);
},

/** @returns {Promise<MarketData>} */
get() {
return storage.getItem(key);
},

/**
*
* @param {(evt: StorageEvent) => void} listener
* @returns {(() => void)} The function to remove the listener.
*/
onChange(listener) {
/** @param {StorageEvent} evt */
const handleStorageChange = (evt) => {
if (evt.key === key) {
listener(evt);
}
};

window.addEventListener("storage", handleStorageChange);

return () => window.removeEventListener("storage", handleStorageChange);
},

/** @param {MarketDataStorage} value */
set(value) {
return storage.setItem(key, value);
},
};
4 changes: 4 additions & 0 deletions explorer/src/lib/services/services.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type MarketDataStorage = {
data: MarketData;
lastUpdate: Date;
};
Loading

0 comments on commit a2ea6cd

Please sign in to comment.