Merge pull request #260 from pybricks/dlech

updates for beta 3
This commit is contained in:
David Lechner
2021-01-24 14:15:09 -06:00
committed by GitHub
16 changed files with 195 additions and 147 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pybricks/pybricks-code",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"license": "MIT",
"author": "The Pybricks Authors",
"repository": {
+2
View File
@@ -11,6 +11,7 @@ import { HubAction, HubMessageAction } from './hub';
import { LicenseAction } from './license';
import {
BootloaderConnectionAction,
BootloaderDidFailToRequestAction,
BootloaderDidRequestAction,
BootloaderRequestAction,
BootloaderResponseAction,
@@ -31,6 +32,7 @@ export type Action =
| BleUartAction
| BootloaderConnectionAction
| BootloaderDidRequestAction
| BootloaderDidFailToRequestAction
| BootloaderRequestAction
| BootloaderResponseAction
| EditorAction
+55 -14
View File
@@ -37,6 +37,10 @@ export enum BootloaderConnectionActionType {
* Finished sending a message.
*/
DidSend = 'bootloader.action.connection.did.send',
/**
* Sending a message failed with error.
*/
DidFailToSend = 'bootloader.action.connection.did.failToSend',
/**
* The connection received a message.
*/
@@ -152,12 +156,18 @@ export function send(
return { type: BootloaderConnectionActionType.Send, data, withResponse };
}
export type BootloaderConnectionDidSendAction = Action<BootloaderConnectionActionType.DidSend> & {
err?: Error;
export type BootloaderConnectionDidSendAction = Action<BootloaderConnectionActionType.DidSend>;
export function didSend(): BootloaderConnectionDidSendAction {
return { type: BootloaderConnectionActionType.DidSend };
}
export type BootloaderConnectionDidFailToSendAction = Action<BootloaderConnectionActionType.DidFailToSend> & {
err: Error;
};
export function didSend(err?: Error): BootloaderConnectionDidSendAction {
return { type: BootloaderConnectionActionType.DidSend, err };
export function didFailToSend(err: Error): BootloaderConnectionDidFailToSendAction {
return { type: BootloaderConnectionActionType.DidFailToSend, err };
}
export type BootloaderConnectionDidReceiveAction = Action<BootloaderConnectionActionType.DidReceive> & {
@@ -184,7 +194,7 @@ export type BootloaderConnectionAction =
| BootloaderConnectionDidErrorAction
| BootloaderConnectionSendAction
| BootloaderConnectionDidSendAction
| BootloaderConnectionDidSendAction
| BootloaderConnectionDidFailToSendAction
| BootloaderConnectionDidReceiveAction
| BootloaderConnectionDidDisconnectAction;
@@ -355,26 +365,57 @@ export type BootloaderDidRequestType = 'bootloader.action.did.request';
export const BootloaderDidRequestType = 'bootloader.action.did.request';
/**
* Action that indicates a request was sent or failed to send.
* Action that indicates a request was sent.
*/
export type BootloaderDidRequestAction = Action<BootloaderDidRequestType> & {
/**
* The unique identifier of the action.
*/
id: number;
/**
* The error on failure or undefined on success.
*/
err?: Error;
};
/**
* Creates an action that indicates a request was sent or failed to send.
* Creates an action that indicates a request was sent.
* @param id The unique identifier of the action.
* @param err The error message on failure or undefined on success.
*/
export function didRequest(id: number, err?: Error): BootloaderDidRequestAction {
return { type: BootloaderDidRequestType, id, err };
export function didRequest(id: number): BootloaderDidRequestAction {
return { type: BootloaderDidRequestType, id };
}
/**
* Action type for bootloader did fail to request action.
*/
export type BootloaderDidFailToRequestType = 'bootloader.action.did.failToRequest';
/**
* Action type for bootloader did fail to request action.
*/
export const BootloaderDidFailToRequestType = 'bootloader.action.did.failToRequest';
/**
* Action that indicates a request failed to send.
*/
export type BootloaderDidFailToRequestAction = Action<BootloaderDidFailToRequestType> & {
/**
* The unique identifier of the action.
*/
id: number;
/**
* The error.
*/
err: Error;
};
/**
* Creates an action that indicates a request failed to send.
* @param id The unique identifier of the action.
* @param err The error message.
*/
export function didFailToRequest(
id: number,
err: Error,
): BootloaderDidFailToRequestAction {
return { type: BootloaderDidFailToRequestType, id, err };
}
/**
+25 -1
View File
@@ -16,6 +16,7 @@ import { IAceEditor } from 'react-ace/lib/types';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { setEditSession, storageChanged } from '../actions/editor';
import { compile } from '../actions/mpy';
import { RootState } from '../reducers';
import { isMacOS } from '../utils/os';
import { EditorStringId } from './editor-i18n';
@@ -38,6 +39,7 @@ type StateProps = {
type DispatchProps = {
onSessionChanged: (session?: Ace.EditSession) => void;
onProgramStorageChanged: (newValue: string) => void;
onCheck: (script: string) => void;
};
type EditorProps = StateProps & DispatchProps & WithI18nProps;
@@ -76,7 +78,7 @@ class Editor extends React.Component<EditorProps> {
}
render(): JSX.Element {
const { darkMode, i18n, onSessionChanged } = this.props;
const { darkMode, i18n, onSessionChanged, onCheck } = this.props;
return (
<div className="h-100">
<ResizeSensor onResize={(): void => this.editor?.resize()}>
@@ -97,12 +99,19 @@ class Editor extends React.Component<EditorProps> {
enableSnippets: true,
}}
onLoad={(e): void => {
// default binding is F2 which conflicts with 'check'
e.commands.byName['toggleFoldWidget'].bindKey = {
win: 'Shift-F2',
mac: 'Shift-F2',
};
config.loadModule(
'ace/ext/menu_tools/get_editor_keyboard_shortcuts',
(m) => {
this.keyBindings = m.getEditorKeybordShortcuts(e);
},
);
config.loadModule('ace/ext/keybinding_menu', (m) =>
m.init(e),
);
@@ -114,6 +123,12 @@ class Editor extends React.Component<EditorProps> {
localStorage.setItem('program', v);
}}
commands={[
{
// command to check current program for errors
name: 'check',
bindKey: { win: 'F2', mac: 'F2' },
exec: (editor) => onCheck(editor.getValue()),
},
{
name: 'save',
bindKey: { win: 'Ctrl-S', mac: 'Cmd-S' },
@@ -158,6 +173,12 @@ class Editor extends React.Component<EditorProps> {
icon="clipboard"
label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'}
/>
<MenuItem
onClick={() => this.editor?.selectAll()}
text={i18n.translate(EditorStringId.SelectAll)}
icon="blank"
label={isMacOS() ? 'Cmd-A' : 'Ctrl-A'}
/>
<MenuDivider />
<MenuItem
onClick={(): void => this.editor?.undo()}
@@ -186,6 +207,9 @@ const mapStateToProps = (state: RootState): StateProps => ({
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onSessionChanged: (s): Action => dispatch(setEditSession(s)),
onProgramStorageChanged: (v): Action => dispatch(storageChanged(v)),
// REVISIT: the options here might need to be changed - hopefully there is
// one setting that works for all hub types for cases where we aren't connected.
onCheck: (script) => dispatch(compile(script)),
});
export default connect(
+30 -25
View File
@@ -100,33 +100,38 @@ class OpenFileButton extends React.Component<Props> {
position={Position.BOTTOM}
hoverOpenDelay={tooltipDelay}
>
<Button
<div
{...getRootProps()}
intent={Intent.PRIMARY}
disabled={this.props.enabled === false}
className="no-box-shadow"
style={
this.props.enabled === false
? { pointerEvents: 'none' }
: undefined
}
// onClick={this.props.onClick}
// breaks Dropzone when this.props.onClick is undefined
// so we have to do it the long way
{...(this.props.onClick
? { onClick: this.props.onClick }
: {})}
tabIndex={-1}
className="pb-open-file-button-root"
>
<input {...getInputProps()} />
{this.props.showProgress ? (
<Spinner
value={this.props.progress}
intent={Intent.PRIMARY}
/>
) : (
<img src={this.props.icon} alt={this.props.id} />
)}
</Button>
<Button
intent={Intent.PRIMARY}
disabled={this.props.enabled === false}
className="no-box-shadow"
style={
this.props.enabled === false
? { pointerEvents: 'none' }
: undefined
}
// onClick={this.props.onClick}
// breaks Dropzone when this.props.onClick is undefined
// so we have to do it the long way
{...(this.props.onClick
? { onClick: this.props.onClick }
: {})}
>
<input {...getInputProps()} />
{this.props.showProgress ? (
<Spinner
value={this.props.progress}
intent={Intent.PRIMARY}
/>
) : (
<img src={this.props.icon} alt={this.props.id} />
)}
</Button>
</div>
</Tooltip>
)}
</Dropzone>
+5
View File
@@ -149,6 +149,11 @@ class Terminal extends React.Component<TerminalProps> {
icon="clipboard"
label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'}
/>
<MenuItem
onClick={() => this.xterm.selectAll()}
text={i18n.translate(TerminalStringId.SelectAll)}
icon="blank"
/>
<MenuDivider />
<MenuItem
onClick={(): void => this.xterm.clear()}
+1
View File
@@ -3,6 +3,7 @@
"placeholder": "Write your program here...",
"copy": "Copy",
"paste": "Paste",
"selectAll": "Select All",
"undo": "Undo",
"redo": "Redo"
}
+1
View File
@@ -7,6 +7,7 @@ export enum EditorStringId {
Placeholder = 'editor.placeholder',
Copy = 'editor.copy',
Paste = 'editor.paste',
SelectAll = 'editor.selectAll',
Undo = 'editor.undo',
Redo = 'editor.redo',
}
+21 -91
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
@@ -10,19 +10,20 @@
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 50 50"
enable-background="new 0 0 20 20"
xml:space="preserve"
sodipodi:docname="settings.svg"
width="50"
height="50"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata
id="metadata41"><rdf:RDF><cc:Work
id="metadata12"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs39" /><sodipodi:namedview
id="defs10" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
@@ -31,101 +32,30 @@
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2089"
inkscape:window-height="1160"
id="namedview37"
inkscape:window-width="1480"
inkscape:window-height="1046"
id="namedview8"
showgrid="false"
inkscape:pagecheckerboard="true"
inkscape:zoom="15.418475"
inkscape:cx="21.7408"
inkscape:cy="28.83975"
inkscape:zoom="11.8"
inkscape:cx="24.88532"
inkscape:cy="16.779661"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="Capa_1" />
inkscape:current-layer="Layer_1" />
<g
id="cog_2_"
transform="matrix(2.25,0,0,2.25,2.5,2.5)"
style="fill:#ffffff;fill-opacity:1">
<g
id="g4"
transform="matrix(0.08166572,0,0,0.08166572,4.9999835,4.9999835)"
style="fill:#ffffff;fill-opacity:0.90707964">
<path
d="m 20.701,281.901 32.1,0.2 c 4.8,24.7 14.3,48.7 28.7,70.5 l -22.8,22.6 c -8.2,8.1 -8.2,21.2 -0.2,29.4 l 24.6,24.9 c 8.1,8.2 21.2,8.2 29.4,0.2 l 22.8,-22.6 c 21.6,14.6 45.5,24.5 70.2,29.5 l -0.2,32.1 c -0.1,11.5 9.2,20.8 20.7,20.9 l 35,0.2 c 11.5,0.1 20.8,-9.2 20.9,-20.7 l 0.2,-32.1 c 24.7,-4.8 48.7,-14.3 70.5,-28.7 l 22.6,22.8 c 8.1,8.2 21.2,8.2 29.4,0.2 l 24.9,-24.6 c 8.2,-8.1 8.2,-21.2 0.2,-29.4 l -22.6,-22.8 c 14.6,-21.6 24.5,-45.5 29.5,-70.2 l 32.1,0.2 c 11.5,0.1 20.8,-9.2 20.9,-20.7 l 0.2,-35 c 0.1,-11.5 -9.2,-20.8 -20.7,-20.9 l -32.1,-0.2 c -4.8,-24.7 -14.3,-48.7 -28.7,-70.5 l 22.8,-22.6 c 8.2,-8.1 8.2,-21.2 0.2,-29.4 l -24.6,-24.9 c -8.1,-8.2 -21.2,-8.2 -29.4,-0.2 l -22.8,22.6 c -21.6,-14.6 -45.5,-24.5 -70.2,-29.5 l 0.2,-32.1 c 0.1,-11.5 -9.2,-20.8 -20.7,-20.9 l -35,-0.2 c -11.5,-0.1 -20.8,9.2 -20.9,20.7 l -0.3,32.1 c -24.8,4.8 -48.8,14.3 -70.5,28.7 l -22.6,-22.8 c -8.1,-8.2 -21.2,-8.2 -29.4,-0.2 l -24.8,24.6 c -8.2,8.1 -8.2,21.2 -0.2,29.4 l 22.6,22.8 c -14.6,21.6 -24.5,45.5 -29.5,70.2 l -32.1,-0.2 c -11.5,-0.1 -20.8,9.2 -20.9,20.7 l -0.2,35 c -0.1,11.4 9.2,20.8 20.7,20.9 z m 158.6,-103.3 c 36.6,-36.2 95.5,-35.9 131.7,0.7 36.2,36.6 35.9,95.5 -0.7,131.7 -36.6,36.2 -95.5,35.9 -131.7,-0.7 -36.2,-36.6 -35.9,-95.5 0.7,-131.7 z"
style="fill:#ffffff;fill-opacity:1">
<path
d="M 19,8 H 16.69 C 16.55,7.54 16.36,7.11 16.13,6.7 L 17.83,5 c 0.39,-0.39 0.39,-1.02 0,-1.41 L 16.42,2.18 c -0.39,-0.39 -1.02,-0.39 -1.41,0 l -1.7,1.7 C 12.9,3.66 12.47,3.47 12.01,3.33 V 1 c 0,-0.55 -0.45,-1 -1,-1 H 9 C 8.45,0 8,0.45 8,1 V 3.33 C 7.52,3.47 7.06,3.67 6.63,3.91 L 5,2.28 C 4.63,1.91 4.02,1.91 3.64,2.28 L 2.28,3.64 C 1.91,4.02 1.91,4.63 2.28,5 L 3.9,6.62 C 3.66,7.06 3.46,7.51 3.31,8 H 1 C 0.45,8 0,8.45 0,9 v 2 c 0,0.55 0.45,1 1,1 h 2.31 c 0.14,0.46 0.33,0.89 0.56,1.3 L 2.17,15 c -0.39,0.39 -0.39,1.02 0,1.41 l 1.41,1.41 c 0.39,0.39 1.02,0.39 1.41,0 l 1.7,-1.7 c 0.41,0.22 0.84,0.41 1.3,0.55 V 19 c 0,0.55 0.45,1 1,1 h 2 c 0.55,0 1,-0.45 1,-1 v -2.33 c 0.48,-0.14 0.94,-0.35 1.37,-0.59 L 15,17.72 c 0.37,0.37 0.98,0.37 1.36,0 l 1.36,-1.36 c 0.37,-0.37 0.37,-0.98 0,-1.36 L 16.1,13.38 C 16.34,12.95 16.55,12.49 16.7,12 H 19 c 0.55,0 1,-0.45 1,-1 V 9 C 20,8.45 19.55,8 19,8 Z m -9,6 C 7.79,14 6,12.21 6,10 6,7.79 7.79,6 10,6 c 2.21,0 4,1.79 4,4 0,2.21 -1.79,4 -4,4 z"
id="path2"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:0.90707964" />
</g>
<g
id="g6"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g8"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g10"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g12"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g14"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g16"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g18"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g20"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g22"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g24"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g26"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g28"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g30"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g32"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
</g>
<g
id="g34"
transform="matrix(0.08166572,0,0,0.08166572,224.90098,-214.90102)"
style="fill:#ffffff;fill-opacity:0.90707964">
style="clip-rule:evenodd;fill-rule:evenodd;fill:#ffffff;fill-opacity:1" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

+1
View File
@@ -2,6 +2,7 @@
"terminal": {
"copy": "Copy",
"paste": "Paste",
"selectAll": "Select All",
"clear": "Clear"
}
}
+1
View File
@@ -6,5 +6,6 @@
export enum TerminalStringId {
Copy = 'terminal.copy',
Paste = 'terminal.paste',
SelectAll = 'terminal.selectAll',
Clear = 'terminal.clear',
}
+5 -1
View File
@@ -40,7 +40,11 @@ body {
// global style tweaks
.#{$ns}-dialog {
.#{$ns}-toast {
user-select: text;
}
.#{$ns}-button {
user-select: none;
}
+3 -2
View File
@@ -27,6 +27,7 @@ import {
didConnect,
didDisconnect,
didFailToConnect,
didFailToRequest,
didRequest,
disconnect,
eraseRequest,
@@ -329,7 +330,7 @@ describe('flashFirmware', () => {
// On city hub, we can end up in this situation. BLE writeValueWithResponse()
// doesn't return until erasing is done, so there is a long window for
// this to happen.
saga.put(didRequest(0, new Error('failed due to disconnect')));
saga.put(didFailToRequest(0, new Error('failed due to disconnect')));
await saga.end();
});
@@ -387,7 +388,7 @@ describe('flashFirmware', () => {
expect(action).toEqual(infoRequest(0));
const testError = new Error('test');
saga.put(didRequest(0, testError));
saga.put(didFailToRequest(0, testError));
// should get a failure to start
+18 -6
View File
@@ -34,6 +34,8 @@ import {
BootloaderChecksumResponseAction,
BootloaderConnectionAction,
BootloaderConnectionActionType,
BootloaderDidFailToRequestAction,
BootloaderDidFailToRequestType,
BootloaderDidRequestAction,
BootloaderDidRequestType,
BootloaderEraseResponseAction,
@@ -85,15 +87,25 @@ function* disconnectAndCancel(): SagaGenerator<void> {
}
function* waitForDidRequest(id: number): SagaGenerator<BootloaderDidRequestAction> {
const request = yield* take<BootloaderDidRequestAction>(
(a: Action) => a.type === BootloaderDidRequestType && a.id === id,
);
if (request.err) {
yield* put(didFailToFinish(FailToFinishReasonType.BleError, request.err));
const { requested, failedToRequest } = yield* race({
requested: take<BootloaderDidRequestAction>(
(a: Action) => a.type === BootloaderDidRequestType && a.id === id,
),
failedToRequest: take<BootloaderDidFailToRequestAction>(
(a: Action) => a.type === BootloaderDidFailToRequestType && a.id === id,
),
});
if (failedToRequest) {
yield* put(
didFailToFinish(FailToFinishReasonType.BleError, failedToRequest.err),
);
yield* disconnectAndCancel();
}
return request;
defined(requested);
return requested;
}
/**
+2 -1
View File
@@ -13,6 +13,7 @@ import {
didConnect,
didDisconnect,
didFailToConnect,
didFailToSend,
didReceive,
didSend,
} from '../actions/lwp3-bootloader';
@@ -34,7 +35,7 @@ function* write(
}
yield* put(didSend());
} catch (err) {
yield* put(didSend(err));
yield* put(didFailToSend(err));
}
}
+24 -5
View File
@@ -3,16 +3,25 @@
// File: sagas/lwp3-bootloader-protocol.ts
// Handles LEGO Wireless Protocol v3 Bootloader protocol.
import { actionChannel, fork, put, take, takeEvery } from 'typed-redux-saga/macro';
import {
actionChannel,
fork,
put,
race,
take,
takeEvery,
} from 'typed-redux-saga/macro';
import { Action } from '../actions';
import {
BootloaderConnectionActionType,
BootloaderConnectionDidFailToSendAction,
BootloaderConnectionDidReceiveAction,
BootloaderConnectionDidSendAction,
BootloaderRequestAction,
BootloaderRequestActionType,
checksumResponse,
didError,
didFailToRequest,
didRequest,
eraseResponse,
errorResponse,
@@ -104,10 +113,20 @@ function* encodeRequest(): Generator {
continue;
}
const sent = yield* take<BootloaderConnectionDidSendAction>(
BootloaderConnectionActionType.DidSend,
);
yield* put(didRequest(action.id, sent.err));
const { failedToSend } = yield* race({
sent: take<BootloaderConnectionDidSendAction>(
BootloaderConnectionActionType.DidSend,
),
failedToSend: take<BootloaderConnectionDidFailToSendAction>(
BootloaderConnectionActionType.DidFailToSend,
),
});
if (failedToSend) {
yield* put(didFailToRequest(action.id, failedToSend.err));
} else {
yield* put(didRequest(action.id));
}
}
}