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
@@ -8,7 +8,7 @@ import AppContainer from "talk-auth/containers/AppContainer";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { createInMemoryStorage } from "talk-framework/lib/storage";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import createEnvironment from "./createEnvironment";
import createFluentBundle from "./createFluentBundle";
@@ -23,8 +23,8 @@ function createTestRenderer(initialView: string): ReactTestRenderer {
const context: TalkContext = {
relayEnvironment: environment,
localeBundles: [createFluentBundle()],
localStorage: createInMemoryStorage(),
sessionStorage: createInMemoryStorage(),
localStorage: createPromisifiedStorage(),
sessionStorage: createPromisifiedStorage(),
rest: new RestClient("http://localhost/api"),
postMessage: new PostMessageService(),
browserInfo: { ios: false },
+3 -3
View File
@@ -11,7 +11,7 @@ import { animationFrame, timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { createInMemoryStorage } from "talk-framework/lib/storage";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import createEnvironment from "./createEnvironment";
import createFluentBundle from "./createFluentBundle";
@@ -33,8 +33,8 @@ beforeEach(() => {
context = {
relayEnvironment: environment,
localeBundles: [createFluentBundle()],
localStorage: createInMemoryStorage(),
sessionStorage: createInMemoryStorage(),
localStorage: createPromisifiedStorage(),
sessionStorage: createPromisifiedStorage(),
rest: new RestClient("http://localhost/api"),
postMessage: new PostMessageService(),
browserInfo: { ios: false },
+3 -3
View File
@@ -11,7 +11,7 @@ import { animationFrame, timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { createInMemoryStorage } from "talk-framework/lib/storage";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import createEnvironment from "./createEnvironment";
import createFluentBundle from "./createFluentBundle";
@@ -33,8 +33,8 @@ beforeEach(() => {
context = {
relayEnvironment: environment,
localeBundles: [createFluentBundle()],
localStorage: createInMemoryStorage(),
sessionStorage: createInMemoryStorage(),
localStorage: createPromisifiedStorage(),
sessionStorage: createPromisifiedStorage(),
rest: new RestClient("http://localhost/api"),
postMessage: new PostMessageService(),
browserInfo: { ios: false },
@@ -1,15 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`withPymStorage should handle handle errors 1`] = `"[{\\"key\\":\\"pymStorage.localStorage.error\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\",\\\\\\"error\\\\\\":\\\\\\"error\\\\\\"}\\"}]"`;
exports[`withPymStorage should clear storage 1`] = `"{}"`;
exports[`withPymStorage should clear storage 2`] = `
Array [
Object {
"key": "pymStorage.localStorage.response",
"value": "{\\"id\\":\\"0\\"}",
},
]
`;
exports[`withPymStorage should get key of storage 1`] = `
Array [
Object {
"key": "pymStorage.localStorage.response",
"value": "{\\"id\\":\\"0\\",\\"result\\":\\"b\\"}",
},
Object {
"key": "pymStorage.localStorage.response",
"value": "{\\"id\\":\\"0\\",\\"result\\":null}",
},
]
`;
exports[`withPymStorage should get length of storage 1`] = `
Array [
Object {
"key": "pymStorage.localStorage.response",
"value": "{\\"id\\":\\"0\\",\\"result\\":3}",
},
]
`;
exports[`withPymStorage should handle handle errors 1`] = `
Array [
Object {
"key": "pymStorage.localStorage.error",
"value": "{\\"id\\":\\"0\\",\\"error\\":\\"error\\"}",
},
]
`;
exports[`withPymStorage should handle unknown method 1`] = `"[{\\"key\\":\\"pymStorage.localStorage.error\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\",\\\\\\"error\\\\\\":\\\\\\"Unknown method unknown\\\\\\"}\\"}]"`;
exports[`withPymStorage should set, get and remove item 1`] = `
Object {
"talkPymStorage:key": "test",
}
`;
exports[`withPymStorage should set, get and remove item 1`] = `"{\\"talk:key\\":\\"test\\"}"`;
exports[`withPymStorage should set, get and remove item 2`] = `Object {}`;
exports[`withPymStorage should set, get and remove item 2`] = `"{}"`;
exports[`withPymStorage should set, get and remove item 3`] = `"[{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\"}\\"},{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"1\\\\\\",\\\\\\"result\\\\\\":\\\\\\"test\\\\\\"}\\"},{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"2\\\\\\"}\\"}]"`;
@@ -1,23 +1,8 @@
import sinon from "sinon";
import { createInMemoryStorage } from "../testUtils";
import withPymStorage from "./withPymStorage";
// tslint:disable:max-classes-per-file
class FakeStorage {
public store: Record<string, string> = {};
public setItem(key: string, value: string) {
this.store[key] = value;
}
public removeItem(key: string) {
delete this.store[key];
}
public getItem(key: string) {
return this.store[key];
}
}
class PymStub {
public listeners: Record<string, ((msg: string) => void)> = {};
public messages: Array<{ key: string; value: string }> = [];
@@ -38,10 +23,8 @@ class PymStub {
describe("withPymStorage", () => {
it("should set, get and remove item", () => {
const pym = new PymStub("localStorage");
const storage = new FakeStorage();
withPymStorage(storage as any, "localStorage", "talkPymStorage:")(
pym as any
);
const storage = createInMemoryStorage();
withPymStorage(storage, "localStorage", "talk:")(pym as any);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
@@ -49,7 +32,7 @@ describe("withPymStorage", () => {
parameters: { key: "key", value: "test" },
})
);
expect(storage.store).toMatchSnapshot();
expect(storage.toString()).toMatchSnapshot();
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "1",
@@ -64,15 +47,72 @@ describe("withPymStorage", () => {
parameters: { key: "key" },
})
);
expect(storage.store).toMatchSnapshot();
expect(storage.toString()).toMatchSnapshot();
expect(JSON.stringify(pym.messages)).toMatchSnapshot();
});
it("should get key of storage", () => {
const pym = new PymStub("localStorage");
const storage = createInMemoryStorage({
a: "1",
b: "2",
c: "3",
});
withPymStorage(storage, "localStorage", "")(pym as any);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
method: "key",
parameters: { n: 1 },
})
);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
method: "key",
parameters: { n: 3 },
})
);
expect(pym.messages).toMatchSnapshot();
});
it("should get length of storage", () => {
const pym = new PymStub("localStorage");
const storage = createInMemoryStorage({
a: "1",
b: "2",
c: "3",
});
withPymStorage(storage, "localStorage", "")(pym as any);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
method: "length",
parameters: {},
})
);
expect(pym.messages).toMatchSnapshot();
});
it("should clear storage", () => {
const pym = new PymStub("localStorage");
const storage = createInMemoryStorage({
a: "1",
b: "2",
c: "3",
});
withPymStorage(storage, "localStorage", "")(pym as any);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
method: "clear",
parameters: {},
})
);
expect(storage.toString()).toMatchSnapshot();
expect(pym.messages).toMatchSnapshot();
});
it("should handle unknown method", () => {
const pym = new PymStub("localStorage");
const storage = new FakeStorage();
withPymStorage(storage as any, "localStorage", "talkPymStorage:")(
pym as any
);
const storage = createInMemoryStorage();
withPymStorage(storage, "localStorage", "talk:")(pym as any);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
@@ -84,14 +124,12 @@ describe("withPymStorage", () => {
});
it("should handle handle errors", () => {
const pym = new PymStub("localStorage");
const storage = new FakeStorage();
const storage = createInMemoryStorage();
sinon
.mock(storage)
.expects("getItem")
.throws("error");
withPymStorage(storage as any, "localStorage", "talkPymStorage:")(
pym as any
);
withPymStorage(storage, "localStorage", "talk:")(pym as any);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
@@ -99,6 +137,6 @@ describe("withPymStorage", () => {
parameters: {},
})
);
expect(JSON.stringify(pym.messages)).toMatchSnapshot();
expect(pym.messages).toMatchSnapshot();
});
});
@@ -1,14 +1,15 @@
import { prefixStorage } from "../utils";
import { Decorator } from "./types";
const withPymStorage = (
storage: Storage,
type: "localStorage" | "sessionStorage",
prefix = "talkPymStorage:"
prefix = "talk:"
): Decorator => pym => {
pym.onMessage(`pymStorage.${type}.request`, (msg: any) => {
const { id, method, parameters } = JSON.parse(msg);
const { key, value } = parameters;
const prefixedKey = `${prefix}${key}`;
const { n, key, value } = parameters;
const prefixedStorage = prefixStorage(storage, prefix);
// Variable for the method return value.
let result;
@@ -25,13 +26,22 @@ const withPymStorage = (
try {
switch (method) {
case "setItem":
result = storage.setItem(prefixedKey, value);
result = prefixedStorage.setItem(key, value);
break;
case "getItem":
result = storage.getItem(prefixedKey);
result = prefixedStorage.getItem(key);
break;
case "removeItem":
result = storage.removeItem(prefixedKey);
result = prefixedStorage.removeItem(key);
break;
case "key":
result = prefixedStorage.key(n);
break;
case "length":
result = prefixedStorage.length;
break;
case "clear":
result = prefixedStorage.clear();
break;
default:
sendError(`Unknown method ${method}`);
@@ -0,0 +1,34 @@
import createInMemoryStorage from "./InMemoryStorage";
it("should set and unset values", () => {
const storage = createInMemoryStorage();
storage.setItem("test", "value");
expect(storage.getItem("test")).toBe("value");
storage.removeItem("test");
expect(storage.getItem("test")).toBeNull();
});
it("should return length", () => {
const storage = createInMemoryStorage();
storage.setItem("a", "value");
storage.setItem("b", "value");
storage.setItem("c", "value");
expect(storage.length).toBe(3);
});
it("should nth key", () => {
const storage = createInMemoryStorage();
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();
});
@@ -0,0 +1,49 @@
/**
* InMemoryStorage is a dumb implementation of the Storage interface that will
* not persist the data at all. It implements the Storage interface found:
*
* https://developer.mozilla.org/en-US/docs/Web/API/Storage
*/
class InMemoryStorage implements Storage {
private data: Record<string, string>;
constructor(data: Record<string, string> = {}) {
this.data = data;
}
get length() {
return Object.keys(this.data).length;
}
public clear() {
this.data = {};
}
public key(n: number) {
if (this.length <= n) {
return null;
}
return Object.keys(this.data)[n];
}
public getItem(key: string) {
return this.data[key] || null;
}
public setItem(key: string, value: string) {
this.data[key] = value;
}
public removeItem(key: string) {
delete this.data[key];
}
public toString() {
return JSON.stringify(this.data);
}
}
export default function createInMemoryStorage(data?: Record<string, string>) {
return new InMemoryStorage(data);
}
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`accepts predefined data 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"c\\":\\"2\\"}"`;
+1
View File
@@ -0,0 +1 @@
export { default as createInMemoryStorage } from "./InMemoryStorage";
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should call clear 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"d\\":\\"3\\"}"`;
+2
View File
@@ -1,2 +1,4 @@
export { default as buildURL } from "./buildURL";
export { default as ensureEndSlash } from "./ensureEndSlash";
export { default as startsWith } from "./startsWith";
export { default as prefixStorage } from "./prefixStorage";
@@ -0,0 +1,79 @@
import sinon from "sinon";
import { createInMemoryStorage } from "../testUtils";
import prefixStorage from "./prefixStorage";
it("should get nth key", () => {
const storage = createInMemoryStorage({
a: "0",
b: "1",
"talk:c": "2",
d: "3",
"talk:e": "4",
});
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();
expect(storage.toString()).toMatchSnapshot();
});
it("should call length", () => {
const storage = createInMemoryStorage({
a: "0",
b: "1",
"talk:c": "2",
d: "3",
"talk:e": "4",
});
const prefixed = prefixStorage(storage, "talk:");
expect(prefixed.length).toBe(2);
});
it("should prefix setItem", () => {
const storage = {
setItem: sinon.mock().withArgs("talk:key", "value"),
};
const prefixed = prefixStorage(storage as any, "talk:");
prefixed.setItem("key", "value");
storage.setItem.verify();
});
it("should prefix removeItem", () => {
const storage = {
removeItem: sinon.mock().withArgs("talk:key"),
};
const prefixed = prefixStorage(storage as any, "talk:");
prefixed.removeItem("key");
storage.removeItem.verify();
});
it("should prefix getItem", () => {
const ret = "value";
const storage = {
getItem: sinon
.mock()
.withArgs("talk:key")
.returns(ret),
};
const prefixed = prefixStorage(storage as any, "talk:");
expect(prefixed.getItem("key")).toBe(ret);
(storage.getItem as any).verify();
});
@@ -0,0 +1,66 @@
import startsWith from "./startsWith";
/**
* PrefixedStorage decorates a Storage and prefixes keys in
* getItem, setItem and removeItem with given prefix.
*/
class PrefixedStorage implements Storage {
private storage: Storage;
private prefix: string;
constructor(storage: Storage, prefix: string) {
this.storage = storage;
this.prefix = prefix;
}
get length() {
let count = 0;
for (let i = 0; i < this.storage.length; i++) {
if (startsWith(this.storage.key(i)!, this.prefix)) {
count++;
}
}
return count;
}
public clear() {
const toBeDeleted = [];
for (let i = 0; i < this.storage.length; i++) {
const key = this.storage.key(i)!;
if (startsWith(key, this.prefix)) {
toBeDeleted.push(key);
}
}
toBeDeleted.forEach(key => this.storage.removeItem(key));
}
public key(n: number) {
let count = 0;
for (let i = 0; i < this.storage.length; i++) {
const key = this.storage.key(i)!;
if (startsWith(key, this.prefix)) {
if (count === n) {
return key;
}
count++;
}
}
return null;
}
public getItem(key: string) {
return this.storage.getItem(`${this.prefix}${key}`);
}
public setItem(key: string, value: string) {
return this.storage.setItem(`${this.prefix}${key}`, value);
}
public removeItem(key: string) {
return this.storage.removeItem(`${this.prefix}${key}`);
}
}
export default function prefixStorage(storage: Storage, prefix: string) {
return new PrefixedStorage(storage, prefix);
}
@@ -0,0 +1,7 @@
import startsWith from "./startsWith";
it("should work correctly", () => {
const str1 = "Saturday night plans";
expect(startsWith(str1, "Sat")).toBe(true);
expect(startsWith(str1, "Sat", 3)).toBe(false);
});
@@ -0,0 +1,4 @@
/** A substitute for string.startsWith */
export default function startsWith(str: string, search: string, pos?: number) {
return str.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
}
@@ -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,
@@ -3,10 +3,9 @@ import { noop } from "lodash";
import React from "react";
import sinon from "sinon";
import { PropTypesOf } from "talk-framework/types";
import { timeout } from "talk-common/utils";
import { createFakePymStorage } from "talk-framework/testHelpers";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { PropTypesOf } from "talk-framework/types";
import { PostCommentFormContainer } from "./PostCommentFormContainer";
const contextKey = "postCommentFormBody";
@@ -16,7 +15,7 @@ it("renders correctly", async () => {
// tslint:disable-next-line:no-empty
createComment: (() => {}) as any,
assetID: "asset-id",
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
};
const wrapper = shallow(<PostCommentFormContainer {...props} />);
@@ -30,10 +29,10 @@ it("renders with initialValues", async () => {
// tslint:disable-next-line:no-empty
createComment: (() => {}) as any,
assetID: "asset-id",
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
};
await props.pymSessionStorage.setItem(contextKey, "Hello World!");
await props.sessionStorage.setItem(contextKey, "Hello World!");
const wrapper = shallow(<PostCommentFormContainer {...props} />);
await timeout();
@@ -46,10 +45,10 @@ it("save values", async () => {
// tslint:disable-next-line:no-empty
createComment: (() => {}) as any,
assetID: "asset-id",
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
};
await props.pymSessionStorage.setItem(contextKey, "Hello World!");
await props.sessionStorage.setItem(contextKey, "Hello World!");
const wrapper = shallow(<PostCommentFormContainer {...props} />);
await timeout();
@@ -58,7 +57,7 @@ it("save values", async () => {
.first()
.props()
.onChange({ values: { body: "changed" } });
expect(await props.pymSessionStorage.getItem(contextKey)).toBe("changed");
expect(await props.sessionStorage.getItem(contextKey)).toBe("changed");
});
it("creates a comment", async () => {
@@ -76,10 +75,10 @@ it("creates a comment", async () => {
// tslint:disable-next-line:no-empty
createComment: createCommentStub,
assetID,
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
};
await props.pymSessionStorage.setItem(contextKey, "Hello World!");
await props.sessionStorage.setItem(contextKey, "Hello World!");
const wrapper = shallow(<PostCommentFormContainer {...props} />);
await timeout();
@@ -13,7 +13,7 @@ import { CreateCommentMutation, withCreateCommentMutation } from "../mutations";
interface InnerProps {
createComment: CreateCommentMutation;
assetID: string;
pymSessionStorage: PromisifiedStorage;
sessionStorage: PromisifiedStorage;
}
interface State {
@@ -32,7 +32,7 @@ export class PostCommentFormContainer extends Component<InnerProps, State> {
}
private async init() {
const body = await this.props.pymSessionStorage.getItem(contextKey);
const body = await this.props.sessionStorage.getItem(contextKey);
if (body) {
this.setState({
initialValues: {
@@ -67,9 +67,9 @@ export class PostCommentFormContainer extends Component<InnerProps, State> {
private handleOnChange: PostCommentFormProps["onChange"] = state => {
if (state.values.body) {
this.props.pymSessionStorage.setItem(contextKey, state.values.body);
this.props.sessionStorage.setItem(contextKey, state.values.body);
} else {
this.props.pymSessionStorage.removeItem(contextKey);
this.props.sessionStorage.removeItem(contextKey);
}
};
@@ -87,8 +87,8 @@ export class PostCommentFormContainer extends Component<InnerProps, State> {
}
}
const enhanced = withContext(({ pymSessionStorage }) => ({
pymSessionStorage,
const enhanced = withContext(({ sessionStorage }) => ({
sessionStorage,
}))(withCreateCommentMutation(PostCommentFormContainer));
export type PostCommentFormContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -3,13 +3,10 @@ import { noop } from "lodash";
import React from "react";
import sinon from "sinon";
import { PropTypesOf } from "talk-framework/types";
import { timeout } from "talk-common/utils";
import {
createFakePymStorage,
removeFragmentRefs,
} from "talk-framework/testHelpers";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import { ReplyCommentFormContainer } from "./ReplyCommentFormContainer";
const ReplyCommentFormContainerN = removeFragmentRefs(
@@ -29,7 +26,7 @@ it("renders correctly", async () => {
comment: {
id: "comment-id",
},
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
autofocus: false,
};
@@ -48,11 +45,11 @@ it("renders with initialValues", async () => {
comment: {
id: "comment-id",
},
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
autofocus: false,
};
await props.pymSessionStorage.setItem(
await props.sessionStorage.setItem(
getContextKey(props.comment.id),
"Hello World!"
);
@@ -72,11 +69,11 @@ it("save values", async () => {
comment: {
id: "comment-id",
},
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
autofocus: false,
};
await props.pymSessionStorage.setItem(
await props.sessionStorage.setItem(
getContextKey(props.comment.id),
"Hello World!"
);
@@ -89,7 +86,7 @@ it("save values", async () => {
.props()
.onChange({ values: { body: "changed" } });
expect(
await props.pymSessionStorage.getItem(getContextKey(props.comment.id))
await props.sessionStorage.getItem(getContextKey(props.comment.id))
).toBe("changed");
});
@@ -108,12 +105,12 @@ it("creates a comment", async () => {
comment: {
id: "comment-id",
},
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
onClose: onCloseStub,
autofocus: false,
};
await props.pymSessionStorage.setItem(
await props.sessionStorage.setItem(
getContextKey(props.comment.id),
"Hello World!"
);
@@ -146,12 +143,12 @@ it("closes on cancel", async () => {
comment: {
id: "comment-id",
},
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
onClose: onCloseStub,
autofocus: false,
};
await props.pymSessionStorage.setItem(
await props.sessionStorage.setItem(
getContextKey(props.comment.id),
"Hello World!"
);
@@ -166,7 +163,7 @@ it("closes on cancel", async () => {
// Removes saved value.
expect(
await props.pymSessionStorage.getItem(getContextKey(props.comment.id))
await props.sessionStorage.getItem(getContextKey(props.comment.id))
).toBeNull();
});
@@ -181,7 +178,7 @@ it("autofocuses", async () => {
comment: {
id: "comment-id",
},
pymSessionStorage: createFakePymStorage(),
sessionStorage: createPromisifiedStorage(),
autofocus: true,
};
@@ -17,7 +17,7 @@ import { CreateCommentMutation, withCreateCommentMutation } from "../mutations";
interface InnerProps {
createComment: CreateCommentMutation;
pymSessionStorage: PromisifiedStorage;
sessionStorage: PromisifiedStorage;
comment: CommentData;
asset: AssetData;
onClose?: () => void;
@@ -45,7 +45,7 @@ export class ReplyCommentFormContainer extends Component<InnerProps, State> {
};
private async init() {
const body = await this.props.pymSessionStorage.getItem(this.contextKey);
const body = await this.props.sessionStorage.getItem(this.contextKey);
if (body) {
this.setState({
initialValues: {
@@ -59,7 +59,7 @@ export class ReplyCommentFormContainer extends Component<InnerProps, State> {
}
private handleOnCancel = () => {
this.props.pymSessionStorage.removeItem(this.contextKey);
this.props.sessionStorage.removeItem(this.contextKey);
if (this.props.onClose) {
this.props.onClose();
}
@@ -76,7 +76,7 @@ export class ReplyCommentFormContainer extends Component<InnerProps, State> {
...input,
});
this.props.pymSessionStorage.removeItem(this.contextKey);
this.props.sessionStorage.removeItem(this.contextKey);
if (this.props.onClose) {
this.props.onClose();
}
@@ -92,9 +92,9 @@ export class ReplyCommentFormContainer extends Component<InnerProps, State> {
private handleOnChange: ReplyCommentFormProps["onChange"] = state => {
if (state.values.body) {
this.props.pymSessionStorage.setItem(this.contextKey, state.values.body);
this.props.sessionStorage.setItem(this.contextKey, state.values.body);
} else {
this.props.pymSessionStorage.removeItem(this.contextKey);
this.props.sessionStorage.removeItem(this.contextKey);
}
};
@@ -114,8 +114,8 @@ export class ReplyCommentFormContainer extends Component<InnerProps, State> {
);
}
}
const enhanced = withContext(({ pymSessionStorage, browserInfo }) => ({
pymSessionStorage,
const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
sessionStorage,
// Disable autofocus on ios and enable for the rest.
autofocus: !browserInfo.ios,
}))(
@@ -3,10 +3,7 @@ import { Environment, RecordSource } from "relay-runtime";
import { timeout } from "talk-common/utils";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
import {
createInMemoryStorage,
createPromisifiedStorage,
} from "talk-framework/lib/storage";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import initLocalState from "./initLocalState";
@@ -24,7 +21,7 @@ beforeEach(() => {
it("init local state", async () => {
const context: Partial<TalkContext> = {
pymLocalStorage: createPromisifiedStorage(createInMemoryStorage()),
localStorage: createPromisifiedStorage(),
};
await initLocalState(environment, context as any);
await timeout();
@@ -33,7 +30,7 @@ it("init local state", async () => {
it("set assetID from query", async () => {
const context: Partial<TalkContext> = {
pymLocalStorage: createPromisifiedStorage(createInMemoryStorage()),
localStorage: createPromisifiedStorage(),
};
const assetID = "asset-id";
const previousLocation = location.toString();
@@ -50,7 +47,7 @@ it("set assetID from query", async () => {
it("set commentID from query", async () => {
const context: Partial<TalkContext> = {
pymLocalStorage: createPromisifiedStorage(createInMemoryStorage()),
localStorage: createPromisifiedStorage(),
};
const commentID = "comment-id";
const previousLocation = location.toString();
@@ -67,10 +64,10 @@ it("set commentID from query", async () => {
it("set authToken from localStorage", async () => {
const context: Partial<TalkContext> = {
pymLocalStorage: createPromisifiedStorage(createInMemoryStorage()),
localStorage: createPromisifiedStorage(),
};
const authToken = "auth-token";
context.pymLocalStorage!.setItem("authToken", authToken);
context.localStorage!.setItem("authToken", authToken);
await initLocalState(environment, context as any);
expect(source.get(LOCAL_ID)!.authToken).toBe(authToken);
});
@@ -20,9 +20,9 @@ import {
*/
export default async function initLocalState(
environment: Environment,
{ pymLocalStorage }: TalkContext
{ localStorage }: TalkContext
) {
const authToken = await pymLocalStorage!.getItem("authToken");
const authToken = await localStorage!.getItem("authToken");
commitLocalUpdate(environment, s => {
// TODO: (cvle) move local, auth token and network initialization to framework.
+3 -6
View File
@@ -6,8 +6,7 @@ import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { createInMemoryStorage } from "talk-framework/lib/storage";
import { createFakePymStorage } from "talk-framework/testHelpers";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import AppContainer from "talk-stream/containers/AppContainer";
import createEnvironment from "./createEnvironment";
@@ -40,10 +39,8 @@ export default function create(params: CreateParams) {
const context: TalkContext = {
relayEnvironment: environment,
localeBundles: [createFluentBundle()],
localStorage: createInMemoryStorage(),
sessionStorage: createInMemoryStorage(),
pymLocalStorage: createFakePymStorage(),
pymSessionStorage: createFakePymStorage(),
localStorage: createPromisifiedStorage(),
sessionStorage: createPromisifiedStorage(),
rest: new RestClient("http://localhost/api"),
postMessage: new PostMessageService(),
browserInfo: { ios: false },