Full PromisifiedStorage + Simplifications

This commit is contained in:
Chi Vinh Le
2018-09-06 23:53:29 +02:00
parent 3a1b043eb7
commit e132087682
41 changed files with 642 additions and 238 deletions
@@ -24,16 +24,10 @@ export interface TalkContext {
timeagoFormatter?: Formatter;
/** Local Storage */
localStorage: Storage;
localStorage: PromisifiedStorage;
/** Session storage */
sessionStorage: Storage;
/** Session Storage over pym */
pymLocalStorage?: PromisifiedStorage;
/** Session storage over pym */
pymSessionStorage?: PromisifiedStorage;
sessionStorage: PromisifiedStorage;
/** media query values for testing purposes */
mediaQueryValues?: MediaQueryMatchers;
@@ -128,14 +128,12 @@ export default async function createContext({
registerClickFarAway,
rest: new RestClient("/api", tokenGetter),
postMessage: new PostMessageService(),
localStorage: createLocalStorage(),
sessionStorage: createSessionStorage(),
pymLocalStorage:
(pym && (inIframe && createPymStorage(pym, "localStorage"))) ||
createPromisifiedStorage(createLocalStorage("talkPym")),
pymSessionStorage:
(pym && (inIframe && createPymStorage(pym, "sessionStorage"))) ||
createPromisifiedStorage(createSessionStorage("talkPym")),
localStorage:
(pym && inIframe && createPymStorage(pym, "localStorage")) ||
createPromisifiedStorage(createLocalStorage()),
sessionStorage:
(pym && inIframe && createPymStorage(pym, "sessionStorage")) ||
createPromisifiedStorage(createSessionStorage()),
browserInfo: getBrowserInfo(),
};
@@ -16,10 +16,19 @@ it("should return length", () => {
expect(storage.length).toBe(3);
});
it("should nth value", () => {
it("should nth key", () => {
const storage = createInMemoryStorage();
storage.setItem("a", "a");
storage.setItem("b", "b");
storage.setItem("c", "c");
storage.setItem("a", "0");
storage.setItem("b", "1");
storage.setItem("c", "2");
expect(storage.key(2)).toBe("c");
});
it("accepts predefined data", () => {
const storage = createInMemoryStorage({
a: "0",
b: "1",
c: "2",
});
expect(storage.toString()).toMatchSnapshot();
});
@@ -5,18 +5,18 @@
* https://developer.mozilla.org/en-US/docs/Web/API/Storage
*/
class InMemoryStorage implements Storage {
private storage: Record<string, string>;
private data: Record<string, string>;
constructor() {
this.storage = {};
constructor(data: Record<string, string> = {}) {
this.data = data;
}
get length() {
return Object.keys(this.storage).length;
return Object.keys(this.data).length;
}
public clear() {
this.storage = {};
this.data = {};
}
public key(n: number) {
@@ -24,26 +24,26 @@ class InMemoryStorage implements Storage {
return null;
}
return this.storage[Object.keys(this.storage)[n]];
return Object.keys(this.data)[n];
}
public getItem(key: string) {
return this.storage[key] || null;
return this.data[key] || null;
}
public setItem(key: string, value: string) {
this.storage[key] = value;
this.data[key] = value;
}
public removeItem(key: string) {
delete this.storage[key];
delete this.data[key];
}
public toString() {
return JSON.stringify(this.storage);
return JSON.stringify(this.data);
}
}
export default function createInMemoryStorage() {
return new InMemoryStorage();
export default function createInMemoryStorage(data?: Record<string, string>) {
return new InMemoryStorage(data);
}
@@ -1,5 +1,5 @@
import prefixStorage from "./prefixStorage";
export default function createLocalStorage(prefix = "talk"): Storage {
export default function createLocalStorage(prefix = "talk:"): Storage {
return prefixStorage(window.localStorage, prefix);
}
@@ -1,10 +1,26 @@
import createInMemoryStorage from "./InMemoryStorage";
import createPromisifiedStorage from "./PromisifiedStorage";
it("should set and unset values", () => {
it("should set and unset values", async () => {
const storage = createPromisifiedStorage(createInMemoryStorage());
expect(storage.setItem("test", "value")).resolves.toBeUndefined();
expect(storage.getItem("test")).resolves.toBe("value");
await expect(storage.setItem("test", "value")).resolves.toBeUndefined();
await expect(storage.getItem("test")).resolves.toBe("value");
storage.removeItem("test");
expect(storage.getItem("test")).resolves.toBeUndefined();
await expect(storage.getItem("test")).resolves.toBeNull();
});
it("should return length", async () => {
const storage = createPromisifiedStorage(createInMemoryStorage());
storage.setItem("a", "value");
storage.setItem("b", "value");
storage.setItem("c", "value");
await expect(storage.length).resolves.toBe(3);
});
it("should nth value", async () => {
const storage = createPromisifiedStorage(createInMemoryStorage());
storage.setItem("a", "a");
storage.setItem("b", "b");
storage.setItem("c", "c");
await expect(storage.key(2)).resolves.toBe("c");
});
@@ -1,4 +1,12 @@
import createInMemoryStorage from "./InMemoryStorage";
export interface PromisifiedStorage {
length: Promise<number>;
clear(): Promise<void>;
key(n: number): Promise<string | null>;
/**
* value = storage[key]
*/
@@ -23,6 +31,18 @@ class BackedPromisifedStorage implements PromisifiedStorage {
this.storage = storage;
}
get length() {
return Promise.resolve(this.storage.length);
}
public clear() {
return Promise.resolve(this.storage.clear());
}
public key(n: number) {
return Promise.resolve(this.storage.key(n));
}
public getItem(key: string) {
return Promise.resolve(this.storage.getItem(key));
}
@@ -36,6 +56,8 @@ class BackedPromisifedStorage implements PromisifiedStorage {
}
}
export default function createPromisifiedStorage(storage: Storage) {
export default function createPromisifiedStorage(
storage: Storage = createInMemoryStorage()
) {
return new BackedPromisifedStorage(storage);
}
@@ -59,6 +59,49 @@ describe("PymStorage", () => {
expect(promise).resolves.toBe("value");
});
it("should get length", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.length;
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id, method, parameters } = JSON.parse(value);
expect(method).toBe("length");
expect(parameters).toEqual({});
pym.listeners["pymStorage.localStorage.response"](
JSON.stringify({ id, result: 3 })
);
expect(promise).resolves.toBe(3);
});
it("should get key", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.key(2);
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id, method, parameters } = JSON.parse(value);
expect(method).toBe("key");
expect(parameters).toEqual({ n: 2 });
pym.listeners["pymStorage.localStorage.response"](
JSON.stringify({ id, result: "myKey" })
);
expect(promise).resolves.toBe("myKey");
});
it("should clear", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.clear();
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id, method, parameters } = JSON.parse(value);
expect(method).toBe("clear");
expect(parameters).toEqual({});
pym.listeners["pymStorage.localStorage.response"](JSON.stringify({ id }));
expect(promise).resolves.toBeUndefined();
});
describe("on error", () => {
it("should reject set item", () => {
const pym = new PymStub("localStorage");
@@ -20,7 +20,7 @@ class PymStorage implements PromisifiedStorage {
/** Requests method with parameters over pym. */
private call<T>(
method: string,
parameters: { key: string; value?: string }
parameters: Record<string, any> = {}
): Promise<T> {
const id = uuid();
return new Promise((resolve, reject) => {
@@ -55,6 +55,15 @@ class PymStorage implements PromisifiedStorage {
this.listen();
}
get length() {
return this.call<number>("length");
}
public key(n: number) {
return this.call<string | null>("key", { n });
}
public clear() {
return this.call<void>("clear");
}
public setItem(key: string, value: string) {
return this.call<void>("setItem", { key, value });
}
@@ -1,5 +1,5 @@
import prefixStorage from "./prefixStorage";
export default function createSessionStorage(prefix = "talk"): Storage {
export default function createSessionStorage(prefix = "talk:"): Storage {
return prefixStorage(window.sessionStorage, prefix);
}
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`accepts predefined data 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"c\\":\\"2\\"}"`;
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should call clear 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"d\\":\\"3\\"}"`;
@@ -1,54 +1,47 @@
import sinon from "sinon";
import createInMemoryStorage from "./InMemoryStorage";
import prefixStorage from "./prefixStorage";
it("should call clear", () => {
const storage = {
clear: sinon.mock().once(),
};
it("should get nth key", () => {
const storage = createInMemoryStorage({
a: "0",
b: "1",
"talk:c": "2",
d: "3",
"talk:e": "4",
});
const prefixed = prefixStorage(storage as any, "talk");
const prefixed = prefixStorage(storage, "talk:");
expect(prefixed.key(0)).toBe("talk:c");
expect(prefixed.key(1)).toBe("talk:e");
expect(prefixed.key(2)).toBeNull();
});
it("should call clear", () => {
const storage = createInMemoryStorage({
a: "0",
b: "1",
"talk:c": "2",
d: "3",
"talk:e": "4",
});
const prefixed = prefixStorage(storage, "talk:");
prefixed.clear();
storage.clear.verify();
expect(storage.toString()).toMatchSnapshot();
});
it("should call length", () => {
const ret = 10;
const storage = {
get length() {
return ret;
},
};
const storage = createInMemoryStorage({
a: "0",
b: "1",
"talk:c": "2",
d: "3",
"talk:e": "4",
});
const prefixed = prefixStorage(storage as any, "talk");
expect(prefixed.length).toBe(ret);
});
it("should call key", () => {
const ret = "value";
const storage = {
key: sinon
.mock()
.withArgs(3)
.returns(ret),
};
const prefixed = prefixStorage(storage as any, "talk");
expect(prefixed.key(3)).toBe(ret);
(storage.key as any).verify();
});
it("should call key", () => {
const ret = "value";
const storage = {
key: sinon
.mock()
.withArgs(3)
.returns(ret),
};
const prefixed = prefixStorage(storage as any, "talk");
expect(prefixed.key(3)).toBe(ret);
(storage.key as any).verify();
const prefixed = prefixStorage(storage, "talk:");
expect(prefixed.length).toBe(2);
});
it("should prefix setItem", () => {
@@ -56,7 +49,7 @@ it("should prefix setItem", () => {
setItem: sinon.mock().withArgs("talk:key", "value"),
};
const prefixed = prefixStorage(storage as any, "talk");
const prefixed = prefixStorage(storage as any, "talk:");
prefixed.setItem("key", "value");
storage.setItem.verify();
});
@@ -66,7 +59,7 @@ it("should prefix removeItem", () => {
removeItem: sinon.mock().withArgs("talk:key"),
};
const prefixed = prefixStorage(storage as any, "talk");
const prefixed = prefixStorage(storage as any, "talk:");
prefixed.removeItem("key");
storage.removeItem.verify();
});
@@ -80,7 +73,7 @@ it("should prefix getItem", () => {
.returns(ret),
};
const prefixed = prefixStorage(storage as any, "talk");
const prefixed = prefixStorage(storage as any, "talk:");
expect(prefixed.getItem("key")).toBe(ret);
(storage.getItem as any).verify();
});
@@ -12,27 +12,50 @@ class PrefixedStorage implements Storage {
}
get length() {
return this.storage.length;
let count = 0;
for (let i = 0; i < this.storage.length; i++) {
if (this.storage.key(i)!.startsWith(this.prefix)) {
count++;
}
}
return count;
}
public clear() {
this.storage.clear();
const toBeDeleted = [];
for (let i = 0; i < this.storage.length; i++) {
const key = this.storage.key(i)!;
if (key.startsWith(this.prefix)) {
toBeDeleted.push(key);
}
}
toBeDeleted.forEach(key => this.storage.removeItem(key));
}
public key(n: number) {
return this.storage.key(n);
let count = 0;
for (let i = 0; i < this.storage.length; i++) {
const key = this.storage.key(i)!;
if (key.startsWith(this.prefix)) {
if (count === n) {
return key;
}
count++;
}
}
return null;
}
public getItem(key: string) {
return this.storage.getItem(`${this.prefix}:${key}`);
return this.storage.getItem(`${this.prefix}${key}`);
}
public setItem(key: string, value: string) {
return this.storage.setItem(`${this.prefix}:${key}`, value);
return this.storage.setItem(`${this.prefix}${key}`, value);
}
public removeItem(key: string) {
return this.storage.removeItem(`${this.prefix}:${key}`);
return this.storage.removeItem(`${this.prefix}${key}`);
}
}
@@ -33,24 +33,3 @@ it("Removes auth token from localStorage", () => {
commit(environment, { authToken: null }, context as any);
expect(context.localStorage.getItem("authToken")).toBeNull();
});
it("Sets auth token to pymLocalStorage", async () => {
const context = {
pymLocalStorage: createInMemoryStorage(),
localStorage: createInMemoryStorage(),
};
const authToken = "auth token";
commit(environment, { authToken }, context as any);
expect(source.get(LOCAL_ID)!.authToken).toEqual(authToken);
expect(await context.pymLocalStorage.getItem("authToken")).toEqual(authToken);
});
it("Removes auth token from pymLocalStorage", async () => {
const context = {
pymLocalStorage: createInMemoryStorage(),
localStorage: createInMemoryStorage(),
};
localStorage.setItem("authToken", "tmp");
commit(environment, { authToken: null }, context as any);
expect(await context.pymLocalStorage.getItem("authToken")).toBeNull();
});
@@ -13,16 +13,15 @@ export type SetAuthTokenMutation = (input: SetAuthTokenInput) => Promise<void>;
export async function commit(
environment: Environment,
input: SetAuthTokenInput,
{ localStorage, pymLocalStorage }: TalkContext
{ localStorage }: TalkContext
) {
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
record.setValue(input.authToken, "authToken");
const storage = pymLocalStorage || localStorage;
if (input.authToken) {
storage.setItem("authToken", input.authToken);
localStorage.setItem("authToken", input.authToken);
} else {
storage.removeItem("authToken");
localStorage.removeItem("authToken");
}
// Increment auth revision to indicate a change in auth state.
record.setValue(record.getValue("authRevision") + 1, "authRevision");
@@ -1,8 +0,0 @@
import {
createInMemoryStorage,
createPromisifiedStorage,
} from "talk-framework/lib/storage";
export default function createFakePymStorage() {
return createPromisifiedStorage(createInMemoryStorage());
}
@@ -4,7 +4,6 @@ export {
} from "./createRelayEnvironment";
export { default as createFluentBundle } from "./createFluentBundle";
export { default as createSinonStub } from "./createSinonStub";
export { default as createFakePymStorage } from "./createFakePymStorage";
export {
default as removeFragmentRefs,
NoFragmentRefs,