async-selector-kit
Version:
An opinionated API to simplify using async-selector
907 lines (850 loc) • 26 kB
JavaScript
import {
createAsyncSelectorResults,
throttleSelectorResults,
createSelector,
createAsyncSelectorWithCache,
createThrottledSelectorResults,
createAsyncAction,
createMiddleware,
ACTION_STARTED,
ACTION_FINISHED
} from "../dist/index";
/* random underscore functions */
const _ = {};
var restArguments = function (func, startIndex) {
startIndex = startIndex == null ? func.length - 1 : +startIndex;
return function () {
var length = Math.max(arguments.length - startIndex, 0);
var rest = Array(length);
var index = 0;
for (; index < length; index++) {
rest[index] = arguments[index + startIndex];
}
switch (startIndex) {
case 0:
return func.call(this, rest);
case 1:
return func.call(this, arguments[0], rest);
case 2:
return func.call(this, arguments[0], arguments[1], rest);
}
var args = Array(startIndex + 1);
for (index = 0; index < startIndex; index++) {
args[index] = arguments[index];
}
args[startIndex] = rest;
return func.apply(this, args);
};
};
_.delay = restArguments(function (func, wait, args) {
return setTimeout(function () {
return func.apply(null, args);
}, wait);
});
_.now =
Date.now ||
function () {
return new Date().getTime();
};
_.debounce = function (func, wait, immediate) {
var timeout, result;
var later = function (context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArguments(function (args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
_.throttle = function (func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function () {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function () {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function () {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
test("Tests Work", done => {
expect(true).toBe(true);
done();
});
function makeMiddleware(dispatcher) {
createMiddleware()({ dispatch: dispatcher });
}
test("Basic createAsyncSelectorResults example", done => {
let state = {
text: "hello"
};
let generatedAction;
const dispatch = action => {
generatedAction = action;
};
makeMiddleware(dispatch);
let count = 0;
const [getValue, waiting, error] = createAsyncSelectorResults(
{
async: async function exampleApi(text) {
await new Promise(res => setTimeout(res, 25));
count += 1;
return "hi " + text;
}
},
[state => state.text]
);
expect(getValue({ ...state })).toEqual([]);
expect(getValue({ ...state })).toEqual([]);
expect(waiting({ ...state })).toEqual(true);
expect(waiting({ ...state })).toEqual(true);
expect(error({ ...state })).toEqual(null);
setTimeout(() => {
try {
expect(getValue({ ...state })).toBe("hi hello");
expect(getValue({ ...state })).toBe("hi hello");
expect(waiting({ ...state })).toEqual(false);
expect(error({ ...state })).toEqual(null);
expect(count).toBe(1);
expect(generatedAction.result).toBe("hi hello");
state = { ...state, text: "new" };
expect(getValue({ ...state })).toEqual("hi hello");
expect(waiting({ ...state })).toEqual(true);
expect(error({ ...state })).toEqual(null);
setTimeout(() => {
expect(getValue({ ...state })).toEqual("new hello");
expect(generatedAction.result).toBe("new hello");
expect(waiting({ ...state })).toEqual(false);
expect(error({ ...state })).toEqual(null);
}, 50);
} catch (e) {
done.fail(e);
}
done();
}, 50);
});
test("createAsyncSelectorResults onResolve, onReject, onCancel", done => {
let state = {
text: "hello"
};
let generatedAction;
const dispatch = action => {
generatedAction = action;
};
makeMiddleware(dispatch);
let count = 0;
let resolved, rejected, cancelled, cancelledText, cancelledBool
const [getValue, waiting, error] = createAsyncSelectorResults(
{
async: async function exampleApi(text, status) {
status.onCancel = () => {
cancelledText = text;
cancelledBool = status.cancelled;
}
if (text.length > 10) {
throw new Error("Text to long");
}
await new Promise(res => setTimeout(res, 25));
count += 1;
return "hi " + text;
},
onResolve: val => {
resolved = val;
},
onReject: val => {
rejected = val;
},
onCancel: val => {
cancelled = val;
},
defaultValue: null
},
[state => state.text]
);
expect(getValue({ ...state })).toBe(null);
expect(count).toBe(0);
expect(cancelled || resolved || rejected).toBeFalsy();
expect(cancelledBool).toBe(undefined);
expect(cancelledText).toBe(undefined);
setTimeout(() => {
expect(getValue({ ...state })).toBe("hi hello");
expect(getValue({ ...state })).toBe("hi hello");
expect(waiting({ ...state })).toEqual(false);
expect(error({ ...state })).toEqual(null);
expect(count).toBe(1);
expect(generatedAction.result).toBe("hi hello");
expect(resolved).toBe("hi hello");
state = { ...state, text: "new" };
expect(getValue({ ...state })).toBe("hi hello");
expect(waiting({ ...state })).toBe(true);
expect(Boolean(cancelled)).toBe(false);
state = { ...state, text: "new2" };
expect(getValue({ ...state })).toBe("hi hello");
expect(cancelledBool).toBe(true);
expect(cancelledText).toBe('new');
expect(waiting({ ...state })).toBe(true);
expect(Boolean(cancelled.then)).toBe(true);
state = { ...state, text: "errorerrorerror" };
expect(getValue({ ...state })).toBe("hi hello");
expect(waiting({ ...state })).toBe(true);
expect(cancelledBool).toBe(true);
expect(cancelledText).toBe('new2');
setTimeout(() => {
try {
expect(getValue({ ...state })).toBe("hi hello");
expect(waiting({ ...state })).toBe(false);
expect(String(error({ ...state }))).toBe("Error: Text to long");
expect(cancelled.then).toBeTruthy();
expect(String(rejected)).toBe("Error: Text to long");
expect(String(generatedAction.error)).toBe("Error: Text to long");
expect(cancelledBool).toBe(true);
expect(cancelledText).toBe('new2');
done();
} catch (e) {
done.fail(e);
}
}, 50);
}, 50);
});
test("createAsyncSelectorResults forceUpdate", done => {
let state = {
text: "hello"
};
let generatedAction;
const dispatch = action => {
generatedAction = action;
};
makeMiddleware(dispatch);
let count = 0;
let concat = "hi ";
const [getValue, waiting, error, forceUpdate] = createAsyncSelectorResults(
{
async: async function exampleApi(text) {
if (text.length > 10) {
throw new Error("Text to long");
}
await new Promise(res => setTimeout(res, 25));
count += 1;
return concat + text;
}
},
[state => state.text]
);
expect(getValue({ ...state })).toEqual([]);
setTimeout(() => {
try {
expect(getValue({ ...state })).toEqual("hi hello");
expect(count).toBe(1);
concat = "new ";
forceUpdate({ ...state });
setTimeout(() => {
try {
expect(getValue({ ...state })).toEqual("new hello");
expect(count).toBe(2);
done();
} catch (e) {
done.fail(e);
}
}, 50);
} catch (e) {
done.fail(e);
}
}, 50);
});
test("createAsyncSelectorResults forceUpdate promise", done => {
let state = {
text: "hello"
};
let generatedAction;
const dispatch = action => {
generatedAction = action;
};
makeMiddleware(dispatch);
let count = 0;
let concat = "hi ";
const [getValue, waiting, error, forceUpdate] = createAsyncSelectorResults(
{
async: async function exampleApi(text) {
if (text.length > 10) {
throw new Error("Text to long");
}
await new Promise(res => setTimeout(res, 25));
count += 1;
return concat + text;
}
},
[state => state.text]
);
console.log('hrumph');
try {
expect(getValue({ ...state })).toEqual([]);
expect(getValue({ ...state })).toEqual([]);
expect(waiting()).toEqual(true);
setTimeout(() => {
try {
expect(getValue({ ...state })).toEqual('hi hello');
expect(waiting()).toEqual(false);
const promise = forceUpdate(state);
let result = null;
promise.then(text => {
result = text;
})
setTimeout(() => {
try {
expect(result).toEqual(null)
setTimeout(() => {
try {
expect(result).toEqual('hi hello')
done();
} catch (e) {
done.fail(e);
}
}, 25);
} catch (e) {
done.fail(e);
}
}, 10);
} catch (e) {
done.fail(e);
}
}, 35);
} catch (e) {
done.fail(e);
}
});
test("createAsyncSelectorResults throttle shouldUseAsync", done => {
let state = {
text: "hello"
};
makeMiddleware(action => null);
let count = 0;
const [getValue, waiting, error] = createAsyncSelectorResults(
{
async: async function exampleApi(text) {
if (text.length > 10) {
throw new Error("Text to long");
}
await new Promise(res => setTimeout(res, 25));
count += 1;
return "hi " + text;
},
shouldUseAsync: text => text.length <= 10,
throttle: f => _.debounce(f, 200)
},
[state => state.text]
);
expect(getValue({ ...state })).toEqual([]);
expect(count).toBe(0);
setTimeout(() => {
try {
expect(getValue({ ...state })).toEqual([]);
expect(getValue({ ...state })).toEqual([]);
expect(waiting({ ...state })).toEqual(true);
expect(error({ ...state })).toEqual(null);
setTimeout(() => {
try {
state = { ...state, text: "new" };
expect(getValue({ ...state })).toEqual([]);
expect(getValue({ ...state })).toEqual([]);
expect(waiting({ ...state })).toEqual(true);
expect(error({ ...state })).toEqual(null);
setTimeout(() => {
try {
state = { ...state, text: "new" };
expect(getValue({ ...state })).toEqual([]);
expect(getValue({ ...state })).toEqual([]);
expect(waiting({ ...state })).toEqual(true);
expect(error({ ...state })).toEqual(null);
setTimeout(() => {
try {
state = { ...state, text: "new" };
expect(getValue({ ...state })).toEqual("hi new");
expect(getValue({ ...state })).toEqual("hi new");
expect(waiting({ ...state })).toEqual(false);
expect(error({ ...state })).toEqual(null);
setTimeout(() => {
try {
state = { ...state, text: "errorerrorerror" };
expect(getValue({ ...state })).toEqual("hi new");
expect(getValue({ ...state })).toEqual("hi new");
expect(waiting({ ...state })).toEqual(true);
expect(error({ ...state })).toEqual(null);
setTimeout(() => {
try {
state = { ...state, text: "errorerrorerror" };
expect(getValue({ ...state })).toEqual("hi new");
expect(getValue({ ...state })).toEqual("hi new");
expect(waiting({ ...state })).toEqual(true);
expect(error({ ...state })).toEqual(null);
done();
} catch (e) {
setTimeout(() => {
try {
state = { ...state, text: "new2" };
expect(getValue({ ...state })).toEqual("hi new");
expect(getValue({ ...state })).toEqual("hi new");
expect(waiting({ ...state })).toEqual(true);
expect(error({ ...state })).toEqual(null);
setTimeout(() => {
try {
state = { ...state, text: "new2" };
expect(getValue({ ...state })).toEqual(
"hi new2"
);
expect(getValue({ ...state })).toEqual(
"hi new2"
);
expect(waiting({ ...state })).toEqual(false);
expect(error({ ...state })).toEqual(null);
done();
} catch (e) {
done.fail(e);
}
}, 50);
} catch (e) {
done.fail(e);
}
}, 50);
}
}, 300);
} catch (e) {
done.fail(e);
}
}, 50);
} catch (e) {
done.fail(e);
}
}, 100);
} catch (e) {
done.fail(e);
}
}, 150);
} catch (e) {
done.fail(e);
}
}, 50);
} catch (e) {
done.fail(e);
}
}, 50);
});
test("throttleSelectorResults", done => {
let state = {
text: "hello"
};
const selector = createSelector([state => state.text], text => {
return text + " hi";
});
const [getValue, waiting] = throttleSelectorResults(selector, f =>
_.debounce(f, 100)
);
expect(waiting()).toBe(false);
expect(getValue({ ...state })).toBe("hello hi");
expect(waiting({ ...state, text: "new1" })).toBe(true);
expect(getValue({ ...state, text: "new1" })).toBe("hello hi");
expect(waiting({ ...state, text: "new2" })).toBe(true);
expect(getValue({ ...state, text: "new2" })).toBe("hello hi");
setTimeout(() => {
expect(waiting({ ...state, text: "new2" })).toBe(true);
expect(getValue({ ...state, text: "new2" })).toBe("hello hi");
setTimeout(() => {
try {
expect(waiting({ ...state, text: "new2" })).toBe(false);
expect(getValue({ ...state, text: "new2" })).toBe("new2 hi");
expect(waiting({ ...state, text: "new3" })).toBe(true);
expect(getValue({ ...state, text: "new3" })).toBe("new2 hi");
expect(waiting({ ...state, text: "new3" })).toBe(true);
done();
} catch (e) {
done.fail(e);
}
}, 100);
}, 50);
});
test("createThrottledSelectorResults", done => {
let callCount = 0;
const [getValue, waiting] = createThrottledSelectorResults(
[state => state.text],
text => {
callCount += 1;
return text + "!";
},
f => _.debounce(f, 100)
);
expect(callCount).toBe(0);
expect(getValue({ text: "hello" })).toBe("hello!");
expect(getValue({ text: "new1" })).toBe("hello!");
expect(waiting({ text: "new1" })).toBe(true);
setTimeout(() => {
expect(getValue({ text: "new1" })).toBe("hello!");
expect(waiting({ text: "new1" })).toBe(true);
expect(callCount).toBe(1);
setTimeout(() => {
expect(getValue({ text: "new1" })).toBe("new1!");
expect(waiting({ text: "new1" })).toBe(false);
expect(callCount).toBe(2);
expect(getValue({ text: "new1" })).toBe("new1!");
expect(getValue({ text: "new1" })).toBe("new1!");
expect(callCount).toBe(2);
done();
}, 100);
}, 50);
});
test("createAsyncSelectorWithCache", done => {
class Api {
constructor() {
this.db = {
mark: "metzger",
project: "euler"
};
this.cache = {};
}
getCache(name) {
return this.cache[name];
}
async getName(name) {
await new Promise(res => setTimeout(res, 50));
const result = this.db[name];
this.cache[name] = result;
return result;
}
}
const api = new Api();
const [getValue, waiting, error] = createAsyncSelectorWithCache(
{
async: name => api.getName(name),
getCache: name => api.getCache(name),
defaultValue: null
},
[state => state.name]
);
try {
expect(getValue({ name: "project" })).toEqual(null);
expect(waiting({ name: "project" })).toEqual(true);
setTimeout(() => {
try {
expect(getValue({ name: "mark" })).toEqual("euler");
expect(waiting({ name: "mark" })).toEqual(true);
} catch (e) {
done.fail(e);
}
setTimeout(() => {
try {
expect(getValue({ name: "mark" })).toEqual("metzger");
expect(waiting({ name: "mark" })).toEqual(false);
} catch (e) {
done.fail(e);
}
setTimeout(() => {
try {
expect(getValue({ name: "project" })).toEqual("euler");
expect(waiting({ name: "project" })).toEqual(false);
expect(getValue({ name: "mark" })).toEqual("metzger");
expect(waiting({ name: "mark" })).toEqual(false);
} catch (e) {
done.fail(e);
}
done();
}, 100);
}, 100);
}, 100);
} catch (e) {
done.fail(e);
}
});
// test("createAsyncSelectorWithSubscription", done => {
// class ManageSubscription {
// constructor() {
// this.counter = 0;
// this.id = this.counter;
// }
// setNewIdCallback(callback) {
// this.callback = callback;
// }
// openSubscription() {
// this.interval = setInterval(() => {
// this.id = ++this.counter;
// this.callback(this.getId);
// }, 25);
// }
// closeSubscription() {
// clearInterval(this.interval);
// }
// getId() {
// return this.id;
// }
// }
// const manager = new ManageSubscription();
// const state = {
// text: "hello"
// };
// const [
// getValue,
// waiting,
// error,
// forceUpdate
// ] = createAsyncSelectorWithSubscription({
// async: async () => manager.getId(),
// onUnsubscribe: () => manager.closeSubscription(),
// inputsChanged: text => console.log(text)
// });
// manager.setNewIdCallback(() => forceUpdate());
// });
test("createAsyncAction", done => {
const state = {
name: "mark"
};
let cancelCount = 0;
let store_ = undefined;
let input_ = undefined;
const actions = [];
function createMiddlewareTest() {
const middleware = createMiddleware();
const store = {
getState: () => state,
dispatch: action => actions.push(action)
};
middleware(store)(store.dispatch)({ type: "test" });
}
createMiddlewareTest();
const [action, loading, error] = createAsyncAction({
id: "my-action",
async: (store, status) => async input => {
status.onCancel = () => {
cancelCount += 1;
};
store_ = store;
input_ = input;
await new Promise(res => setTimeout(res, 50));
return input;
}
});
expect(loading(state)).toEqual(false);
const status = action("hello");
expect(loading(state)).toEqual(true);
expect(error(state)).toEqual(undefined);
expect(status.cancelled).toEqual(false);
expect(typeof status.onCancel).toEqual("function");
setTimeout(() => {
try {
expect(typeof store_).toEqual("object");
expect(input_).toEqual("hello");
action("wow");
expect(loading(state)).toEqual(true);
expect(error(state)).toEqual(undefined);
expect(status.cancelled).toEqual(true);
expect(cancelCount).toEqual(1);
action("wow2");
expect(cancelCount).toEqual(2);
} catch (e) {
done.fail(e);
}
setTimeout(() => {
try {
expect(loading(state)).toEqual(true);
expect(error(state)).toEqual(undefined);
} catch (e) {
done.fail(e);
}
setTimeout(() => {
try {
expect(loading(state)).toEqual(false);
expect(actions[1].type).toEqual(ACTION_STARTED);
expect(actions[2].type).toEqual(ACTION_STARTED);
expect(actions[3].type).toEqual(ACTION_STARTED);
expect(actions[4].type).toEqual(ACTION_FINISHED);
expect(actions[4].result).toEqual("wow2");
done();
} catch (e) {
done.fail(e);
}
}, 20);
}, 40);
}, 25);
});
test("createAsyncAction should catch error", done => {
const errorThrowingFunc = async () => {
await new Promise(res => setTimeout(res, 50));
throw new Error("Expect this to be caught");
};
const [command, loading, error] = createAsyncAction({
id: "my-action",
async: (_store, _status) => async () => {
await errorThrowingFunc();
}
});
command();
expect(error()).toBeFalsy();
setTimeout(() => {
try {
expect(error()).toBeTruthy();
expect(loading()).toBeFalsy();
done();
} catch (e) {
done.fail(e);
}
}, 75);
});
test("createAsyncAction should work as middleware", done => {
const actions = [];
function createMiddlewareTest() {
const middleware = createMiddleware();
const store = {
getState: () => true,
dispatch: action => null
};
return action => middleware(store)(store.dispatch)(action);
}
const dispatch = createMiddlewareTest();
const [command, loading, error] = createAsyncAction({
id: "my-action",
async: (_store, _status) => async action => {
actions.push(action);
return true;
},
subscription: (action, store) => {
return action.type === "wow";
}
});
const action1 = { type: "wow" };
const action2 = { type: "omg" };
const action3 = { type: "wow" };
dispatch(action1);
setTimeout(() => {
dispatch(action2);
setTimeout(() => {
dispatch(action3);
setTimeout(() => {
try {
expect(actions.length).toEqual(2);
expect(actions[0]).toEqual(action1);
expect(actions[1]).toEqual(action3);
done();
} catch (e) {
done.fail(e);
}
}, 10);
}, 10);
}, 10);
});
test("createAsyncAction debounced", done => {
const state = {
name: "mark"
};
let cancelCount = 0;
let store_ = undefined;
let input_ = undefined;
const actions = [];
function createMiddlewareTest() {
const middleware = createMiddleware();
const store = {
getState: () => state,
dispatch: action => actions.push(action)
};
middleware(store)(store.dispatch)({ type: "test" });
}
createMiddlewareTest();
const [action, loading, error] = createAsyncAction({
id: "my-action",
async: (store, status) => async input => {
status.onCancel = () => {
cancelCount += 1;
};
store_ = store;
input_ = input;
await new Promise(res => setTimeout(res, 50));
return input;
},
throttle: f => _.debounce(f, 50)
});
expect(loading(state)).toEqual(false);
const status = action("hello");
expect(loading(state)).toEqual(false);
expect(error(state)).toEqual(undefined);
expect(status.cancelled).toEqual(false);
expect(typeof status.onCancel).toEqual("function");
setTimeout(() => {
// 25
try {
expect(store_).toEqual(undefined);
expect(input_).toEqual(undefined);
action("wow");
expect(loading(state)).toEqual(false);
expect(error(state)).toEqual(undefined);
expect(status.cancelled).toEqual(false);
expect(cancelCount).toEqual(0);
action("wow2");
expect(cancelCount).toEqual(0);
} catch (e) {
done.fail(e);
}
setTimeout(() => {
//40
try {
expect(loading(state)).toEqual(false);
expect(error(state)).toEqual(undefined);
} catch (e) {
done.fail(e);
}
setTimeout(() => {
//20
try {
expect(loading(state)).toEqual(true);
expect(actions[1].type).toEqual(ACTION_STARTED);
expect(actions[1].inputs[0]).toEqual("wow2");
setTimeout(() => {
// 50
try {
expect(loading(state)).toEqual(false);
expect(actions[2].type).toEqual(ACTION_FINISHED);
expect(actions[2].result).toEqual("wow2");
done();
} catch (e) {
done.fail(e);
}
}, 50);
} catch (e) {
done.fail(e);
}
}, 20);
}, 40);
}, 25);
});