From 58fd75c8a1069de2d82c3c66518e7c1e94a50b6d Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Mon, 20 May 2019 17:09:04 -0700 Subject: [PATCH] POC: Try adding rexux-observable I don't know how we'll feasibly solve the multide of potential race conditons and async clean up memory leaks without something like rxjs. So, this is a trial to see the following: 1. How much does it increase bundle size? 2. How effective is typechecking? 3. Does it solve some of the difficult cancelation issues we have? --- js/actionCreators/files.ts | 37 ++------------------- js/actionTypes.ts | 1 + js/epics.ts | 66 ++++++++++++++++++++++++++++++++++++++ js/store.ts | 9 ++++++ js/types.ts | 7 +++- package.json | 4 ++- yarn.lock | 12 +++++++ 7 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 js/epics.ts diff --git a/js/actionCreators/files.ts b/js/actionCreators/files.ts index 455c3419..d45b4458 100644 --- a/js/actionCreators/files.ts +++ b/js/actionCreators/files.ts @@ -7,7 +7,6 @@ import { promptForFileReferences, genArrayBufferFromFileReference, genMediaDuration, - genMediaTags, } from "../fileUtils"; import skinParser from "../skinParser"; import { @@ -23,8 +22,6 @@ import { BUFFER_TRACK, SET_MEDIA_TAGS, SET_MEDIA_DURATION, - MEDIA_TAG_REQUEST_INITIALIZED, - MEDIA_TAG_REQUEST_FAILED, SET_SKIN_DATA, LOADED, LOADING, @@ -33,7 +30,7 @@ import LoadQueue from "../loadQueue"; import { removeAllTracks } from "./playlist"; import { setPreamp, setEqBand } from "./equalizer"; -import { LoadStyle, Thunk, Track, EqfPreset, SkinData } from "../types"; +import { LoadStyle, Thunk, Action, Track, EqfPreset, SkinData } from "../types"; // Lower is better const DURATION_VISIBLE_PRIORITY = 5; @@ -297,36 +294,8 @@ function queueFetchingMediaTags(id: number): Thunk { }; } -export function fetchMediaTags(file: string | Blob, id: number): Thunk { - return async (dispatch, getState, { requireMusicMetadata }) => { - dispatch({ type: MEDIA_TAG_REQUEST_INITIALIZED, id }); - - try { - const metadata = await genMediaTags(file, await requireMusicMetadata()); - // There's more data here, but we don't have a use for it yet: - const { artist, title, album, picture } = metadata.common; - const { numberOfChannels, bitrate, sampleRate } = metadata.format; - let albumArtUrl = null; - if (picture && picture.length >= 1) { - const byteArray = new Uint8Array(picture[0].data); - const blob = new Blob([byteArray], { type: picture[0].format }); - albumArtUrl = URL.createObjectURL(blob); - } - dispatch({ - type: SET_MEDIA_TAGS, - artist: artist ? artist : "", - title: title ? title : "", - album, - albumArtUrl, - numberOfChannels, - bitrate, - sampleRate, - id, - }); - } catch (e) { - dispatch({ type: MEDIA_TAG_REQUEST_FAILED, id }); - } - }; +export function fetchMediaTags(file: string | Blob, id: number): Action { + return { type: "FETCH_MEDIA_TAGS", file, id }; } export function setEqFromFileReference(fileReference: File): Thunk { diff --git a/js/actionTypes.ts b/js/actionTypes.ts index 8daa196b..e3988068 100644 --- a/js/actionTypes.ts +++ b/js/actionTypes.ts @@ -87,3 +87,4 @@ export const TOGGLE_RANDOMIZE_PRESETS = "TOGGLE_RANDOMIZE_PRESETS"; export const TOGGLE_PRESET_CYCLING = "TOGGLE_PRESET_CYCLING"; export const SCHEDULE_MILKDROP_MESSAGE = "SCHEDULE_MILKDROP_MESSAGE"; export const SET_MILKDROP_FULLSCREEN = "SET_MILKDROP_FULLSCREEN"; +export const FETCH_MEDIA_TAGS = "FETCH_MEDIA_TAGS"; diff --git a/js/epics.ts b/js/epics.ts new file mode 100644 index 00000000..0bc5e794 --- /dev/null +++ b/js/epics.ts @@ -0,0 +1,66 @@ +import { Observable, of, defer, concat } from "rxjs"; +import { filter, mergeMap, catchError } from "rxjs/operators"; +import { Action, AppState, Extras } from "./types"; +import { + MEDIA_TAG_REQUEST_INITIALIZED, + SET_MEDIA_TAGS, + MEDIA_TAG_REQUEST_FAILED, + FETCH_MEDIA_TAGS, +} from "./actionTypes"; +import { genMediaTags } from "./fileUtils"; + +export const fetchMediaTagsEpic = ( + actions: Observable, + states: Observable, + { requireMusicMetadata }: Extras +): Observable => { + return actions.pipe( + filter(action => action.type === FETCH_MEDIA_TAGS), + mergeMap(action => { + // TODO: Check out https://github.com/piotrwitek/typesafe-actions + if (action.type !== FETCH_MEDIA_TAGS) { + throw new Error("Invalid"); + } + const { file, id } = action; + + return concat( + of({ + type: MEDIA_TAG_REQUEST_INITIALIZED, + id, + } as const), + defer(async () => { + const musicMetadata = await requireMusicMetadata(); + const metadata = await genMediaTags(file, musicMetadata); + // There's more data here, but we don't have a use for it yet: + const { artist, title, album, picture } = metadata.common; + const { numberOfChannels, bitrate, sampleRate } = metadata.format; + let albumArtUrl = null; + if (picture && picture.length >= 1) { + const byteArray = new Uint8Array(picture[0].data); + const blob = new Blob([byteArray], { type: picture[0].format }); + albumArtUrl = URL.createObjectURL(blob); + } + return { + type: SET_MEDIA_TAGS, + artist: artist ? artist : "", + title: title ? title : "", + album, + albumArtUrl, + numberOfChannels, + bitrate, + sampleRate, + id, + } as const; + }).pipe( + catchError(e => { + console.error("Failed to fetch media tags", e); + return of({ + type: MEDIA_TAG_REQUEST_FAILED, + id, + } as const); + }) + ) + ); + }) + ); +}; diff --git a/js/store.ts b/js/store.ts index 1b0d4010..8dfeaa20 100644 --- a/js/store.ts +++ b/js/store.ts @@ -8,6 +8,8 @@ import { UPDATE_TIME_ELAPSED, STEP_MARQUEE } from "./actionTypes"; import Media from "./media"; import Emitter from "./emitter"; import { Extras, Dispatch, Action, AppState, Middleware } from "./types"; +import { createEpicMiddleware, combineEpics } from "redux-observable"; +import * as Epics from "./epics"; // TODO: Move to demo const compose = composeWithDevTools({ @@ -34,10 +36,15 @@ export default function( return next(action); }; + const epicMiddleware = createEpicMiddleware({ + dependencies: extras, + }); + const enhancer = compose( applyMiddleware( ...[ thunk.withExtraArgument(extras), + epicMiddleware, mediaMiddleware(media), emitterMiddleware, ...customMiddlewares, @@ -50,5 +57,7 @@ export default function( const store = initialState ? createStore(reducer, initialState, enhancer) : createStore(reducer, enhancer); + + epicMiddleware.run(combineEpics(...Object.values(Epics))); return store; } diff --git a/js/types.ts b/js/types.ts index cb98ca77..7dca66d5 100644 --- a/js/types.ts +++ b/js/types.ts @@ -509,7 +509,12 @@ export type Action = index: number; transitionType: TransitionType; } - | { type: "TOGGLE_PRESET_OVERLAY" }; + | { type: "TOGGLE_PRESET_OVERLAY" } + | { + type: "FETCH_MEDIA_TAGS"; + file: string | Blob; + id: number; + }; export type MediaTagRequestStatus = | "INITIALIZED" diff --git a/package.json b/package.json index 47629e79..7a450a3f 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,9 @@ }, "dependencies": { "eslint-plugin-react-hooks": "^1.5.1", - "fscreen": "^1.0.2" + "fscreen": "^1.0.2", + "redux-observable": "^1.1.0", + "rxjs": "^6.5.2" }, "bundlesize": [ { diff --git a/yarn.lock b/yarn.lock index d740f52d..a37e52dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9079,6 +9079,11 @@ redux-devtools-extension@^2.13.2: resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1" integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg== +redux-observable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.1.0.tgz#323a8fe53e89fdb519be2807b55f08e21c13e6f1" + integrity sha512-G0nxgmTZwTK3Z3KoQIL8VQu9n0YCUwEP3wc3zxKQ8zAZm+iYkoZvBqAnBJfLi4EsD1E64KR4s4jFH/dFXpV9Og== + redux-thunk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" @@ -9459,6 +9464,13 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" +rxjs@^6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" + integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"