[next] Save Comment Draft + Pym Storage (#1843)

* Implement pym storage

* Save comment draft + test

* Apply suggestions

* Use class for PymStorage implementation

* Add some comments
This commit is contained in:
Kiwi
2018-09-06 17:07:17 +00:00
committed by Wyatt Johnson
parent 0477d456a2
commit 53e548d77a
36 changed files with 727 additions and 283 deletions
@@ -8,6 +8,7 @@ import { Environment } from "relay-runtime";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { PymStorage } from "talk-framework/lib/storage";
import { UIContext } from "talk-ui/components";
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
@@ -21,12 +22,18 @@ export interface TalkContext {
/** formatter for timeago. */
timeagoFormatter?: Formatter;
/** Session Storage */
/** Local Storage */
localStorage: Storage;
/** Session storage */
sessionStorage: Storage;
/** Local Storage over pym */
pymLocalStorage?: PymStorage;
/** Session storage over pym */
pymSessionStorage?: PymStorage;
/** media query values for testing purposes */
mediaQueryValues?: MediaQueryMatchers;
@@ -8,6 +8,7 @@ import { Environment, Network, RecordSource, Store } from "relay-runtime";
import { LOCAL_ID } from "talk-framework/lib/relay";
import {
createLocalStorage,
createPymStorage,
createSessionStorage,
} from "talk-framework/lib/storage";
@@ -118,6 +119,8 @@ export default async function createContext({
postMessage: new PostMessageService(),
localStorage: createLocalStorage(),
sessionStorage: createSessionStorage(),
pymLocalStorage: pym && createPymStorage(pym, "localStorage"),
pymSessionStorage: pym && createPymStorage(pym, "sessionStorage"),
};
// Run custom initializations.
@@ -38,6 +38,10 @@ class InMemoryStorage implements Storage {
public removeItem(key: string) {
delete this.storage[key];
}
public toString() {
return JSON.stringify(this.storage);
}
}
export default function createInMemoryStorage() {
@@ -0,0 +1,100 @@
import createPymStorage from "./PymStorage";
class PymStub {
public listeners: Record<string, ((msg: string) => void)> = {};
public messages: Array<{ key: string; value: string }> = [];
public type: string;
constructor(type: string) {
this.type = type;
}
public onMessage(key: string, callback: (msg: string) => void) {
this.listeners[key] = callback;
}
public sendMessage(key: string, value: string) {
this.messages.push({ key, value });
}
}
describe("PymStorage", () => {
it("should set item", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.setItem("test", "value");
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id, method, parameters } = JSON.parse(value);
expect(method).toBe("setItem");
expect(parameters).toEqual({ key: "test", value: "value" });
pym.listeners["pymStorage.localStorage.response"](JSON.stringify({ id }));
expect(promise).resolves.toBeUndefined();
});
it("should remove item", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.removeItem("test");
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id, method, parameters } = JSON.parse(value);
expect(method).toBe("removeItem");
expect(parameters).toEqual({ key: "test" });
pym.listeners["pymStorage.localStorage.response"](JSON.stringify({ id }));
expect(promise).resolves.toBeUndefined();
});
it("should get item", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.getItem("test");
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id, method, parameters } = JSON.parse(value);
expect(method).toBe("getItem");
expect(parameters).toEqual({ key: "test" });
pym.listeners["pymStorage.localStorage.response"](
JSON.stringify({ id, result: "value" })
);
expect(promise).resolves.toBe("value");
});
describe("on error", () => {
it("should reject set item", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.setItem("test", "value");
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id } = JSON.parse(value);
pym.listeners["pymStorage.localStorage.error"](
JSON.stringify({ id, error: "error" })
);
expect(promise).rejects.toThrow(new Error("error"));
});
it("should reject remove item", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.removeItem("test");
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id } = JSON.parse(value);
pym.listeners["pymStorage.localStorage.error"](
JSON.stringify({ id, error: "error" })
);
expect(promise).rejects.toThrow(new Error("error"));
});
it("should reject get item", () => {
const pym = new PymStub("localStorage");
const storage = createPymStorage(pym as any, "localStorage");
const promise = storage.getItem("test");
const { key, value } = pym.messages.pop()!;
expect(key).toBe(`pymStorage.localStorage.request`);
const { id } = JSON.parse(value);
pym.listeners["pymStorage.localStorage.error"](
JSON.stringify({ id, error: "error" })
);
expect(promise).rejects.toThrow(new Error("error"));
});
});
});
@@ -0,0 +1,94 @@
import { Child, Parent } from "pym.js";
import uuid from "uuid/v4";
type Pym = Child | Parent;
export interface PymStorage {
/**
* value = storage[key]
*/
getItem(key: string): Promise<string | null>;
/**
* delete storage[key]
*/
removeItem(key: string): Promise<void>;
/**
* storage[key] = value
*/
setItem(key: string, value: string): Promise<void>;
}
class PymStorageImpl implements PymStorage {
/** Instance to pym */
private pym: Pym;
/** Requested storage type */
private type: string;
/** A Map of requestID => {resolve, reject} */
private requests: Record<
string,
{ resolve: ((v: any) => void); reject: ((v: any) => void) }
> = {};
/** Requests method with parameters over pym. */
private call<T>(
method: string,
parameters: { key: string; value?: string }
): Promise<T> {
const id = uuid();
return new Promise((resolve, reject) => {
this.requests[id] = { resolve, reject };
this.pym.sendMessage(
`pymStorage.${this.type}.request`,
JSON.stringify({ id, method, parameters })
);
});
}
/** Listen to pym responses */
private listen() {
// Receive successful responses.
this.pym.onMessage(`pymStorage.${this.type}.response`, (msg: string) => {
const { id, result } = JSON.parse(msg);
this.requests[id].resolve(result);
delete this.requests[id];
});
// Receive error responses.
this.pym.onMessage(`pymStorage.${this.type}.error`, (msg: string) => {
const { id, error } = JSON.parse(msg);
this.requests[id].reject(new Error(error));
delete this.requests[id];
});
}
constructor(pym: Pym, type: string) {
this.pym = pym;
this.type = type;
this.listen();
}
public setItem(key: string, value: string) {
return this.call<void>("setItem", { key, value });
}
public getItem(key: string) {
return this.call<string | null>("getItem", { key });
}
public removeItem(key: string) {
return this.call<void>("removeItem", { key });
}
}
/**
* Creates a storage that put requests onto pym.
* This is the counterpart of `connectStorageToPym`.
* @param {string} pym pym
* @return {Object} storage
*/
export default function createPymStorage(
pym: Pym,
type: "localStorage" | "sessionStorage"
): PymStorage {
return new PymStorageImpl(pym, type);
}
@@ -1,3 +1,4 @@
export { default as createInMemoryStorage } from "./InMemoryStorage";
export { default as createLocalStorage } from "./LocalStorage";
export { default as createSessionStorage } from "./SessionStorage";
export { default as createPymStorage, PymStorage } from "./PymStorage";
@@ -0,0 +1,21 @@
import { PymStorage } from "talk-framework/lib/storage";
export class FakeStorage implements PymStorage {
public store: Record<string, string> = {};
public setItem(key: string, value: string) {
this.store[key] = value;
return Promise.resolve();
}
public removeItem(key: string) {
delete this.store[key];
return Promise.resolve();
}
public getItem(key: string) {
return Promise.resolve(this.store[key]);
}
}
export default function createFakePymStorage() {
return new FakeStorage();
}
@@ -4,6 +4,7 @@ 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,