Implement pym storage

This commit is contained in:
Chi Vinh Le
2018-08-31 15:09:00 +02:00
parent bcf24cbb4b
commit ccf91480da
14 changed files with 374 additions and 3 deletions
+3
View File
@@ -7,6 +7,7 @@ import {
withClickEvent,
withEventEmitter,
withIOSSafariWidthWorkaround,
withPymStorage,
withSetCommentID,
} from "./decorators";
import PymControl from "./PymControl";
@@ -29,6 +30,8 @@ export function createPymControl(config: CreatePymControlConfig) {
withClickEvent,
withSetCommentID,
withEventEmitter(config.eventEmitter),
withPymStorage(localStorage, "localStorage"),
withPymStorage(sessionStorage, "sessionStorage"),
];
const query = qs.stringify({
@@ -0,0 +1,15 @@
// 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 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 2`] = `Object {}`;
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\\\\\\"}\\"}]"`;
@@ -6,6 +6,7 @@ export { default as withAutoHeight } from "./withAutoHeight";
export { default as withClickEvent } from "./withClickEvent";
export { default as withSetCommentID } from "./withSetCommentID";
export { default as withEventEmitter } from "./withEventEmitter";
export { default as withPymStorage } from "./withPymStorage";
export {
default as withIOSSafariWidthWorkaround,
} from "./withIOSSafariWidthWorkaround";
@@ -0,0 +1,104 @@
import sinon from "sinon";
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 }> = [];
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("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
);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
method: "setItem",
parameters: { key: "key", value: "test" },
})
);
expect(storage.store).toMatchSnapshot();
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "1",
method: "getItem",
parameters: { key: "key" },
})
);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "2",
method: "removeItem",
parameters: { key: "key" },
})
);
expect(storage.store).toMatchSnapshot();
expect(JSON.stringify(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
);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
method: "unknown",
parameters: {},
})
);
expect(JSON.stringify(pym.messages)).toMatchSnapshot();
});
it("should handle handle errors", () => {
const pym = new PymStub("localStorage");
const storage = new FakeStorage();
sinon
.mock(storage)
.expects("getItem")
.throws("error");
withPymStorage(storage as any, "localStorage", "talkPymStorage:")(
pym as any
);
pym.listeners["pymStorage.localStorage.request"](
JSON.stringify({
id: "0",
method: "getItem",
parameters: {},
})
);
expect(JSON.stringify(pym.messages)).toMatchSnapshot();
});
});
@@ -0,0 +1,52 @@
import { Decorator } from "./";
const withPymStorage = (
storage: Storage,
type: "localStorage" | "sessionStorage",
prefix = "talkPymStorage:"
): Decorator => pym => {
pym.onMessage(`pymStorage.${type}.request`, (msg: any) => {
const { id, method, parameters } = JSON.parse(msg);
const { key, value } = parameters;
const prefixedKey = `${prefix}${key}`;
// Variable for the method return value.
let result;
const sendError = (error: string) => {
// tslint:disable-next-line:no-console
console.error(error);
pym.sendMessage(
`pymStorage.${type}.error`,
JSON.stringify({ id, error })
);
};
try {
switch (method) {
case "setItem":
result = storage.setItem(prefixedKey, value);
break;
case "getItem":
result = storage.getItem(prefixedKey);
break;
case "removeItem":
result = storage.removeItem(prefixedKey);
break;
default:
sendError(`Unknown method ${method}`);
return;
}
} catch (err) {
sendError(err.toString());
return;
}
pym.sendMessage(
`pymStorage.${type}.response`,
JSON.stringify({ id, result })
);
});
};
export default withPymStorage;
@@ -8,6 +8,7 @@ import { Environment } from "relay-runtime";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { Storage } from "talk-framework/lib/storage";
import { UIContext } from "talk-ui/components";
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
@@ -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";
@@ -116,8 +117,12 @@ export default async function createContext({
registerClickFarAway,
rest: new RestClient("/api", tokenGetter),
postMessage: new PostMessageService(),
localStorage: createLocalStorage(),
sessionStorage: createSessionStorage(),
localStorage: pym
? createPymStorage(pym, "localStorage")
: createLocalStorage(),
sessionStorage: pym
? createPymStorage(pym, "sessionStorage")
: createSessionStorage(),
};
// 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,57 @@
import { Child, Parent } from "pym.js";
import uuid from "uuid/v4";
import { Storage } from "./interface";
type Pym = Child | Parent;
/**
* 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"
): Storage {
// A Map of requestID => {resolve, reject}
const requests: Record<
string,
{ resolve: ((v: any) => void); reject: ((v: any) => void) }
> = {};
// Requests method with parameters over pym.
const call = <T>(
method: string,
parameters: { key: string; value?: string }
): Promise<T> => {
const id = uuid();
return new Promise((resolve, reject) => {
requests[id] = { resolve, reject };
pym.sendMessage(
`pymStorage.${type}.request`,
JSON.stringify({ id, method, parameters })
);
});
};
// Receive successful responses.
pym.onMessage(`pymStorage.${type}.response`, (msg: string) => {
const { id, result } = JSON.parse(msg);
requests[id].resolve(result);
delete requests[id];
});
// Receive error responses.
pym.onMessage(`pymStorage.${type}.error`, (msg: string) => {
const { id, error } = JSON.parse(msg);
requests[id].reject(new Error(error));
delete requests[id];
});
return {
setItem: (key: string, value: string) => call("setItem", { key, value }),
getItem: (key: string) => call("getItem", { key }),
removeItem: (key: string) => call("removeItem", { key }),
};
}
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`connectStorageToPym should handle handle errors 1`] = `"[{\\"key\\":\\"pymStorage.localStorage.error\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\",\\\\\\"error\\\\\\":\\\\\\"error\\\\\\"}\\"}]"`;
exports[`connectStorageToPym should handle unknown method 1`] = `"[{\\"key\\":\\"pymStorage.localStorage.error\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\",\\\\\\"error\\\\\\":\\\\\\"Unknown method unknown\\\\\\"}\\"}]"`;
exports[`connectStorageToPym should set, get and remove item 1`] = `"{\\"talkPymStorage:key\\":\\"test\\"}"`;
exports[`connectStorageToPym should set, get and remove item 2`] = `"{}"`;
exports[`connectStorageToPym 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,3 +1,5 @@
export { default as createInMemoryStorage } from "./InMemoryStorage";
export { default as createLocalStorage } from "./LocalStorage";
export { default as createSessionStorage } from "./SessionStorage";
export { default as createPymStorage } from "./PymStorage";
export { Storage } from "./interface";
@@ -0,0 +1,14 @@
export interface Storage {
/**
* value = storage[key]
*/
getItem(key: string): Promise<string | null> | string | null;
/**
* delete storage[key]
*/
removeItem(key: string): Promise<void> | void;
/**
* storage[key] = value
*/
setItem(key: string, value: string): Promise<void> | void;
}
@@ -22,6 +22,8 @@ export default async function initLocalState(
environment: Environment,
{ localStorage }: TalkContext
) {
const authToken = await localStorage.getItem("authToken");
commitLocalUpdate(environment, s => {
// TODO: (cvle) move local, auth token and network initialization to framework.
const root = s.getRoot();
@@ -31,7 +33,7 @@ export default async function initLocalState(
root.setLinkedRecord(localRecord, "local");
// Set auth token
localRecord.setValue(localStorage.getItem("authToken") || "", "authToken");
localRecord.setValue(authToken || "", "authToken");
// Set initial auth revision, this is increment whenenver auth state might have changed.
localRecord.setValue(0, "authRevision");