I am trying to convert some code from objective-c to swift and along with that use async/await as much as possible.
This code leverages a C library that uses callbacks with a void* context. There are parts of our code where we need to "wait" until the callback is completed. This can often be 2 different callbacks for success or failure.
Below is an example of the code
class Test
{
var sessionStarted: Bool = false
let sessionSemaphore = DispatchSemaphore(value: 0)
// We need the session to be available once this function returns
func startSession() throws
{
guard !sessionStarted else {
print("Session already started")
return
}
let callback: KMSessionEventCallback = { inSession, inEvent, inContext in
guard let session = inSession, let context = inContext else { return }
let testContext: Test = Unmanaged<Test>.fromOpaque(context).takeUnretainedValue()
testContext.handleSessionUpdate(session: session, event: inEvent)
}
let result = KMSessionAttach("SessionTest", &callback, Unmanaged.passUnretained(self).toOpaque())
if result != KM_SUCCESS {
print("Failed to attach session")
}
// Currently uses a semaphore to wait. =(
let dispatchResult = self.sessionSemaphore.wait(timeout: .now() + DispatchTimeInterval.seconds(30))
if dispatchResult == .timedOut {
print("Waited 30 seconds and never attached session")
}
}
private func handleSessionUpdate(session: KMSession, event: KMSessionEvent)
{
switch event
{
case KM_SESSION_ATTACHED:
self.sessionStarted = true
case KM_SESSION_TERMINATED:
fallthrough
case KM_SESSION_FAILED:
fallthrough
case KM_SESSION_DETACHED:
self.sessionStarted = false
default:
print("Unknown State")
}
self.sessionSemaphore.signal()
}
}
I am struggling to figure out how to use continuations here because I cannot capture any context within these C callbacks.
Related
What is the proper way to throw an error from a function like this:
func fetch(by id: String, page: Int = 1) -> Promise<ProductReviewBase> {
// call api
guard let url = URL(string: "") else {
return Promise { _ in return IntegrationError.invalidURL }
}
return query(with: url)
}
I'm confused whether to make this a function that throws an error, or return a promise that returns an error. Thanks
I really hate interfaces that mix metaphors. If you are going to return a promise, then use the promise's error system. If you want more justification than my hatred, then visualize what it would look like at the call site:
do {
(try fetch(by: id))
.then {
// do something
}
.catch { error in
// handle error
}
}
catch {
// handle error
}
vs
fetch(by: id)
.then {
// do something
}
.catch { error in
// handle error
}
The latter looks a whole lot cleaner.
Here's the best way (IMO) to write your example function:
func fetch(by id: String, page: Int = 1) -> Promise<ProductReviewBase> {
guard let url = URL(string: "") else { return Promise(error: IntegrationError.invalidURL) }
return query(with: url)
}
I'm basically trying to create a macro that automatically generates an if/else if chain by supplying one common outcome statement for all conditions.
This is what I've tried so far (modified the code just to show as an example):
import haxe.macro.Expr;
class LazyUtils {
public macro static function tryUntilFalse( xBool:Expr, xConds:Array<Expr> ) {
var con1, con2, con3, con4, con5;
/*
* Here's a switch to handle specific # of conditions, because for-loops
* don't seem to be allowed here (at least in the ways I've tried so far).
*
* If you know how to use for-loop for this, PLEASE do tell!
*/
switch(xConds.length) {
case 1: {
con1 = conds[0];
return macro {
if (!$con1) $xBool;
}
}
case 2: {
con1 = conds[0];
con2 = conds[1];
return macro {
if (!$con1) $xBool;
else if (!$con2) $xBool;
}
}
case 3: {
con1 = conds[0];
con2 = conds[1];
con3 = conds[2];
return macro {
if (!$con1) $xBool;
else if (!$con2) $xBool;
else if (!$con3) $xBool;
}
}
// ... so on and so forth
}
return macro { trace("Unhandled length of conditions :("); };
}
}
Then, in theory it could be used like this:
class Main {
static function main() {
var isOK = true;
LazyUtils.tryUntilFalse( isOK = false, [
doSomething(),
doSomethingElse(), //Returns false, so should stop here.
doFinalThing()
]);
}
static function doSomething():Bool {
// ???
return true;
}
static function doSomethingElse():Bool {
// ???
return false;
}
static function doFinalThing():Bool {
return true;
}
}
Which should generate this condition tree:
if (!doSomething()) isOK = false;
else if (!doSomethingElse()) isOK = false;
else if (!doFinalThing()) isOK = false;
Alternatively, I suppose it could output this instead:
if(!doSomething() || !doSomethingElse() || !doFinalThing()) isOK = false;
Looking back at this now, true - it may not make much sense to write a whole macro to generate code that would be easier to type out in it's raw format.
But for the sake of learning about macros, does anyone know if multiple expressions can be passed in an Array<Expr> like I tried in the above code sample?
You probably couldn't get the xConds argument to behave like you expected because the final argument of an expression macro with the type Array<Expr> is implicitly a rest argument. That means you ended up with an array that contained a single EArrayDecl expression. This can be fixed by simply omitting the [].
Regarding generating the if-else-chain - let's take a look at EIf:
/**
An `if(econd) eif` or `if(econd) eif else eelse` expression.
**/
EIf( econd : Expr, eif : Expr, eelse : Null<Expr> );
The chain can be thought of as a singly linked list - the eelse if the first EIf should reference the next EIf and so forth, until we stop with eelse = null for the last EIf. So we want to generate this for your example (pseudo-code):
EIf(doSomething(), isOk = false, /* else */
EIf(doSomethingElse, isOk = false, /* else */
EIf(doFinalThing(), isOk = false, null)
)
)
Recursion works well for this.
Typically it's more convenient to work with reification than raw expressions like I do here, but I'm not sure the former is really possible when dynamically generating expressions like this.
import haxe.macro.Context;
import haxe.macro.Expr;
class LazyUtils {
public macro static function tryUntilFalse(setBool:Expr, conditions:Array<Expr>):Expr {
return generateIfChain(setBool, conditions);
}
private static function generateIfChain(eif:Expr, conditions:Array<Expr>):Expr {
// get the next condition
var condition = conditions.shift();
if (condition == null) {
return null; // no more conditions
}
// recurse deeper to generate the next if
var nextIf = generateIfChain(eif, conditions);
return {
expr: EIf(condition, eif, nextIf),
pos: Context.currentPos()
};
}
}
And Main.hx (mostly unchanged):
class Main {
static function main() {
var isOK = true;
LazyUtils.tryUntilFalse(isOK = false,
!doSomething(),
!doSomethingElse(), //Returns false, so should stop here.
!doFinalThing()
);
}
static function doSomething():Bool {
trace("doSomething");
return true;
}
static function doSomethingElse():Bool {
trace("doSomethingElse");
return false;
}
static function doFinalThing():Bool {
trace("doFinalThing");
return true;
}
}
To keep things simple I inverted the function call arguments with ! at the call site instead of handling that in the macro.
You can use -D dump=pretty to generate AST dumps and check what code is being generated. Here's the result:
if ((! Main.doSomething()))isOK = false else if ((! Main.doSomethingElse()))isOK = false else if ((! Main.doFinalThing()))isOK = false;
I am working on a promise chain. The first call is an $http call to check if a user exists, and then if it does, theres a bunch of .then() statements that run sequentially.
My question is this.. in that first call, i don't want to return the promise of the $http request because if the user doesn't exist, the results are just an empty array and the promise resolves, thus triggering the next action to look up information about the user. I wrote the following code...
(see the part in comments about being the important part i'm asking about)
$scope.checkIfUserExists = function() {
if (angular.isObject($scope.admin.Inductee.Contactor)) {
var handleFault = function( fault ) {
if (typeof(fault) === 'string') {
switch (fault.toUpperCase()){
case 'NODATA':
// Go ahead an save
$scope.pushInductee();
break;
case 'STATUS':
// just get the 'duplicate records check' sign off of there
// The save button is disabled by the critical error
$scope.hideSave = false;
break;
case 'ASSIGNED':
// just get the 'duplicate records check' sign off of there
// The save button is disabled by the critical error
$scope.hideSave = true;
break;
default:
$log.error(fault);
$location.path('/error/default');
}
} else {
$log.error(fault);
$location.path('/error/default');
}
};
$scope.getMatchingIndData()
.then($scope.procBusLogic)
.then($scope.pushInductee)
.catch(handleFault);
}
};
////HERE IS THE IMPORTANT PART I AM ASKING ABOUT
$scope.getMatchingIndData = function() {
var deferred = $q.defer();
var locals = {};
var checkUser = function(dupeJson){
var checkUserDeferred = $q.defer();
// abandoned promise replaced with my own
sttiJoinDataFactory.checkIfUserExistsNurseleader(dupeJson)
.then(function(results) {
var data = results.data;
if (angular.isArray(data) && data.length > 0){
var highestMatch = data[0];
for (var i = 0; i < data.length; i++) {
if (parseInt(data[i].Score) > parseInt(highestMatch.Score)) {
highestMatch = data[i];
}
}
checkUserDeferred.resolve(highestMatch);
} else {
// Reject the 'overall' promise here
// to effectively break the chain
return deferred.reject('NODATA');
}
})
.catch(function(fault) {
// Any other failure should break the chain
// of http requests at this point
return deferred.reject(fault);
});
return checkUserDeferred.promise;
},
loadindividual = function (highestMatch) {
return $http stuff about the highestmatch
// set data in locals
},
parallelLoadStatusAndInducteeData = function(individual) {
return another $http promise based on the last then()
// set data in locals
},
loadCeremonyData = function (inductees){
return another $http promise based on the last call then() // set data in locals
},
reportProblems = function( fault ) {
deferred.reject(fault);
};
checkUser($scope.generateDupJson())
.then(loadindividual, reportProblems)
.then(parallelLoadStatusAndInducteeData, reportProblems)
.then(loadCeremonyData, reportProblems)
.then(function() {
deferred.resolve(locals);
})
.catch( reportProblems );
return deferred.promise;
};
Must I take into account the abandoned promise, since I really need to promise to resolve when the data comes back, and i need to reject it if there is NODATA. This is handled in the calling function's chain.
Also, I'm aware of antipatterns here. I'm trying my best to not nest promises, maintain the chain, as well as handle exceptions.
Ok I have a few comments for you:
...
// revert if and return immediately
// to reduce indentation
if (typeof(fault) !== 'string') {
$log.error(fault);
$location.path('/error/default');
return;
}
switch (fault.toUpperCase()) {
...
You don't need deferred objects:
var checkUser = function(dupeJson){
// this is not abandoned because we are returning it
return sttiJoinDataFactory.checkIfUserExistsNurseleader(dupeJson)
.then(function(results) {
var data = results.data;
if (!angular.isArray(data) || data.length <= 0) {
return $q.reject('NODATA');
}
var highestMatch = data.reduce(function (highest, d) {
return parseInt(d.Score) > parseInt(highest.Score) ?
d : highest;
}, data[0]);
return highestMatch;
}); // you don't need catch here if you're gonna reject it again
}
...
checkUser(...)
// loadIndividual will be called
// after everything inside checkUser resolves
// so you will have your highestMatch
.then(loadIndividual)
.then(parallelLoadStatusAndInducteeData)
.then(loadCeremonyData)
// you don't need to repeat reportProblems, just catch in the end
// if anything rejects prior to this point
// reportProblems will be called
.catch(reportProblems)
...
I calling getBubblesUserAccess that returns json objects that are orderd in a special way. This results i wanna run a foreach and get other messages but there i wanna return them in "order". I know that it will run these async but there must be a way that i can force it to "sequential" execution. (above code is my last attempt to add a defer...)
Example
pseudo code - get my groups
{
"id":"016cd1fc-89a3-4e4a-9e6e-a102df1b03d9",
"parent":"53750396-7d26-41f3-913d-1b93276b9e09",
"name":"XX",
"createdBy":"c9c63080-2c5b-4e8e-a093-2cfcd628a9d0",
"hasWriteAccess":true,
"hasCreateAccess":false,
"hasDeleteAccess":false,
"hasAdminAccess":false,
"settingsBubbleId":"00000000-0000-0000-0000-000000000000"
},
{
"id":"016cd1fc-89a3-4e4a-9e6e-a102df1b03d9",
"parent":"53750396-7d26-41f3-913d-1b93276b9e09",
"name":"XX",
"createdBy":"c9c63080-2c5b-4e8e-a093-2cfcd628a9d0",
"hasWriteAccess":true,
"hasCreateAccess":false,
"hasDeleteAccess":false,
"hasAdminAccess":false,
"settingsBubbleId":"00000000-0000-0000-0000-000000000000"
}
From this result i wanna iterate over those parent id strings and call another service that respond with this.
pseudo code
for each group above call another service with parent id and get result. This result will be added to a new JSON object.
"messages":[
{
"id":"f1d1aeda-d4e2-4563-85d5-d954c335b31c",
"text":"asd",
"sent":"2015-09-10T22:31:09.897+00:00",
"sender":"6b9e404b-ef37-4d07-9267-3e7b2579003b",
"senderName":"XXX XXXX"
},
{
"id":"a7ac0432-e945-440e-91ce-185170cbf3de",
"text":"asd",
"sent":"2015-09-10T22:28:24.383+00:00",
"sender":"c9c63080-2c5b-4e8e-a093-2cfcd628a9d0",
"senderName":"ZZZZZ ZZZZ"
},
My problem is that my second foreach are running async (as it should) and i want it to resolve back in same order as first json object...
My code::
var loadBubblesAccess = function () {
if (vm.running && angular.isDefined(vm.running)) { return; }
vm.running = true;
vm.bubblesWithMessages = null;
return BubbleFactory.getBubblesUserAccess().then(function (bubblesAccessTo) {
return bubblesAccessTo;
});
},
loadSubBubbles = function (bubblesAccessTo) {
/**
* Result from chain method with all bubbles user has access to.
*/
var promiseArray = [];
//var promiseArrayError = [];
var i = 0;
/**
* Creates a defer object so that we will not resolve before for each loop has been gone thru.. async problems.
*/
var deferred = $q.defer();
angular.forEach(bubblesAccessTo, function (bubble) {
$log.error(JSON.stringify(bubblesAccessTo));
/**
* Get 20 because default thats default and cache and e-tags are done to that number..
*/
BubbleFactory.getBubbleMessages(bubble.id, 0, 20, false).then(function (data) {
i++;
if (data.messages.length > 0) {
promiseArray.push({ bubbleSortOrder: i, bubbleId: bubble.parent, bubbleName: bubble.name, bubbleMessagesId: bubble.id, bubbleMessages: smartTrim(data.messages[0].text, 400, ' ', ' ...'), bubbleMessagesSent: data.messages[0].sent });
}
else {
// console.log("YYYY::: " + bubble.parent);
promiseArray.push({ bubbleSortOrder:i, bubbleId: bubble.parent, bubbleName: bubble.name, bubbleMessagesId: bubble.id, bubbleMessages: 'Inget meddelande än..', bubbleMessagesSent: '' });
}
});
/**
* Check if we have gone thru all bubbles - when finished we resolve defer object.
*/
if(i===bubblesAccessTo.length)
{
deferred.resolve(promiseArray);
}
});
//$log.debug.log(promiseArray);
vm.bubblesWithMessages = promiseArray;
promiseArray.length = 0;
vm.running = false;
};
loadBubblesAccess().then(loadSubBubbles);
The $q service in AngularJS is described as "lightweight" because it only implements the functions 90% of people need. That keeps its code size small - at the expense of not being able to address requests like yours very easily.
If you have the option, try an alternative such as bluebird. Bluebird provides a reduce() function that can execute an array of promises serially, and return their results in the order they were requested. It makes this task straightforward because your result array will match your data array and you can match up the results very easily.
If you do NOT have that option, there is a standard (if not-exactly-simple) technique with promises where you build an array of the elements you want to promise, then call the processing function (that returns a Promise) on the first value (popped from the array). In the .finally() handler, call the processing function recursively with the next value until it is empty (or an error occurs).
Pseudo-code for this:
var valuesToProcess = [1, 2, 3],
results = [];
function processValue(val) {
myProcessingFunction(val).then(function(result) {
results.push(result);
}).catch(function(e) {
console.log('FAIL!', e);
}).finally(function() {
if (valuesToProcess.length > 0) {
processValue(valuesToProcess.shift());
} else {
// All done - do something with results here
}
});
}
// Note: No error checking done, assumes we have work to do...
processValue(valuesToProcess.shift());
You'll need to adapt this to your use-case but it's a simple technique that guarantees serial operation and result-handling.
I have the following:
$scope.$watch('duration.dayPreference',function(value){
console.log(value);
if(value=='every')
{
that.duration.days = 1;
}
else if(value=='selected')
{
//alert('test');
that.duration.days=[];
}
else if(value=='everyday')
{
that.duration.days='everyday';
}
});
this.selectDay = function (day) {
$scope.duration.dayPreference = 'selected';
//$scope.$apply();
/*if(typeof(this.duration.days)!='object')
{
this.duration.days=[];
}*/
var index = this.duration.days.indexOf(day);
if (index == -1) {
//alert('test2');
this.duration.days.push(day);
}
else {
this.duration.days.splice(index, 1);
}
}
In this, when I do $scope.duration.dayPreference = 'selected'; I expect the line below it to have the this.duration.days set to a blank array. But it doesn't. Upon a closer inspection, I found that the callback in the $watch runs after the line below the assignment.
It may be very probable that, $watch may be using some kinda timers internally. What should be the way to do it then.
The watch won't be triggered until the digest is run. This will be after your entire function is compete.
If you consider that AngularJS is itself written in JavaScript, there would be no way for it to react to your setting of a property at the time. You are using the thread yourself. It can only wait for you to finish and then react.
As for what to do instead...
Perhaps you could call that watch function manually?
Or maybe the code which expects the empty array should belong inside the watch?
Watch will trigger on the $digest, which will occur after current cycle/code finishes running. You need to figure out a way of rearranging your code that handles things asynchronously. One possible quick solution might be:
var selectedDays = [];
$scope.$watch('duration.dayPreference',function(value){
console.log(value);
if(value=='every')
{
that.duration.days = 1;
}
else if(value=='selected')
{
//alert('test');
that.duration.days = selectedDays;
}
else if(value=='everyday')
{
that.duration.days='everyday';
}
});
this.selectDay = function (day) {
$scope.duration.dayPreference = 'selected';
var index = selectedDays.indexOf(day);
if (index == -1) {
//alert('test2');
selectedDays.push(day);
}
else {
selectedDays.splice(index, 1);
}
}