From 218dda4d0b6b6843df0cb11b0e05886427465102 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 21 Jan 2021 22:22:46 -0600 Subject: [PATCH] use typed-redux-saga in flash-firmware sagas This make things a bit more type safe and a bit easier to read. --- package.json | 5 + src/sagas/flash-firmware.ts | 250 +++++++++++++++++------------------- src/utils/index.test.ts | 15 ++- src/utils/index.ts | 17 ++- yarn.lock | 127 +++++++++++++++++- 5 files changed, 267 insertions(+), 147 deletions(-) diff --git a/package.json b/package.json index bedd77a5..589073ec 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/web-bluetooth": "^0.0.9", "@types/zen-push": "^0.1.1", "ace-builds": "^1.4.12", + "babel-plugin-macros": "^3.0.1", "file-saver": "^2.0.5", "jszip": "^3.5.0", "license-webpack-plugin": "^2.3.11", @@ -45,6 +46,7 @@ "redux-logger": "^3.0.6", "redux-saga": "^1.1.3", "spdx-satisfies": "^5.0.0", + "typed-redux-saga": "^1.3.1", "typescript": "~4.1.3", "web-vitals": "^1.0.1", "xterm": "^4.9.0", @@ -80,9 +82,12 @@ "@typescript-eslint/parser": "^4.13.0", "eslint": "^7.17.0", "eslint-config-prettier": "^7.1.0", + "eslint-config-typed-fp": "^1.3.0", + "eslint-plugin-functional": "^3.2.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-react": "^7.22.0", + "eslint-plugin-total-functions": "^4.7.2", "jest-mock-extended": "^1.0.9", "prettier": "^2.2.1" } diff --git a/src/sagas/flash-firmware.ts b/src/sagas/flash-firmware.ts index 0ba93ab6..c6bc1ff1 100644 --- a/src/sagas/flash-firmware.ts +++ b/src/sagas/flash-firmware.ts @@ -1,19 +1,12 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020-2021 The Pybricks Authors -import { - FirmwareMetadata, - FirmwareReader, - FirmwareReaderError, - HubType, -} from '@pybricks/firmware'; +import { FirmwareReader, FirmwareReaderError, HubType } from '@pybricks/firmware'; import cityHubZip from '@pybricks/firmware/build/cityhub.zip'; import moveHubZip from '@pybricks/firmware/build/movehub.zip'; import technicHubZip from '@pybricks/firmware/build/technichub.zip'; -import { Ace } from 'ace-builds'; import { - Effect, - StrictEffect, + SagaGenerator, all, call, cancel, @@ -24,7 +17,7 @@ import { select, take, takeEvery, -} from 'redux-saga/effects'; +} from 'typed-redux-saga/macro'; import { Action } from '../actions'; import { FailToStartReasonType, @@ -36,24 +29,17 @@ import { didStart, } from '../actions/flash-firmware'; import { - BootloaderChecksumRequestAction, BootloaderChecksumResponseAction, BootloaderConnectionActionType, BootloaderConnectionDidConnectAction, BootloaderConnectionDidFailToConnectAction, BootloaderDidRequestAction, BootloaderDidRequestType, - BootloaderDisconnectRequestAction, - BootloaderEraseRequestAction, BootloaderEraseResponseAction, BootloaderErrorResponseAction, - BootloaderInfoRequestAction, BootloaderInfoResponseAction, - BootloaderInitRequestAction, BootloaderInitResponseAction, - BootloaderProgramRequestAction, BootloaderProgramResponseAction, - BootloaderRebootRequestAction, BootloaderResponseAction, BootloaderResponseActionType, checksumRequest, @@ -74,7 +60,7 @@ import { import * as notification from '../actions/notification'; import { MaxProgramFlashSize } from '../protocols/lwp3-bootloader'; import { RootState } from '../reducers'; -import { Maybe, maybe } from '../utils'; +import { defined, maybe } from '../utils'; import { fmod, sumComplement32 } from '../utils/math'; const firmwareZipMap = new Map([ @@ -83,25 +69,10 @@ const firmwareZipMap = new Map([ [HubType.MoveHub, moveHubZip], ]); -/** - * Helper type for return value of wait() function. - */ -type WaitResponse = [ - T, - BootloaderErrorResponseAction, - boolean, -]; - -function* waitForDidRequest(id: number): Generator { - const didRequest = (yield take( - (a: Action) => - a.type === BootloaderDidRequestType && - (a as BootloaderDidRequestAction).id === id, - )) as BootloaderDidRequestAction; - if (didRequest.err) { - console.error(didRequest.err); - } - return didRequest; +function* waitForDidRequest(id: number): SagaGenerator { + return yield* take( + (a: Action) => a.type === BootloaderDidRequestType && a.id === id, + ); } /** @@ -110,8 +81,19 @@ function* waitForDidRequest(id: number): Generator { * @param type The action type to wait for. * @param timeout The timeout in milliseconds. */ -function waitForResponse(type: BootloaderResponseActionType, timeout = 500): Effect { - return race([take(type), take(BootloaderResponseActionType.Error), delay(timeout)]); +function* waitForResponse( + type: BootloaderResponseActionType, + timeout = 500, +): SagaGenerator<{ + response?: T; + error?: BootloaderErrorResponseAction; + timeout?: boolean; +}> { + return yield* race({ + response: take(type), + error: take(BootloaderResponseActionType.Error), + timeout: delay(timeout), + }); } function* firmwareIterator(data: DataView, maxSize: number): Generator { @@ -133,27 +115,27 @@ function* firmwareIterator(data: DataView, maxSize: number): Generator { function* loadFirmware( data: ArrayBuffer, program: string | undefined, -): Generator { - const reader = (yield call(() => - maybe(FirmwareReader.load(data)), - )) as Maybe; +): SagaGenerator<{ firmware: Uint8Array; deviceId: HubType }> { + const [reader, readerErr] = yield* call(() => maybe(FirmwareReader.load(data))); - if (reader instanceof Error) { - if (reader instanceof FirmwareReaderError) { - yield put(didFailToStart(FailToStartReasonType.ZipError, reader)); + if (readerErr) { + // istanbul ignore else: unexpected error + if (readerErr instanceof FirmwareReaderError) { + yield* put(didFailToStart(FailToStartReasonType.ZipError, readerErr)); } else { - yield put(didFailToStart(FailToStartReasonType.Unknown, reader)); + yield* put(didFailToStart(FailToStartReasonType.Unknown, readerErr)); } - yield cancel(); - throw 'not reached'; + yield* cancel(); } - const firmwareBase = (yield call(() => reader.readFirmwareBase())) as Uint8Array; - const metadata = (yield call(() => reader.readMetadata())) as FirmwareMetadata; + defined(reader); + + const firmwareBase = yield* call(() => reader.readFirmwareBase()); + const metadata = yield* call(() => reader.readMetadata()); // if a user program was not given, then use main.py from the frimware.zip if (program === undefined) { - program = (yield call(() => reader.readMainPy())) as string; + program = yield* call(() => reader.readMainPy()); } if (metadata['mpy-abi-version'] !== 5) { @@ -162,16 +144,18 @@ function* loadFirmware( ); } - yield put(compile(program, metadata['mpy-cross-options'])); - const [mpy, mpyFail] = (yield race([ - take(MpyActionType.DidCompile), - take(MpyActionType.DidFailToCompile), - ])) as [MpyDidCompileAction, MpyDidFailToCompileAction]; + yield* put(compile(program, metadata['mpy-cross-options'])); + const { mpy, mpyFail } = yield* race({ + mpy: take(MpyActionType.DidCompile), + mpyFail: take(MpyActionType.DidFailToCompile), + }); if (mpyFail) { throw Error(mpyFail.err.join('\n')); } + defined(mpy); + // compute offset for checksum - must be aligned to 4-byte boundary const checksumOffset = metadata['user-mpy-offset'] + 4 + mpy.data.length + fmod(-mpy.data.length, 4); @@ -210,14 +194,12 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { let program: string | undefined = undefined; - const flashCurrentProgram = (yield select( + const flashCurrentProgram = yield* select( (s: RootState) => s.settings.flashCurrentProgram, - )) as boolean; + ); if (flashCurrentProgram) { - const editor = (yield select( - (s: RootState) => s.editor.current, - )) as Ace.EditSession | null; + const editor = yield* select((s: RootState) => s.editor.current); // istanbul ignore if: it is a bug to dispatch this action with no current editor if (editor === null) { @@ -232,109 +214,111 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { ({ firmware, deviceId } = yield* loadFirmware(action.data, program)); } - yield put(connect()); - const connectResult = (yield take([ + yield* put(connect()); + const connectResult = yield* take< + | BootloaderConnectionDidConnectAction + | BootloaderConnectionDidFailToConnectAction + >([ BootloaderConnectionActionType.DidConnect, BootloaderConnectionActionType.DidFailToConnect, - ])) as - | BootloaderConnectionDidConnectAction - | BootloaderConnectionDidFailToConnectAction; + ]); if (connectResult.type === BootloaderConnectionActionType.DidFailToConnect) { return; } - const nextMessageId = (yield getContext('nextMessageId')) as () => number; + const nextMessageId = yield* getContext<() => number>('nextMessageId'); - const infoAction = (yield put( - infoRequest(nextMessageId()), - )) as BootloaderInfoRequestAction; - const [, info] = (yield all([ - waitForDidRequest(infoAction.id), - waitForResponse(BootloaderResponseActionType.Info), - ])) as [BootloaderDidRequestAction, WaitResponse]; - if (!info[0]) { + const infoAction = yield* put(infoRequest(nextMessageId())); + const { info } = yield* all({ + sent: waitForDidRequest(infoAction.id), + info: waitForResponse( + BootloaderResponseActionType.Info, + ), + }); + if (!info.response) { throw Error(`failed to get info: ${info}`); } - if (deviceId !== undefined && info[0].hubType !== deviceId) { - throw Error(`Connected to ${info[0].hubType} but firmware is for ${deviceId}`); + if (deviceId !== undefined && info.response.hubType !== deviceId) { + throw Error( + `Connected to ${info.response.hubType} but firmware is for ${deviceId}`, + ); } if (firmware === undefined) { - const firmwarePath = firmwareZipMap.get(info[0].hubType); + const firmwarePath = firmwareZipMap.get(info.response.hubType); if (firmwarePath === undefined) { - yield put( + yield* put( notification.add( 'error', "Sorry, we don't have firmware for this hub yet.", ), ); - yield put(disconnectRequest(nextMessageId())); + yield* put(disconnectRequest(nextMessageId())); return; } - const response = (yield call(() => fetch(firmwarePath))) as Response; + const response = yield* call(() => fetch(firmwarePath)); if (!response.ok) { - yield put(notification.add('error', 'Failed to fetch firmware.')); - const disconnectAction = (yield put( - disconnectRequest(nextMessageId()), - )) as BootloaderDisconnectRequestAction; - yield waitForDidRequest(disconnectAction.id); + yield* put(notification.add('error', 'Failed to fetch firmware.')); + const disconnectAction = yield* put(disconnectRequest(nextMessageId())); + yield* waitForDidRequest(disconnectAction.id); return; } - const data = (yield call(() => response.arrayBuffer())) as ArrayBuffer; + const data = yield* call(() => response.arrayBuffer()); ({ firmware, deviceId } = yield* loadFirmware(data, program)); - if (deviceId !== undefined && info[0].hubType !== deviceId) { + if (deviceId !== undefined && info.response.hubType !== deviceId) { throw Error( - `Connected to ${info[0].hubType} but firmware is for ${deviceId}`, + `Connected to ${info.response.hubType} but firmware is for ${deviceId}`, ); } } - yield put(didStart()); + yield* put(didStart()); - const eraseAction = (yield put( - eraseRequest(nextMessageId()), - )) as BootloaderEraseRequestAction; - const [, erase] = (yield all([ - waitForDidRequest(eraseAction.id), - waitForResponse(BootloaderResponseActionType.Erase, 5000), - ])) as [BootloaderDidRequestAction, WaitResponse]; - if (!erase[0] || erase[0].result) { + const eraseAction = yield* put(eraseRequest(nextMessageId())); + const { erase } = yield* all({ + sent: waitForDidRequest(eraseAction.id), + erase: waitForResponse( + BootloaderResponseActionType.Erase, + 5000, + ), + }); + if (!erase.response || erase.response.result) { // TODO: proper error handling throw Error(`Failed to erase: ${erase}`); } - const initAction = (yield put( - initRequest(nextMessageId(), firmware.length), - )) as BootloaderInitRequestAction; - const [, init] = (yield all([ - waitForDidRequest(initAction.id), - waitForResponse(BootloaderResponseActionType.Init), - ])) as [BootloaderDidRequestAction, WaitResponse]; - if (!init[0] || init[0].result) { + const initAction = yield* put(initRequest(nextMessageId(), firmware.length)); + const { init } = yield* all({ + sent: waitForDidRequest(initAction.id), + init: waitForResponse( + BootloaderResponseActionType.Init, + ), + }); + if (!init.response || init.response.result) { // TODO: proper error handling throw Error(`Failed to init: ${init}`); } // 14 is "safe" size for all hubs - const maxDataSize = MaxProgramFlashSize.get(info[0].hubType) || 14; + const maxDataSize = MaxProgramFlashSize.get(info.response.hubType) || 14; for (let count = 1, offset = 0; ; count++) { const payload = firmware.slice(offset, offset + maxDataSize); - const programAction = (yield put( + const programAction = yield* put( programRequest( nextMessageId(), - info[0].startAddress + offset, + info.response.startAddress + offset, payload.buffer, ), - )) as BootloaderProgramRequestAction; - yield waitForDidRequest(programAction.id); + ); + yield* waitForDidRequest(programAction.id); - yield put(didProgress(offset / firmware.length)); + yield* put(didProgress(offset / firmware.length)); // we don't want to request checksum if this is the last packet since // the bootloader will send a response to the program request already. @@ -348,46 +332,42 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { // number of packets that can be queued in the Bluetooth chip on // the hub is not known and could vary by device. if (count % 10 === 0) { - const checksumAction = (yield put( - checksumRequest(nextMessageId()), - )) as BootloaderChecksumRequestAction; - const [, checksum] = (yield all([ - waitForDidRequest(checksumAction.id), - waitForResponse(BootloaderResponseActionType.Checksum, 5000), - ])) as [ - BootloaderDidRequestAction, - WaitResponse, - ]; - if (!checksum[0]) { + const checksumAction = yield* put(checksumRequest(nextMessageId())); + const { checksum } = yield* all({ + sent: waitForDidRequest(checksumAction.id), + checksum: waitForResponse( + BootloaderResponseActionType.Checksum, + 5000, + ), + }); + if (!checksum.response) { // TODO: proper error handling throw Error(`Failed to get checksum: ${checksum}`); } } } - const flash = (yield waitForResponse( + const flash = yield* waitForResponse( BootloaderResponseActionType.Program, 5000, - )) as WaitResponse; - if (!flash[0]) { + ); + if (!flash.response) { throw Error(`failed to get final response: ${flash}`); } - if (flash[0].count !== firmware.length) { + if (flash.response.count !== firmware.length) { // TODO: proper error handling throw Error("Didn't flash all bytes"); } - yield put(didProgress(1)); + yield* put(didProgress(1)); // this will cause the remote device to disconnect and reboot - const rebootAction = (yield put( - rebootRequest(nextMessageId()), - )) as BootloaderRebootRequestAction; - yield waitForDidRequest(rebootAction.id); + const rebootAction = yield* put(rebootRequest(nextMessageId())); + yield* waitForDidRequest(rebootAction.id); - yield put(didFinish()); + yield* put(didFinish()); } export default function* (): Generator { - yield takeEvery(FlashFirmwareActionType.FlashFirmware, flashFirmware); + yield* takeEvery(FlashFirmwareActionType.FlashFirmware, flashFirmware); } diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index 2d3fd7b4..39dd7be5 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020 The Pybricks Authors -import { assert, hex, maybe } from '.'; +import { assert, defined, hex, maybe } from '.'; test('assert', () => { const assertTrue = jest.fn(() => assert(true, 'should not throw')); @@ -11,14 +11,21 @@ test('assert', () => { expect(() => assert(false, 'should throw')).toThrow(); }); +describe('defined', () => { + expect(() => defined('test')).not.toThrow(); + expect(() => defined(undefined)).toThrowError(); +}); + describe('maybe', () => { test('resolved', async () => { - const result = await maybe(Promise.resolve('test')); + const [result, error] = await maybe(Promise.resolve('test')); expect(result).toBe('test'); + expect(error).toBeUndefined(); }); test('rejected', async () => { - const result = await maybe(Promise.reject(new Error('test'))); - expect(result).toBeInstanceOf(Error); + const [result, error] = await maybe(Promise.reject(new Error('test'))); + expect(result).toBeUndefined(); + expect(error).toBeInstanceOf(Error); }); }); diff --git a/src/utils/index.ts b/src/utils/index.ts index ed26210f..60fee754 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,20 +7,29 @@ * @param condition A condition that is assumed to be true * @param message Informational message for debugging */ -export function assert(condition: boolean, message: string): void { +export function assert(condition: boolean, message: string): asserts condition { if (!condition) { throw Error(message); } } -export type Maybe = T | Error; +/** + * Asserts that an object is not undefined. This is used to make the type + * checker happy with `maybe()` and saga `race()` and `all()` effects where + * we have the condition "if A is undefined, then B is not undefined". + */ +export function defined(obj: T): asserts obj is NonNullable { + assert(obj !== undefined, 'undefined object'); +} + +export type Maybe = [T?, Error?]; /** Wraps a promise in try/catch and returns the promise result or error. */ export async function maybe(promise: Promise): Promise> { try { - return await promise; + return [await promise]; } catch (err) { - return err; + return [undefined, err]; } } diff --git a/yarn.lock b/yarn.lock index 8b8652dd..8dbc8900 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2138,6 +2138,20 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/eslint-plugin@^4.8.2": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.0.tgz#92db8e7c357ed7d69632d6843ca70b71be3a721d" + integrity sha512-IJ5e2W7uFNfg4qh9eHkHRUCbgZ8VKtGwD07kannJvM5t/GU8P8+24NX8gi3Hf5jST5oWPY8kyV1s/WtfiZ4+Ww== + dependencies: + "@typescript-eslint/experimental-utils" "4.14.0" + "@typescript-eslint/scope-manager" "4.14.0" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + lodash "^4.17.15" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + "@typescript-eslint/experimental-utils@4.13.0", "@typescript-eslint/experimental-utils@^4.0.1": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.13.0.tgz#9dc9ab375d65603b43d938a0786190a0c72be44e" @@ -2150,6 +2164,18 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" +"@typescript-eslint/experimental-utils@4.14.0", "@typescript-eslint/experimental-utils@^4.8.2", "@typescript-eslint/experimental-utils@^4.9.1": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.0.tgz#5aa7b006736634f588a69ee343ca959cd09988df" + integrity sha512-6i6eAoiPlXMKRbXzvoQD5Yn9L7k9ezzGRvzC/x1V3650rUk3c3AOjQyGYyF9BDxQQDK2ElmKOZRD0CbtdkMzQQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/scope-manager" "4.14.0" + "@typescript-eslint/types" "4.14.0" + "@typescript-eslint/typescript-estree" "4.14.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + "@typescript-eslint/experimental-utils@^3.10.1": version "3.10.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" @@ -2171,6 +2197,16 @@ "@typescript-eslint/typescript-estree" "4.13.0" debug "^4.1.1" +"@typescript-eslint/parser@^4.8.2": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.14.0.tgz#62d4cd2079d5c06683e9bfb200c758f292c4dee7" + integrity sha512-sUDeuCjBU+ZF3Lzw0hphTyScmDDJ5QVkyE21pRoBo8iDl7WBtVFS+WDN3blY1CH3SBt7EmYCw6wfmJjF0l/uYg== + dependencies: + "@typescript-eslint/scope-manager" "4.14.0" + "@typescript-eslint/types" "4.14.0" + "@typescript-eslint/typescript-estree" "4.14.0" + debug "^4.1.1" + "@typescript-eslint/scope-manager@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz#5b45912a9aa26b29603d8fa28f5e09088b947141" @@ -2179,6 +2215,14 @@ "@typescript-eslint/types" "4.13.0" "@typescript-eslint/visitor-keys" "4.13.0" +"@typescript-eslint/scope-manager@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.0.tgz#55a4743095d684e1f7b7180c4bac2a0a3727f517" + integrity sha512-/J+LlRMdbPh4RdL4hfP1eCwHN5bAhFAGOTsvE6SxsrM/47XQiPSgF5MDgLyp/i9kbZV9Lx80DW0OpPkzL+uf8Q== + dependencies: + "@typescript-eslint/types" "4.14.0" + "@typescript-eslint/visitor-keys" "4.14.0" + "@typescript-eslint/types@3.10.1": version "3.10.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" @@ -2189,6 +2233,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.13.0.tgz#6a7c6015a59a08fbd70daa8c83dfff86250502f8" integrity sha512-/+aPaq163oX+ObOG00M0t9tKkOgdv9lq0IQv/y4SqGkAXmhFmCfgsELV7kOCTb2vVU5VOmVwXBXJTDr353C1rQ== +"@typescript-eslint/types@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.0.tgz#d8a8202d9b58831d6fd9cee2ba12f8a5a5dd44b6" + integrity sha512-VsQE4VvpldHrTFuVPY1ZnHn/Txw6cZGjL48e+iBxTi2ksa9DmebKjAeFmTVAYoSkTk7gjA7UqJ7pIsyifTsI4A== + "@typescript-eslint/typescript-estree@3.10.1": version "3.10.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" @@ -2217,6 +2266,20 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.0.tgz#4bcd67486e9acafc3d0c982b23a9ab8ac8911ed7" + integrity sha512-wRjZ5qLao+bvS2F7pX4qi2oLcOONIB+ru8RGBieDptq/SudYwshveORwCVU4/yMAd4GK7Fsf8Uq1tjV838erag== + dependencies: + "@typescript-eslint/types" "4.14.0" + "@typescript-eslint/visitor-keys" "4.14.0" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + "@typescript-eslint/visitor-keys@3.10.1": version "3.10.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" @@ -2232,6 +2295,14 @@ "@typescript-eslint/types" "4.13.0" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.0.tgz#b1090d9d2955b044b2ea2904a22496849acbdf54" + integrity sha512-MeHHzUyRI50DuiPgV9+LxcM52FCJFYjJiWHtXlbyC27b80mfOwKeiKI+MHOTEpcpfmoPFm/vvQS88bYIx6PZTA== + dependencies: + "@typescript-eslint/types" "4.14.0" + eslint-visitor-keys "^2.0.0" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -2681,7 +2752,7 @@ array.prototype.flat@^1.2.3: define-properties "^1.1.3" es-abstract "^1.18.0-next.1" -array.prototype.flatmap@^1.2.3: +array.prototype.flatmap@^1.2.3, array.prototype.flatmap@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9" integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q== @@ -2893,7 +2964,7 @@ babel-plugin-jest-hoist@^26.6.2: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-macros@2.8.0: +babel-plugin-macros@2.8.0, babel-plugin-macros@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== @@ -2902,6 +2973,15 @@ babel-plugin-macros@2.8.0: cosmiconfig "^6.0.0" resolve "^1.12.0" +babel-plugin-macros@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.0.1.tgz#0d412d68f5b3d1b64358f24ab099bd148724e2a9" + integrity sha512-CKt4+Oy9k2wiN+hT1uZzOw7d8zb1anbQpf7KLwaaXRCi/4pzKdFKHf7v5mvoPmjkmxshh7eKZQuRop06r5WP4w== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + babel-plugin-named-asset-import@^0.3.7: version "0.3.7" resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz#156cd55d3f1228a5765774340937afc8398067dd" @@ -4861,6 +4941,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.14.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -4885,6 +4970,11 @@ eslint-config-react-app@^6.0.0: dependencies: confusing-browser-globals "^1.0.10" +eslint-config-typed-fp@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-typed-fp/-/eslint-config-typed-fp-1.3.0.tgz#ca62050793a80c0b9af6f370e925797fa4c243f9" + integrity sha512-I6+/szKXAbZQ23pCjVAoqaM0AtYXIfo40QrLKHFfZ/Fh+ROnd2vOawCJzgYfIuPZrjbVbnlsLuM8LTK19YihYA== + eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" @@ -4909,6 +4999,17 @@ eslint-plugin-flowtype@^5.2.0: lodash "^4.17.15" string-natural-compare "^3.0.1" +eslint-plugin-functional@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-functional/-/eslint-plugin-functional-3.2.1.tgz#d5ad668b57646ad24f4ef0476328408681d59061" + integrity sha512-uJ8W0FznWsKp4exxO79b0xSc1WNROzDiVNGgSFOwdZCBeUHQf89BqwqlshNW9aSz/kg2gVGs+Ue6AeTpNSFM/g== + dependencies: + "@typescript-eslint/experimental-utils" "^4.9.1" + array.prototype.flatmap "^1.2.4" + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + object.fromentries "^2.0.3" + eslint-plugin-import@^2.22.1: version "2.22.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" @@ -4988,6 +5089,16 @@ eslint-plugin-testing-library@^3.9.2: dependencies: "@typescript-eslint/experimental-utils" "^3.10.1" +eslint-plugin-total-functions@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-total-functions/-/eslint-plugin-total-functions-4.7.2.tgz#e60801da31e1f0e30a2d28b42921ab1f72cb66de" + integrity sha512-NG0Is/W+l9vGMbo6wABGGw00Wl6VR0JdN99u28bTKfRzisxnRFJGR/i12jFgQqJHxPxGxC+lLxZ/E5NtwOjo+A== + dependencies: + "@typescript-eslint/eslint-plugin" "^4.8.2" + "@typescript-eslint/experimental-utils" "^4.8.2" + "@typescript-eslint/parser" "^4.8.2" + tsutils "^3.17.1" + eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" @@ -8386,7 +8497,7 @@ object.entries@^1.1.0, object.entries@^1.1.2: es-abstract "^1.18.0-next.1" has "^1.0.3" -object.fromentries@^2.0.2: +object.fromentries@^2.0.2, object.fromentries@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072" integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw== @@ -10417,7 +10528,7 @@ resolve@1.18.1: is-core-module "^2.0.0" path-parse "^1.0.6" -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1: +resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.3.2, resolve@^1.8.1: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== @@ -11855,6 +11966,14 @@ type@^2.0.0: resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA== +typed-redux-saga@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/typed-redux-saga/-/typed-redux-saga-1.3.1.tgz#92b01db41e3510102f87eb9ff261ec73d38a2e44" + integrity sha512-nUj1/1/SAesEsZrr7o24ID+++CqZ6QfPVDcwhY2rVmm4vEBr/vbDHJ6j/w6SomOcooLwnh3sdaWVhNEIy7VgNA== + optionalDependencies: + "@babel/helper-module-imports" "^7.12.1" + babel-plugin-macros "^2.8.0" + typed-styles@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"