Skip to content

Commit

Permalink
impl store's transit option
Browse files Browse the repository at this point in the history
  • Loading branch information
AkifumiSato committed Sep 9, 2023
1 parent d3bac24 commit d41c3e2
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 10 deletions.
41 changes: 40 additions & 1 deletion packages/location-state-core/src/stores/storage-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ test("On `load` called, if the value of the corresponding key is in Storage, the
);
});

test("On `load` called with transit, if the value of the corresponding key is in Storage, then slice is evaluated by deserialize.", () => {
// Arrange
const navigationKey = "current_location";
storageMock.getItem.mockReturnValueOnce(
JSON.stringify({ foo: "storage value" }),
);
const store = new StorageStore(storage, {
serialize: () => "not-used-value",
deserialize: () => ({
foo: "dummy-result",
}),
});
// Act
store.load(navigationKey);
// Assert
expect(store.get("foo")).toBe("dummy-result");
});

test("On `load` called, all listener notified.", async () => {
// Arrange
const navigationKey = "current_location";
Expand All @@ -126,7 +144,28 @@ test("On `load` called, all listener notified.", async () => {
expect(listener2).toBeCalledTimes(1);
});

test("On `save` called, the state is saved in Storage with the previous Location key.", () => {
test("On `save` called, the state is saved in Storage with evaluated by deserialize.", () => {
// Arrange
const currentLocationKey = "current_location";
const store = new StorageStore(storage, {
serialize: () => "dummy-result",
deserialize: () => ({
foo: "not-used-value",
}),
});
store.load(currentLocationKey);
store.set("foo", "updated");
// Act
store.save();
// Assert
expect(storageMock.setItem).toHaveBeenCalledTimes(1);
expect(storageMock.setItem).toHaveBeenCalledWith(
`${locationKeyPrefix}${currentLocationKey}`,
"dummy-result",
);
});

test("On `save` called with transit, the state is saved in Storage with the previous Location key.", () => {
// Arrange
const currentLocationKey = "current_location";
const store = new StorageStore(storage);
Expand Down
20 changes: 15 additions & 5 deletions packages/location-state-core/src/stores/storage-store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { Listener, Store } from "./types";
import { Listener, Store, Transit } from "./types";

export const locationKeyPrefix = "__location_state_";

const defaultTransit: Transit = {
deserialize: JSON.parse,
serialize: JSON.stringify,
};

export class StorageStore implements Store {
private state: Record<string, unknown> = {};
private readonly listeners: Map<string, Set<Listener>> = new Map();
private currentKey: string | null = null;
private readonly transit: Transit;

constructor(private readonly storage?: Storage) {}
constructor(private readonly storage?: Storage, transit?: Transit) {
this.transit = transit ?? defaultTransit;
}

subscribe(name: string, listener: Listener) {
const listeners = this.listeners.get(name);
Expand Down Expand Up @@ -55,8 +63,7 @@ export class StorageStore implements Store {
this.currentKey = locationKey;
const value = this.storage?.getItem(this.createStorageKey()) ?? null;
if (value !== null) {
// todo: impl JSON or Transit
this.state = JSON.parse(value);
this.state = this.transit.deserialize(value);
} else {
this.state = {};
}
Expand All @@ -71,7 +78,10 @@ export class StorageStore implements Store {
this.storage?.removeItem(this.createStorageKey());
return;
}
this.storage?.setItem(this.createStorageKey(), JSON.stringify(this.state));
this.storage?.setItem(
this.createStorageKey(),
this.transit.serialize(this.state),
);
}

private createStorageKey() {
Expand Down
8 changes: 8 additions & 0 deletions packages/location-state-core/src/stores/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export type Listener = () => void;

type Deserializer = (value: string) => Record<string, unknown>;
type Serializer = (value: Record<string, unknown>) => string;

export type Transit = {
deserialize: Deserializer;
serialize: Serializer;
};

export type Store = {
subscribe(name: string, listener: Listener): () => void;

Expand Down
40 changes: 40 additions & 0 deletions packages/location-state-core/src/stores/url-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ test("On `set` called, store's values are updated and reflected in the URL", ()
);
});

test("On `set` called with transit, store's values are updated and reflected in the URL", () => {
// Arrange
prepareLocation({
pathname: "/",
search: "?hoge=fuga",
});
const store = new URLStore("store-key", syncerMock, {
serialize: () => "dummy-result",
deserialize: () => ({
foo: "not-used-value",
}),
});
// Act
store.set("foo", "updated");
// Assert
expect(store.get("foo")).toBe("updated");
expect(syncerMock.updateURL).toHaveBeenCalledTimes(1);
expect(syncerMock.updateURL).toHaveBeenCalledWith(
"http://localhost/?hoge=fuga&store-key=dummy-result",
);
});

test("listener is called when updating slice.", () => {
// Arrange
const store = new URLStore("store-key", syncerMock);
Expand Down Expand Up @@ -128,6 +150,24 @@ test("On `load` called, the state is loaded from url.", () => {
expect(store.get("foo")).toBe("updated");
});

test("On `load` called with transit, the value is obtained through serialize.", () => {
// Arrange
prepareLocation({
pathname: "/",
search: "?store-key=%7B%22foo%22%3A%22updated%22%7D",
});
const store = new URLStore("store-key", syncerMock, {
serialize: () => "not-used-value",
deserialize: () => ({
foo: "dummy-result",
}),
});
// Act
store.load();
// Assert
expect(store.get("foo")).toBe("dummy-result");
});

test("On `load` called, all listener notified.", async () => {
// Arrange
const store = new URLStore("store-key", syncerMock);
Expand Down
20 changes: 16 additions & 4 deletions packages/location-state-core/src/stores/url-store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { Syncer } from "../types";
import { Listener, Store } from "./types";
import { Listener, Store, Transit } from "./types";

const defaultTransit: Transit = {
deserialize: JSON.parse,
serialize: JSON.stringify,
};

export class URLStore implements Store {
private state: Record<string, unknown> = {};
// `state`'s JSON string for comparison
private stateJSON: string = "{}";
private readonly listeners: Map<string, Set<Listener>> = new Map();
private readonly transit: Transit;

constructor(private readonly key: string, private readonly syncer: Syncer) {}
constructor(
private readonly key: string,
private readonly syncer: Syncer,
transit?: Transit,
) {
this.transit = transit ?? defaultTransit;
}

subscribe(name: string, listener: Listener) {
const listeners = this.listeners.get(name);
Expand Down Expand Up @@ -47,7 +59,7 @@ export class URLStore implements Store {
} else {
this.state[name] = value;
}
this.stateJSON = JSON.stringify(this.state);
this.stateJSON = this.transit.serialize(this.state);
// save to url
const url = new URL(location.href);
url.searchParams.set(this.key, this.stateJSON);
Expand All @@ -62,7 +74,7 @@ export class URLStore implements Store {
if (this.stateJSON === stateJSON) return;
this.stateJSON = stateJSON!;
try {
this.state = JSON.parse(this.stateJSON || "{}");
this.state = this.transit.deserialize(this.stateJSON || "{}");
} catch (e) {
this.state = {};
// remove invalid state from url.
Expand Down

0 comments on commit d41c3e2

Please sign in to comment.