Skip to content

Commit

Permalink
Merge pull request #6 from dgoemans/feature/TypescriptRewrite
Browse files Browse the repository at this point in the history
Attempt to make spot's loading a bit more consistent
  • Loading branch information
dgoemans authored Nov 2, 2020
2 parents 08deda2 + bf1de94 commit 886c4bb
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 23 deletions.
51 changes: 39 additions & 12 deletions __tests__/command-query.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import fetchMock from 'jest-fetch-mock';
import * as crypto from 'crypto';

import { initializeSpot, Spot } from '../src';

fetchMock.enableMocks();
Object.defineProperty(global, 'crypto', {
value: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRandomValues: (arr:any) => crypto.randomBytes(arr.length),
},
});

interface User {
name: string;
Expand All @@ -23,6 +29,10 @@ const baseUrl = 'http://example.com';
const waitForLoadingDone = (spot: Spot<DataType>) => new Promise(spot.subscribeOnce);

describe('spot', () => {
beforeAll(() => {
fetchMock.enableMocks();
});

beforeEach(() => {
users = {
'id-one': {
Expand Down Expand Up @@ -82,7 +92,7 @@ describe('spot', () => {

{
spot.query('fetch-user', { userId: 'id-one' }, ['users', 'id-one']);
expect(spot.data).toStrictEqual({ loading: true });
expect(spot.data).toMatchObject({ loading: true });

await waitForLoadingDone(spot);

Expand All @@ -93,7 +103,7 @@ describe('spot', () => {
loading: false,
};

expect(spot.data).toStrictEqual(expectedResult);
expect(spot.data).toMatchObject(expectedResult);
}

{
Expand All @@ -106,19 +116,19 @@ describe('spot', () => {
},
loading: false,
};
expect(spot.data).toStrictEqual(expectedResult);
expect(spot.data).toMatchObject(expectedResult);
}
});

it('Adds errors when the fetch fails', async () => {
const spot = initializeSpot<DataType>(baseUrl);

spot.query('invalid-json');
expect(spot.data).toStrictEqual({ loading: true });
expect(spot.data).toMatchObject({ loading: true });

await waitForLoadingDone(spot);

expect(spot.data).toStrictEqual({ loading: false });
expect(spot.data).toMatchObject({ loading: false });
expect(spot.errors).toHaveLength(1);
expect(spot.errors[0]).toMatchSnapshot();
});
Expand All @@ -135,7 +145,7 @@ describe('spot', () => {
},
loading: false,
};
expect(spot.data).toStrictEqual(expectedResult);
expect(spot.data).toMatchObject(expectedResult);
}

await spot.command('update-user', { userId: 'id-two', age: 4 });
Expand All @@ -149,7 +159,7 @@ describe('spot', () => {
},
loading: false,
};
expect(spot.data).toStrictEqual(expectedResult);
expect(spot.data).toMatchObject(expectedResult);
}
});

Expand All @@ -159,10 +169,10 @@ describe('spot', () => {
await spot.query('fetch-user', { userId: 'id-two' }, ['users', 'id-two']);

const user = spot.get(['users', 'id-two']);
expect(user).toStrictEqual({ age: 3, name: 'Rufus', role: 'Home Security' });
expect(user).toMatchObject({ age: 3, name: 'Rufus', role: 'Home Security' });

const sameuser = (spot.data).users['id-two'];
expect(sameuser).toStrictEqual({ age: 3, name: 'Rufus', role: 'Home Security' });
expect(sameuser).toMatchObject({ age: 3, name: 'Rufus', role: 'Home Security' });
});

it('Can delete data', async () => {
Expand All @@ -179,7 +189,7 @@ describe('spot', () => {
loading: false,
};

expect(spot.data).toStrictEqual(expectedResult);
expect(spot.data).toMatchObject(expectedResult);
}

{
Expand All @@ -194,7 +204,24 @@ describe('spot', () => {
loading: false,
};

expect(spot.data).toStrictEqual(expectedResult);
expect(spot.data).toMatchObject(expectedResult);
}
});

it('Can run multiple queries with the subscribe once', async () => {
const spot = initializeSpot<DataType>(baseUrl);

spot.query('fetch-user', { userId: 'id-two' }, ['users', 'id-two']);
spot.query('fetch-user', { userId: 'id-one' }, ['users', 'id-one']);

expect(spot.data.loading).toBe(true);
expect(Object.keys(spot.data.spot.active)).toHaveLength(2);

await waitForLoadingDone(spot);

expect(Object.keys(spot.data.spot.active)).toHaveLength(0);
expect(spot.data.loading).toBe(false);
expect(spot.data.users['id-two']).toMatchObject({ age: 3, name: 'Rufus', role: 'Home Security' });
expect(spot.data.users['id-one']).toMatchObject({ age: 7, name: 'Spot', role: 'Good Boy' });
});
});
20 changes: 14 additions & 6 deletions src/fetch-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const fetchMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => async
action.payload.endpoint,
action.payload.params,
);

const correlationId = action.metadata?.correlationId;
try {
const response = await fetch(url, {
...defaultFetchConfig,
Expand All @@ -49,20 +51,26 @@ export const fetchMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => async
api.dispatch({
type: 'QUERY_COMPLETE',
payload,
metadata: { path },
metadata: {
path,
correlationId,
},
});
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
api.dispatch({ type: 'ERROR', payload: e });
api.dispatch({ type: 'ERROR', payload: e, metadata: { correlationId } });
} finally {
api.dispatch({ type: 'STATE_UPDATED' });
api.dispatch({ type: 'STATE_UPDATED', metadata: { correlationId } });
}
} else if (action.type === 'COMMAND') {
const url = buildUrl(
api.getState().config.baseUrl,
action.payload.endpoint,
);

const correlationId = action.metadata?.correlationId;

try {
const response = await fetch(url, {
...defaultFetchConfig,
Expand All @@ -76,13 +84,13 @@ export const fetchMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => async
);
}

api.dispatch({ type: 'COMMAND_COMPLETE' });
api.dispatch({ type: 'COMMAND_COMPLETE', metadata: { correlationId } });
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
api.dispatch({ type: 'ERROR', payload: e });
api.dispatch({ type: 'ERROR', payload: e, metadata: { correlationId } });
} finally {
api.dispatch({ type: 'STATE_UPDATED' });
api.dispatch({ type: 'STATE_UPDATED', metadata: { correlationId } });
}
}

Expand Down
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import deepmerge from 'deepmerge';
import { Store } from 'redux';

import { uuidv4 } from './uuid';
import { makeStore } from './store';

import { Subscription, ActionConfig } from './types';

interface DataType {
loading: boolean;
spot: {
active: { [k: string]: boolean }
};
}

export class Spot<T = unknown> {
Expand Down Expand Up @@ -45,6 +49,9 @@ export class Spot<T = unknown> {
type: 'QUERY',
payload: { params, endpoint, path },
config,
metadata: {
correlationId: uuidv4(),
},
});
await this.waitForQuery();
}
Expand All @@ -54,6 +61,9 @@ export class Spot<T = unknown> {
type: 'COMMAND',
payload: { params, endpoint },
config,
metadata: {
correlationId: uuidv4(),
},
});
await this.waitForQuery();
}
Expand All @@ -62,7 +72,7 @@ export class Spot<T = unknown> {
const hash = subscription.toString();
this.subscriptions[`${hash}_once`] = (state) => {
subscription(state);
delete this.subscriptions[hash];
delete this.subscriptions[`${hash}_once`];
};
}

Expand Down
35 changes: 32 additions & 3 deletions src/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,51 @@ export function commandQuery(state: State, action: Action) {
delete current[segment];
}
});
return merge(state, action.payload);
const newState = merge(state, action.payload);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (newState as any).spot.active[`${action.metadata?.correlationId}`];
return newState;
}
case 'COMMAND_COMPLETE':
return {
{
const newState = {
...state,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (newState as any).spot.active[`${action.metadata?.correlationId}`];
return newState;
}
case 'QUERY':
return {
...state,
loading: true,
spot: {
...state.spot,
active: { ...state.spot.active, [`${action.metadata?.correlationId}`]: true },
},
};
case 'COMMAND':
return {
...state,
loading: true,
spot: {
...state.spot,
active: { ...state.spot.active, [`${action.metadata?.correlationId}`]: true },
},
};
case 'ERROR':
{
const newState = {
...state,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (newState as any).spot.active[`${action.metadata?.correlationId}`];
return newState;
}
case 'STATE_UPDATED':
return {
...state,
loading: false,
loading: false, // (Object.keys(state.spot.active).length > 0),
};
default:
return {
Expand Down
6 changes: 5 additions & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { fetchMiddleware } from './fetch-middleware';

export const makeStore = (debug = false) => createStore(
reducer,
{ config: { debug }, data: { loading: false }, errors: [] },
{
config: { debug },
data: { loading: false, spot: { active: {} } },
errors: [],
},
applyMiddleware(fetchMiddleware),
);
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Action {
payload?: any;
metadata?: {
path?: string[];
correlationId?: string;
},
config?: ActionConfig
}
Expand Down
15 changes: 15 additions & 0 deletions src/uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const uuidv4 = () => {
const hex = [...Array(256).keys()]
.map((index) => (index).toString(16).padStart(2, '0'));

const r = crypto.getRandomValues(new Uint8Array(16));

// eslint-disable-next-line no-bitwise
r[6] = (r[6] & 0x0f) | 0x40;
// eslint-disable-next-line no-bitwise
r[8] = (r[8] & 0x3f) | 0x80;

return [...r.entries()]
.map(([index, int]) => ([4, 6, 8, 10].includes(index) ? `-${hex[int]}` : hex[int]))
.join('');
};
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"noEmit": true,
"jsx": "react",
"baseUrl": "src",
"downlevelIteration": true,
"declaration": true
},
"include": [
Expand Down

0 comments on commit 886c4bb

Please sign in to comment.