Skip to content

Commit

Permalink
Allow versioning of state stores.
Browse files Browse the repository at this point in the history
  • Loading branch information
jameswilddev committed Jan 20, 2022
1 parent 54204bc commit fa722ed
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 46 deletions.
34 changes: 17 additions & 17 deletions react-native/components/createStateStoreManagerComponent/unit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type TestState = { readonly value: number };

test(`displays the loading screen`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -49,7 +49,7 @@ test(`displays the loading screen`, async () => {

test(`shows the ready screen once given time to load`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -92,7 +92,7 @@ test(`shows the ready screen once given time to load`, async () => {

test(`re-renders when the state is changed externally once`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -136,7 +136,7 @@ test(`re-renders when the state is changed externally once`, async () => {

test(`re-renders when the state is changed externally twice`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -181,7 +181,7 @@ test(`re-renders when the state is changed externally twice`, async () => {

test(`re-renders when the state is changed internally once`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -227,7 +227,7 @@ test(`re-renders when the state is changed internally once`, async () => {

test(`re-renders when the state is changed internally twice`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -275,7 +275,7 @@ test(`re-renders when the state is changed internally twice`, async () => {
});

test(`does not try to load without a key`, async () => {
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -308,7 +308,7 @@ test(`does not try to load without a key`, async () => {

test(`starts unloading when the state key changes to null during loading`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -368,7 +368,7 @@ test(`starts unloading when the state key changes to null during loading`, async

test(`fully unloads when the state key changes to null during loading`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -432,7 +432,7 @@ test(`fully unloads when the state key changes to null during loading`, async ()

test(`starts unloading when the state key changes to null after loading`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -496,7 +496,7 @@ test(`starts unloading when the state key changes to null after loading`, async

test(`fully unloads when the state key changes to null after loading`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -565,7 +565,7 @@ test(`fully unloads when the state key changes to null after loading`, async ()
test(`starts reloading when the state key changes to another value during loading`, async () => {
const stateKeyA = uuid.v4();
const stateKeyB = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
await stateStore.load(stateKeyB);
stateStore.set({ value: 10 });
await stateStore.unload();
Expand Down Expand Up @@ -633,7 +633,7 @@ test(`starts reloading when the state key changes to another value during loadin
test(`fully reloads when the state key changes to another value during loading`, async () => {
const stateKeyA = uuid.v4();
const stateKeyB = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
await stateStore.load(stateKeyB);
stateStore.set({ value: 10 });
await stateStore.unload();
Expand Down Expand Up @@ -705,7 +705,7 @@ test(`fully reloads when the state key changes to another value during loading`,
test(`starts reloading when the state key changes to another value after loading`, async () => {
const stateKeyA = uuid.v4();
const stateKeyB = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
await stateStore.load(stateKeyB);
stateStore.set({ value: 10 });
await stateStore.unload();
Expand Down Expand Up @@ -777,7 +777,7 @@ test(`starts reloading when the state key changes to another value after loading
test(`fully reloads when the state key changes to another value after loading`, async () => {
const stateKeyA = uuid.v4();
const stateKeyB = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
await stateStore.load(stateKeyB);
stateStore.set({ value: 10 });
await stateStore.unload();
Expand Down Expand Up @@ -852,7 +852,7 @@ test(`fully reloads when the state key changes to another value after loading`,

test(`displays the loading screen from null`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down Expand Up @@ -912,7 +912,7 @@ test(`displays the loading screen from null`, async () => {

test(`shows the ready screen once given time to load from null`, async () => {
const stateKey = uuid.v4();
const stateStore = new StateStore<TestState>({ value: 5 });
const stateStore = new StateStore<TestState>({ value: 5 }, `Test Version A`);
const StateStoreManager = createStateStoreManagerComponent(stateStore);

const renderer = TestRenderer.create(
Expand Down
29 changes: 26 additions & 3 deletions react-native/services/StateStore/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import { EventEmitter } from "events";
import type { Json } from "../../types/Json";
import type { StateStoreInterface } from "../../types/StateStoreInterface";

type StateStoreContent<T extends Json> = {
readonly version: string;
readonly value: T;
};

/**
* A wrapper around expo-file-system which adds:
* - Concurrency control.
* - JSON parsing and serialization.
* - Change events.
* - A synchronous read/write API (with asynchronous write-back).
* - Versioning.
* @template T The type of JSON stored.
*/
export class StateStore<T extends Json> implements StateStoreInterface<T> {
Expand All @@ -22,8 +28,13 @@ export class StateStore<T extends Json> implements StateStoreInterface<T> {
/**
* @param initial The value to use when no such record exists
* in expo-file-system.
* @param version A string which identifies the version of the data structure
* within the state store. States previously written with a
* differing version will be discarded and replaced with the
* initial value. This can be used to handle drastic data
* store redesigns early in development.
*/
constructor(private readonly initial: T) {}
constructor(private readonly initial: T, private readonly version: string) {}

addListener(eventType: `set`, listener: () => void): void {
this.eventEmitter.addListener(eventType, listener);
Expand Down Expand Up @@ -52,7 +63,14 @@ export class StateStore<T extends Json> implements StateStoreInterface<T> {

if ((await FileSystem.getInfoAsync(fileUri)).exists) {
const raw = await FileSystem.readAsStringAsync(fileUri);
this.value = JSON.parse(raw);

const content: StateStoreContent<T> = JSON.parse(raw);

if (content.version === this.version) {
this.value = content.value;
} else {
this.value = this.initial;
}
} else {
this.value = this.initial;
}
Expand All @@ -73,9 +91,14 @@ export class StateStore<T extends Json> implements StateStoreInterface<T> {

private startWrite(): void {
(async () => {
const content: StateStoreContent<T> = {
version: this.version,
value: this.value as T,
};

await FileSystem.writeAsStringAsync(
this.fileUri as string,
JSON.stringify(this.value as T)
JSON.stringify(content)
);

this.writeQueueLength--;
Expand Down
15 changes: 13 additions & 2 deletions react-native/services/StateStore/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ A wrapper around `expo-file-system` which adds:
- JSON parsing and serialization.
- Change events.
- A synchronous read/write API (with asynchronous write-back).
- Versioning.

## Usage

Expand All @@ -14,7 +15,7 @@ import type { StateStore } from "react-native-app-helpers";

type State = `State A` | `State B`;

const store = new StateStore<State>(`State A`);
const store = new StateStore<State>(`State A`, `Example Version A`);


await store.load(`AsyncStorage Key A`);
Expand All @@ -30,7 +31,7 @@ console.log(store.get());
await store.unload();


await store.load(`AsyncStorage Key A`);
await store.load(`AsyncStorage Key B`);

// State A
console.log(store.get());
Expand All @@ -43,6 +44,16 @@ await store.load(`AsyncStorage Key A`);
// State B
console.log(store.get());

await store.unload();


const updatedStore = new StateStore<State>(`State A`, `Example Version B`);

await updatedStore.load(`AsyncStorage Key A`);

// State A
console.log(store.get());

await store.unload();
```

Expand Down
Loading

0 comments on commit fa722ed

Please sign in to comment.