Skip to content

Commit

Permalink
Release Beta.6.4 (#398)
Browse files Browse the repository at this point in the history
* Bump Version to Beta 7

* Refactor Captcha Token Management (#382)

* Reset Captcha Token After Use

This commit updates the checkout processes to reset the stored captcha token after it has been used. This prevents a token from being used twice.

* Enable Harvesting Suspension

This commit adds a new suspend method for harvesting captchas that allows a runner to temporarily stop harvesting tokens without fully destroying the token queue. This allows the frontend to better detect when a token is needed.

* Add HarvestStates Enum

This commit adds an enumeration for harvest states to better handle harvest state transitions.

* Fix Invalid Form Bug

This commit fixes a bug caused by an invalid form structure when patching the checkout. An empty string passed into the captcha token caused the form structure to be invalid. The change allows an empty string to be passed in without causing an invalid form structure.

* Add better handling for captcha requesters

This commit refactors the handler for captcha requests now that harvest suspension is a thing. This will prevent multiple start/stops from a single runner interfering with the total semaphore count.

* Enable Auto Stop for harvesting tokens

This commit updates the captcha window manager to automatically stop after a certain number of tokens have been harvested. This prevents overharvesting of tokens.

* Fix Captcha Reset Bug

This commit fixes a bug that prevented the captcha from being reset properly. If the captcha window submitted a token and was stopped immediately, the reset logic would call the recaptcha api reset function _while_ the api was in the middle of calling the submit callback. This would cause a bug when resetting and prevent the reset from occurring.

To fix this a new _submitting flag is added to track when the captcha window is in the process of submitting a token. If the stop handler is called when submitting, the stop handler skips resetting the challenge.

The submit handler now calls the reset method regardless of the start status. Further. the reset function has been adjusted to remove the shouldAutoClick parameter. An analysis of the calls to this function show that we only autoclick if the _started flag is true. Thus the parameter was removed and the _started flag is used alone to determine whether autoclick should be run. Additionally, the logic to hide the captcha form is moved to the reset method since we only hide the form when _started is false.

* Add Async Queue to Frontend

This commit adds the async queue to the frontend so the captcha window manager can manage a queue and return tokens using promises. The async queue has been updated to include expiration functionality.

The expiration functionality takes several parameters:
- A filter function to determine whether a datum has "expired"
- A interval time (default 1000ms)
- Update callback to add logic _after_ the filter has occurred
- thisArg optional this reference to use with _both_ the filter and update calls

* Use AsyncQueue to manage tokens

This commit refactors the captcha window manager to use an async queue for tokens. When a maximum number of tokens has been added to the backlog, the token harvesting is suspended. Once all tokens have been used (or expired) the harvesting resumes.

* Source Captcha Tokens CWM

This commit completes the transition to have the launcher source tokens from the CWM rather than directly from a  harvest event. This allows us to take full advantage of the captcha backlog and handle expired tokens automatically.

* Remove unnecessary function

This commit removes a function that is no longer used.

* Add Better Guard against Canceling Queue Request

This commit updates the AsyncQueue to add better guarding against calling the `cancel` function for the returned request. Instead of a null reference, an empty function is used by default. Further, when the request resolves, the cancel function is replaced with an empty function to prevent a rejection from being called after the promise has already finished.

* Add New Debug Commands

This commit updates the frontend to add new debug commands surrounding captcha management:
- viewRunnerRequests - view how many requests each unique runner has pending
- viewCwmQueueStats - view the queue and backlog lengths for the captcha queue
- viewCwmHarvestState - view the current harvest state of the CWM

Additionally, a bug was fixed with the start and stop harvest debug commands to forward parameters in the correct order. Lastly some console logs were added for debugging.

* Minor Refactors to Request Cancelling

This commit updates the launcher to only cancel unfulfilled requests.

Further, some minor refactoring is done sending the token to the launcher. This removes a redundant helper function in place of a one-line insert.

* Fix AsyncQueue Bug

This commit fixes a bug caused by the async queue treating the wait queue as a stack (using push/pop instead of unshift/pop). This caused incoming tokens to be sent to the wrong runner and have two requests cancelled instead of only one per token.

fixes #362

* Loosen Ban Criteria for Proxies (#386)

* Adjust shouldBan flag to only ban on 403 errors

This commit loosens the should ban status to only being on 403 errors. This should prevent a chain of proxy bans from too many requests on the same site.

* Adjust Log Message

This commit updates the log message to add the number of proxies available for searching. This should help diagnose errors where the proxy list is getting out of sync.

* Fix typo

the `size` is a property of a `Map` not a function, so anytime we run this code, an error is thrown. Change this to a property call fixes the issue.

Co-Authored-By: pr1sm <dhanwada.dev@gmail.com>

* Add Redux State Migration (#389)

* Add Migrator Logic

This commit adds the main logic behind migrating a state from a given version to the latest. This requires the addition of the semver package to sort tracked versions sequentially.

This logic will not have to change, but new migrators will have to be imported as we make new changes to the state.

* Add 0.0.0 Migrator

This commit adds the initial migrator that represents the current state tree. A copy of the initial state tree is added so the original source can be updated as changes are made. References to the original source files are added in comments. This base state will be used as an initial starting point for new migrations.

* Add 0.1.0 Migrator

This commit adds a 0.1.0 migrator to handle adding the first "versioned" state tree. This migrator simply attaches the version to the previous 0.0.0 state.

* Refactor Top Level Migrator

This commit refactors the top level migrator to allow a full migrator map to be injected instead of just the versions. This prevents a bug where a version is found (from injected versions), but doesn't exist in the migrator map, causing an error.

* Add Migration Shortcut on Initial State

This commit adds a shortcut to the migration logic when the given state is null. In this case, we simply want to return the latest migrations initial state, so we don't need to go through the logic of creating a full length chain and going through meaningless migrations, ending up with the latest initial state.

* Add Migrate State Action

This commit adds a global action to migrate the state tree and adds the call to the migrator in the top level reducer. Further. the initial state tree is updated to the latest tracked version (0.1.0).

* Call Migrate State

This commit updates the entry point of the frontend react to call migrate state.

* Fix typo

Co-Authored-By: pr1sm <dhanwada.dev@gmail.com>

* Fix Windows Jest Caching Errors [temporary]

This commit temporarily fixes a bug with jest due to bad atomic file writing on Windows. This fix is a temporary on using the resolutions to force a lower version of the offending package.

A fix is available for this package (2.4.2) and is consumed in a newer version of jest (24.x), but both packages are depended on transiently through react-scripts, which has yet to consume the fixed version of jest. Until that fix makes its way to react-scripts, we can use this to pin the working version.

* Adjust Directory Structure

This commit adjusts the directory structure to match the style of importing we have for actions and reducers. No functionality changes, just the file names, locations, and import paths.

* Add Migrator Logic Unit Tests

This commit adds a new suite of unit tests to focus on the testing the logic for the top level migrator. Test suite over specific migrators will follow.

* Add Unit Test Suites for Specific Migrators

This commit adds separate test suites for each migrator added.

* Finish adding Unit Tests

This commit updates the frontend to increase the coverage of changed/added code.

[Bonus]: Tests for the set theme action were also added.

* Source Initial State from latest migration version

This commit updates the frontend to source the initial state from the latest migration version. Instead of having the initial state parts defined in the definition files, the parts are exported similarly from several new files that pull the initial state from the migrator file. The migrator file in turn has a function that looks for the latest migrator and gets the initial state.

All import references were updated to reflect the change.

* Add Better Log Cleanup (#390)

* Add Daily File Rotate Transport

This commit adds the daily file rotate transport to handle rotating files on a schedule. This will be used instead of the regular file transport.

* Use daily rotate file transport

This commit updates the logger implementation to use the new transport instead of the old file transport.

* Prevent Unnecessary Logs

This commit prevents unnecessary log messages from being printed. The content is unnecessary and drastically increases the size of the log file for little to no benefit.

* Remove All Previous Logs

To initially cleanup logs, we need to delete the log directory contents the _first_ time we run this changed version. The presence of the `audit.json` file is what we use to determine this.

If the log directory previously existed, without the audit file, we know we were using an old version, so we recursively delete the directory contents.

We can then proceed with all future files following the DRF cleanup model.

fixes #118

* Add Restock Monitor (#392)

* switch endpoints to .js

* Split GenerateVariants to a Utility File

This commit splits a part of the monitor class out to a utility function. This will allow it to be used other classes as well without needing to extend Monitor.

* Add Initial Filter for Availability

This commit updates the generateVariants utility function to check for available variants before doing anything else. This allows any generated variants to be known as available, while also allowing us to return early if no variants are available.

* Add Variant Related Error Codes

This commit adds variant specific error codes and updates the generateVariants function throw errors with the relevant codes.

A new private function is added to the monitor to call the generateVariants function and handle the error returned (if it exists).

* Ensure all parsers include product url

This commit updates the parser run methods to ensure the parser includes the product url. This ensures that restocking can happen without the need to search for the product again.

That product url is then stored as the restockUrl for the task's product -- this will allow restocking to have an easy reference to the endpoint it needs to poll for more information.

* Consolidate Error Codes Map

This commit consolidates the error codes maps into one. Separate error maps added an unnecessary extra level.

Imports and usages have been updated to reflect this change.

* Implement Restock Monitor

This commit implements a sub class of the monitor that performs restock monitoring. If restocking is supported, the restock monitor will look for matched, available variants. If no variants are available, further restock monitoring will take place. If variants are available, they will be added to the cart.

* Consolidate Parser Error Handling

This commit refactors the monitor to add a new method for handling parser errors. This allows the method to be applied more universally to the monitor as well as the restock monitor without code duplication.

* Update status messages to match

This commit updates the status messages for restocks so they match the ones emitted from the checkout class.

* Add Restocking Handler to Task Runner

This commit adds the restock monitor and restocking handler to the TaskRunner. All nextStates that deal with restocking have been updated to use the restocking monitor.

* Fix Imports

Fix imports to be direct instead of inside an export object.

Co-Authored-By: pr1sm <dhanwada.dev@gmail.com>

* Fix Typo and Reword Status Message

This commit updates a typo that caused a crash when monitoring with keywords. This commit also updates the message that gets emitted when a product has been restocked.

* Bug Fixes

This commit updates several files with the following fixes:
- Fixed typos where the wrong variable was referenced
- Returned the right value
- Fixed Typos in status messages and log messages
- Ensure product url is always the correct format

* Wait for Monitor Delay when Restocking

Co-Authored-By: pr1sm <dhanwada.dev@gmail.com>

Fixes #380

* Add Small Frontend Improvements (#395)

* Update yarn lock

* Remove UK Mens Size Group

This commit removes the UK mens size group until multi region size support is implement. The US sizing is renamed to just "Men's" instead of "US Men's"

* Change default country to US

This commit adds a new migrator to adjust the default state of all locations to the US. If a country was already entered, it is not changed.

* remove isMulti from sizes

* Add Better Handling for Garment Size Matching

This commit updates the variant matching algorithm to improve matching when garment sizes are the input.

When a garment size is determined (no numbers in the input size), All mapped sizes must be non-numeric and start with the input size.

* Increase Test Coverage

This commit updates tests related to changed code to increase coverage.

* Fix Size Change Handlers

This commit updates the size change handlers for both the createTask and taskRow components to allow for single select components. A temporary hot fix to mock two multi-select changes is used to prevent a total rewrite of reducer logic.

* Update packages/frontend/src/__tests__/constants/getAllSizes.test.js

* Fix Typo

* version 6.4
  • Loading branch information
walmat authored Mar 23, 2019
1 parent f26e178 commit d56fb3d
Show file tree
Hide file tree
Showing 163 changed files with 2,114 additions and 768 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"prettier-eslint": "^8.8.2",
"yarn": "^1.12.3"
},
"resolutions": {
"write-file-atomic": "2.3.0"
},
"husky": {
"hooks": {
"pre-push": "yarn ci",
Expand Down
130 changes: 89 additions & 41 deletions packages/frontend/lib/_electron/captchaWindowManager.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { session: Session } = require('electron');
const moment = require('moment');
const shortid = require('shortid');

const { createCaptchaWindow, createYouTubeWindow, urls } = require('./windows');
const nebulaEnv = require('./env');
const IPCKeys = require('../common/constants');
const AsyncQueue = require('../common/classes/asyncQueue');

nebulaEnv.setUpEnvironment();

// TODO: Should we move this to the constants file?
const HARVEST_STATE = {
IDLE: 'idle',
SUSPEND: 'suspend',
ACTIVE: 'active',
};

class CaptchaWindowManager {
/**
* Check if given token is valid
*
* Tokens are invalid if they have exceeded their lifespan
* of 110 seconds. Use moment to check the timestamp
*/
static isTokenValid({ timestamp }) {
return moment().diff(moment(timestamp), 'seconds') <= 110;
}
const MAX_HARVEST_CAPTCHA_COUNT = 5;

class CaptchaWindowManager {
constructor(context) {
/**
* Application Context
Expand All @@ -45,9 +38,9 @@ class CaptchaWindowManager {
this._youtubeWindows = {};

/**
* Map of harvested tokens based on sitekey
* Async Queue to maange tokens
*/
this._tokens = {};
this._tokenQueue = new AsyncQueue();

/**
* Id for check token interval
Expand All @@ -67,6 +60,13 @@ class CaptchaWindowManager {

this.validateSender = this.validateSender.bind(this);

this._tokenQueue.addExpirationFilter(
({ timestamp }) => moment().diff(moment(timestamp), 'seconds') <= 110,
1000,
this._handleTokenExpirationUpdate,
this,
);

// Attach IPC handlers
context.ipc.on(IPCKeys.RequestLaunchYoutube, this.validateSender(this._onRequestLaunchYoutube));
context.ipc.on(IPCKeys.RequestEndSession, this.validateSender(this._onRequestEndSession));
Expand All @@ -86,6 +86,30 @@ class CaptchaWindowManager {
this._captchaWindows.find(win => win.webContents.id === ev.sender.id).close();
}),
);

if (nebulaEnv.isDevelopment()) {
context.ipc.on('debug', (ev, type) => {
switch (type) {
case 'viewCwmQueueStats': {
ev.sender.send(
'debug',
type,
`Queue Line Length: ${this._tokenQueue.lineLength}, Backlog Length: ${
this._tokenQueue.backlogLength
}`,
);
break;
}
case 'viewCwmHarvestState': {
ev.sender.send('debug', type, `State: ${this._harvestStatus.state}`);
break;
}
default: {
break;
}
}
});
}
}

/**
Expand Down Expand Up @@ -168,6 +192,23 @@ class CaptchaWindowManager {
}
}

/**
* Stop Harvesting
*
* Tell all captcha windows to stop harvesting and set the
* harvest state to 'idle'
*/
suspendHarvesting(runnerId, siteKey) {
this._harvestStatus = {
state: HARVEST_STATE.SUSPEND,
runnerId,
siteKey,
};
this._captchaWindows.forEach(win => {
win.webContents.send(IPCKeys.StopHarvestCaptcha, runnerId, siteKey);
});
}

/**
* Stop Harvesting
*
Expand All @@ -185,6 +226,15 @@ class CaptchaWindowManager {
});
}

/**
* Get Captcha
*
* Return the next valid, available captcha from the queue
*/
getNextCaptcha() {
return this._tokenQueue.next();
}

/**
* Create a captcha window and show it
*/
Expand Down Expand Up @@ -312,22 +362,6 @@ class CaptchaWindowManager {
});
}

_checkTokens() {
let tokensTotal = 0;
// Iterate through all sitekey token lists
Object.keys(this._tokens).forEach(siteKey => {
// Filter out invalid tokens
this._tokens[siteKey] = this._tokens[siteKey].filter(CaptchaWindowManager.isTokenValid);
tokensTotal += this._tokens[siteKey].length;
});

// Clear the interval if there are no more tokens
if (this._checkTokenIntervalId && tokensTotal === 0) {
clearInterval(this._checkTokenIntervalId);
this._checkTokenIntervalId = null;
}
}

/**
* Clears the storage data and cache for the session
*/
Expand All @@ -342,31 +376,45 @@ class CaptchaWindowManager {
}
}

/**
* Handle Expiration check updates
*
* Resume harvesting if it was previously suspended
* _and_ the number of tokens in the backlog is 0.
*/
_handleTokenExpirationUpdate() {
const { state, runnerId, siteKey } = this._harvestStatus;
if (this._tokenQueue.backlogLength === 0 && state === HARVEST_STATE.SUSPEND) {
console.log('[DEBUG]: Resuming harvesters...');
this.startHarvesting(runnerId, siteKey);
}
}

/**
* Harvest Token
*
* Harvest the token and store it with other tokens sharing the same site key.
* Start the check token interval if it hasn't started already. This will
* periodically check and remove expired tokens
*/
_onHarvestToken(_, __, token, siteKey = 'unattached', host = 'http://checkout.shopify.com') {
// TEMPORARY
// this.stopHarvesting();

if (!this._tokens[siteKey]) {
// Create token array if it hasn't been created for this sitekey
this._tokens[siteKey] = [];
}
_onHarvestToken(
_,
runnerId,
token,
siteKey = 'unattached',
host = 'http://checkout.shopify.com',
) {
// Store the new token
this._tokens[siteKey].push({
this._tokenQueue.insert({
token,
siteKey,
host,
timestamp: moment(),
});
// Start the check token interval if it hasn't been started
if (this._checkTokenIntervalId === null) {
this._checkTokenIntervalId = setInterval(this._checkTokens.bind(this), 1000);

if (this._tokenQueue.backlogLength >= MAX_HARVEST_CAPTCHA_COUNT) {
console.log('[DEBUG]: Token Queue is greater than max, suspending...');
this.suspendHarvesting(runnerId, siteKey);
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/lib/_electron/windowManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,15 @@ class WindowManager {
this._captchaWindowManager.stopHarvesting(runnerId, siteKey);
}

/**
* Get Next Captcha
*
* Forward call to Captcha Window Manager
*/
getNextCaptcha() {
return this._captchaWindowManager.getNextCaptcha();
}

// // TODO: Add this back in #350 (https://github.com/walmat/nebula/issues/350)
// onRequestChangeTheme(_, opts) {
// const { backgroundColor } = opts;
Expand Down
155 changes: 155 additions & 0 deletions packages/frontend/lib/common/classes/asyncQueue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Async Queue
*
* A variant of a Queue with synchronous inserts and
* asynchronous returns. If there is a backlog, the
* returns are synchronous
*
* The queue also supports expiration of the backlog
* by attaching a custom filter function to remove
* expired items.
*/
class AsyncQueue {
constructor() {
this._backlog = [];
this._waitQueue = [];
this._expiration = {
thisArg: null,
filterFunc: null,
interval: null,
intervalId: null,
onUpdate: null,
};
}

get backlogLength() {
return this._backlog.length;
}

get lineLength() {
return this._waitQueue.length;
}

insert(datum) {
// Check if we have anybody waiting for data
if (this._waitQueue.length) {
// Get the resolution and invoke it with the data
const resolution = this._waitQueue.pop();
resolution.request.status = 'fulfilled';
resolution.request.value = datum;
resolution.request.cancel = () => {};
resolution.resolve(datum);
} else {
// Add data to the backlog
this._backlog.unshift(datum);
this._startExpirationInterval();
}
return this._backlog.length;
}

next() {
// initialize request
const nextRequest = {
status: 'pending', // status of the request
cancel: () => {}, // function to cancel the request with a given reason
promise: null, // the async promise that is waiting for the next value
reason: '', // the reason for cancelling the request
value: null, // the resolved value
};

// Check if we don't have any waiters and we do have a backlog
if (!this._waitQueue.length && this._backlog.length) {
// return from the backlog immediately
const value = this._backlog.pop();
const promise = Promise.resolve(value);
return {
...nextRequest,
status: 'fulfilled',
promise,
value,
};
}

// Setup request promise and cancel function
nextRequest.promise = new Promise((resolve, reject) => {
nextRequest.cancel = reason => {
nextRequest.status = 'cancelled';
nextRequest.reason = reason;
this._waitQueue = this._waitQueue.filter(r => r.request !== nextRequest);
reject(reason);
};
this._waitQueue.unshift({ resolve, reject, request: nextRequest });
});

return nextRequest;
}

clear() {
// Remove all items from the backlog if they exist
this._backlog = [];
this._stopExpirationInterval();
}

destroy() {
// Reject all resolutions in the wait queue
this._waitQueue.forEach(({ reject, request }) => {
request.status = 'destroyed';
request.reason = 'Queue was destroyed';
reject('Queue was destroyed');
});
this._waitQueue = [];

// Remove all items from the backlog if they exist
this.clear();
}

addExpirationFilter(filterFunc, interval = 1000, onUpdate, thisArg) {
// Clear out previous interval if it exists
if (this._expiration.intervalId) {
clearInterval(this._expiration.intervalId);
}
// Update expiration settings
this._expiration = {
filterFunc,
thisArg,
interval,
onUpdate,
intervalId: null,
};
// Start interval if we have items in the backlog
if (this.backlogLength > 0) {
this._startExpirationInterval();
}
}

_startExpirationInterval() {
const { interval, intervalId, filterFunc: func } = this._expiration;
// Prevent start if we already have a running interval
// or if we don't have a defined filter function
if (intervalId || !func) {
return;
}
this._expiration.intervalId = setInterval(() => {
const { filterFunc, thisArg, onUpdate } = this._expiration;
this._backlog = this._backlog.filter(filterFunc, thisArg);
if (this.backlogLength === 0) {
this._stopExpirationInterval();
}
if (onUpdate) {
onUpdate.call(thisArg);
}
}, interval);
}

_stopExpirationInterval() {
const { intervalId } = this._expiration;
// No need to stop twice
if (!intervalId) {
return;
}
clearInterval(intervalId);
this._expiration.intervalId = null;
}
}

module.exports = AsyncQueue;
Loading

0 comments on commit d56fb3d

Please sign in to comment.