Extension module now compiles and some tests pass

pull/504/head
murgatroid99 10 years ago
parent 5efd50fd98
commit 016bb50e76
  1. 2
      src/node/binding.gyp
  2. 109
      src/node/ext/call.cc
  3. 14
      src/node/ext/call.h
  4. 4
      src/node/ext/completion_queue_async_worker.cc
  5. 33
      src/node/ext/node_grpc.cc
  6. 27
      src/node/ext/server.cc
  7. 128
      src/node/test/call_test.js
  8. 37
      src/node/test/constant_test.js
  9. 81
      src/node/test/end_to_end_test.js

@ -34,11 +34,9 @@
"ext/channel.cc",
"ext/completion_queue_async_worker.cc",
"ext/credentials.cc",
"ext/event.cc",
"ext/node_grpc.cc",
"ext/server.cc",
"ext/server_credentials.cc",
"ext/tag.cc",
"ext/timeval.cc"
],
'conditions' : [

@ -55,6 +55,7 @@ namespace node {
using ::node::Buffer;
using v8::Arguments;
using v8::Array;
using v8::Boolean;
using v8::Exception;
using v8::External;
using v8::Function;
@ -80,6 +81,7 @@ bool CreateMetadataArray(
std::vector<unique_ptr<NanUtf8String> > *string_handles,
std::vector<unique_ptr<PersistentHolder> > *handles) {
NanScope();
grpc_metadata_array_init(array);
Handle<Array> keys(metadata->GetOwnPropertyNames());
for (unsigned int i = 0; i < keys->Length(); i++) {
Handle<String> current_key(keys->Get(i)->ToString());
@ -156,12 +158,12 @@ Handle<Value> ParseMetadata(const grpc_metadata_array *metadata_array) {
Handle<Value> Op::GetOpType() const {
NanEscapableScope();
return NanEscapeScope(NanNew(GetTypeString()));
return NanEscapeScope(NanNew<String>(GetTypeString()));
}
class SendMetadataOp : public Op {
public:
Handle<Value> GetNodeValue() {
Handle<Value> GetNodeValue() const {
NanEscapableScope();
return NanEscapeScope(NanTrue());
}
@ -180,14 +182,14 @@ class SendMetadataOp : public Op {
return true;
}
protected:
std::string GetTypeString() {
std::string GetTypeString() const {
return "send metadata";
}
};
class SendMessageOp : public Op {
public:
Handle<Value> GetNodeValue() {
Handle<Value> GetNodeValue() const {
NanEscapableScope();
return NanEscapeScope(NanTrue());
}
@ -197,20 +199,22 @@ class SendMessageOp : public Op {
if (!Buffer::HasInstance(value)) {
return false;
}
out->data.send_message = BufferToByteBuffer(obj->Get(type));
out->data.send_message = BufferToByteBuffer(value);
Persistent<Value> handle;
NanAssignPersistent(handle, value);
handles->push_back(unique_ptr<PersistentHolder>(
new PersistentHolder(handle)));
return true;
}
protected:
std::string GetTypeString() {
std::string GetTypeString() const {
return "send message";
}
};
class SendClientCloseOp : public Op {
public:
Handle<Value> GetNodeValue() {
Handle<Value> GetNodeValue() const {
NanEscapableScope();
return NanEscapeScope(NanTrue());
}
@ -220,14 +224,14 @@ class SendClientCloseOp : public Op {
return true;
}
protected:
std::string GetTypeString() {
std::string GetTypeString() const {
return "client close";
}
};
class SendServerStatusOp : public Op {
public:
Handle<Value> GetNodeValue() {
Handle<Value> GetNodeValue() const {
NanEscapableScope();
return NanEscapeScope(NanTrue());
}
@ -265,10 +269,10 @@ class SendServerStatusOp : public Op {
return true;
}
protected:
std::string GetTypeString() {
std::string GetTypeString() const {
return "send status";
}
}
};
class GetMetadataOp : public Op {
public:
@ -289,10 +293,11 @@ class GetMetadataOp : public Op {
std::vector<unique_ptr<NanUtf8String> > *strings,
std::vector<unique_ptr<PersistentHolder> > *handles) {
out->data.recv_initial_metadata = &recv_metadata;
return true;
}
protected:
std::string GetTypeString() {
std::string GetTypeString() const {
return "metadata";
}
@ -323,7 +328,7 @@ class ReadMessageOp : public Op {
}
protected:
std::string GetTypeString() {
std::string GetTypeString() const {
return "read";
}
@ -334,12 +339,13 @@ class ReadMessageOp : public Op {
class ClientStatusOp : public Op {
public:
ClientStatusOp() {
grpc_metadata_array_init(&metadata);
grpc_metadata_array_init(&metadata_array);
status_details = NULL;
details_capacity = 0;
}
~ClientStatusOp() {
gprc_metadata_array_destroy(&metadata_array);
grpc_metadata_array_destroy(&metadata_array);
gpr_free(status_details);
}
@ -357,7 +363,7 @@ class ClientStatusOp : public Op {
NanEscapableScope();
Handle<Object> status_obj = NanNew<Object>();
status_obj->Set(NanNew("code"), NanNew<Number>(status));
if (event->data.finished.details != NULL) {
if (status_details != NULL) {
status_obj->Set(NanNew("details"), String::New(status_details));
}
status_obj->Set(NanNew("metadata"), ParseMetadata(&metadata_array));
@ -378,7 +384,7 @@ class ServerCloseResponseOp : public Op {
public:
Handle<Value> GetNodeValue() const {
NanEscapableScope();
NanEscapeScope(NanNew<Boolean>(cancelled));
return NanEscapeScope(NanNew<Boolean>(cancelled));
}
bool ParseOp(Handle<Value> value, grpc_op *out,
@ -397,27 +403,43 @@ class ServerCloseResponseOp : public Op {
int cancelled;
};
struct tag {
tag(NanCallback *callback, std::vector<unique_ptr<Op> > *ops,
std::vector<unique_ptr<PersistentHolder> > *handles,
std::vector<unique_ptr<NanUtf8String> > *strings) :
callback(callback), ops(ops), handles(handles), strings(strings){
}
~tag() {
if (strings != null) {
for (std::vector<NanUtf8String *>::iterator it = strings.begin();
it != strings.end(); ++it) {
delete *it;
}
delete strings;
}
delete callback;
delete ops;
if (handles != null) {
delete handles;
}
tag::tag(NanCallback *callback, std::vector<unique_ptr<Op> > *ops,
std::vector<unique_ptr<PersistentHolder> > *handles,
std::vector<unique_ptr<NanUtf8String> > *strings) :
callback(callback), ops(ops), handles(handles), strings(strings){
}
tag::~tag() {
delete callback;
delete ops;
if (handles != NULL) {
delete handles;
}
};
if (strings != NULL) {
delete strings;
}
}
Handle<Value> GetTagNodeValue(void *tag) {
NanEscapableScope();
struct tag *tag_struct = reinterpret_cast<struct tag *>(tag);
Handle<Object> tag_obj = NanNew<Object>();
for (std::vector<unique_ptr<Op> >::iterator it = tag_struct->ops->begin();
it != tag_struct->ops->end(); ++it) {
Op *op_ptr = it->get();
tag_obj->Set(op_ptr->GetOpType(), op_ptr->GetNodeValue());
}
return NanEscapeScope(tag_obj);
}
NanCallback GetTagCallback(void *tag) {
struct tag *tag_struct = reinterpret_cast<struct tag *>(tag);
return *tag_struct->callback;
}
void DestroyTag(void *tag) {
struct tag *tag_struct = reinterpret_cast<struct tag *>(tag);
delete tag_struct;
}
Call::Call(grpc_call *call) : wrapped_call(call) {}
@ -559,13 +581,16 @@ NAN_METHOD(Call::StartBatch) {
default:
return NanThrowError("Argument object had an unrecognized key");
}
op.ParseOp(obj.get(type), &ops[i], strings, handles);
op_vector.push_back(unique_ptr<Op>(op));
if (!op->ParseOp(obj->Get(type), &ops[i], strings, handles)) {
return NanThrowTypeError("Incorrectly typed arguments to startBatch");
}
op_vector->push_back(unique_ptr<Op>(op));
}
grpc_call_error error = grpc_call_start_batch(
call->wrapped_call, ops, nops, new struct tag(args[1].As<Function>(),
op_vector, nops, handles,
strings));
call->wrapped_call, ops, nops, new struct tag(
new NanCallback(args[1].As<Function>()),
op_vector, handles,
strings));
if (error != GRPC_CALL_OK) {
return NanThrowError("startBatch failed", error);
}

@ -35,6 +35,7 @@
#define NET_GRPC_NODE_CALL_H_
#include <memory>
#include <vector>
#include <node.h>
#include <nan.h>
@ -47,9 +48,12 @@ namespace node {
using std::unique_ptr;
v8::Handle<v8::Value> ParseMetadata(const grpc_metadata_array *metadata_array);
class PersistentHolder {
public:
explicit PersistentHolder(v8::Persistent<v8::Value> persist) : persist(persist) {
explicit PersistentHolder(v8::Persistent<v8::Value> persist) :
persist(persist) {
}
~PersistentHolder() {
@ -69,7 +73,7 @@ class Op {
v8::Handle<v8::Value> GetOpType() const;
protected:
virtual std::string GetTypeString() const;
virtual std::string GetTypeString() const = 0;
};
struct tag {
@ -83,6 +87,12 @@ struct tag {
std::vector<unique_ptr<NanUtf8String> > *strings;
};
v8::Handle<v8::Value> GetTagNodeValue(void *tag);
NanCallback GetTagCallback(void *tag);
void DestroyTag(void *tag);
/* Wrapper class for grpc_call structs. */
class Call : public ::node::ObjectWrap {
public:

@ -37,7 +37,7 @@
#include "grpc/grpc.h"
#include "grpc/support/time.h"
#include "completion_queue_async_worker.h"
#include "tag.h"
#include "call.h"
namespace grpc {
namespace node {
@ -78,7 +78,7 @@ void CompletionQueueAsyncWorker::Init(Handle<Object> exports) {
void CompletionQueueAsyncWorker::HandleOKCallback() {
NanScope();
NanCallback callback = GetTagCallback(result->tag);
Handle<Value> argv[] = {NanNull(), GetNodeValue(result->tag)};
Handle<Value> argv[] = {NanNull(), GetTagNodeValue(result->tag)};
DestroyTag(result->tag);
grpc_event_finish(result);

@ -130,37 +130,6 @@ void InitCallErrorConstants(Handle<Object> exports) {
call_error->Set(NanNew("INVALID_FLAGS"), INVALID_FLAGS);
}
void InitOpErrorConstants(Handle<Object> exports) {
NanScope();
Handle<Object> op_error = Object::New();
exports->Set(NanNew("opError"), op_error);
Handle<Value> OK(NanNew<Uint32, uint32_t>(GRPC_OP_OK));
op_error->Set(NanNew("OK"), OK);
Handle<Value> ERROR(NanNew<Uint32, uint32_t>(GRPC_OP_ERROR));
op_error->Set(NanNew("ERROR"), ERROR);
}
void InitCompletionTypeConstants(Handle<Object> exports) {
NanScope();
Handle<Object> completion_type = Object::New();
exports->Set(NanNew("completionType"), completion_type);
Handle<Value> QUEUE_SHUTDOWN(NanNew<Uint32, uint32_t>(GRPC_QUEUE_SHUTDOWN));
completion_type->Set(NanNew("QUEUE_SHUTDOWN"), QUEUE_SHUTDOWN);
Handle<Value> READ(NanNew<Uint32, uint32_t>(GRPC_READ));
completion_type->Set(NanNew("READ"), READ);
Handle<Value> WRITE_ACCEPTED(NanNew<Uint32, uint32_t>(GRPC_WRITE_ACCEPTED));
completion_type->Set(NanNew("WRITE_ACCEPTED"), WRITE_ACCEPTED);
Handle<Value> FINISH_ACCEPTED(NanNew<Uint32, uint32_t>(GRPC_FINISH_ACCEPTED));
completion_type->Set(NanNew("FINISH_ACCEPTED"), FINISH_ACCEPTED);
Handle<Value> CLIENT_METADATA_READ(
NanNew<Uint32, uint32_t>(GRPC_CLIENT_METADATA_READ));
completion_type->Set(NanNew("CLIENT_METADATA_READ"), CLIENT_METADATA_READ);
Handle<Value> FINISHED(NanNew<Uint32, uint32_t>(GRPC_FINISHED));
completion_type->Set(NanNew("FINISHED"), FINISHED);
Handle<Value> SERVER_RPC_NEW(NanNew<Uint32, uint32_t>(GRPC_SERVER_RPC_NEW));
completion_type->Set(NanNew("SERVER_RPC_NEW"), SERVER_RPC_NEW);
}
void InitOpTypeConstants(Handle<Object> exports) {
NanScope();
Handle<Object> op_type = Object::New();
@ -196,8 +165,6 @@ void init(Handle<Object> exports) {
grpc_init();
InitStatusConstants(exports);
InitCallErrorConstants(exports);
InitOpErrorConstants(exports);
InitCompletionTypeConstants(exports);
InitOpTypeConstants(exports);
grpc::node::Call::Init(exports);

@ -45,8 +45,8 @@
#include "grpc/grpc_security.h"
#include "call.h"
#include "completion_queue_async_worker.h"
#include "tag.h"
#include "server_credentials.h"
#include "timeval.h"
namespace grpc {
namespace node {
@ -55,6 +55,7 @@ using std::unique_ptr;
using v8::Arguments;
using v8::Array;
using v8::Boolean;
using v8::Date;
using v8::Exception;
using v8::Function;
using v8::FunctionTemplate;
@ -80,12 +81,12 @@ class NewCallOp : public Op {
~NewCallOp() {
grpc_call_details_destroy(&details);
grpc_metadata_array_destroy(&details);
grpc_metadata_array_destroy(&request_metadata);
}
Handle<Value> GetNodeValue() const {
NanEscapableScope();
if (*call == NULL) {
if (call == NULL) {
return NanEscapeScope(NanNull());
}
Handle<Object> obj = NanNew<Object>();
@ -99,15 +100,20 @@ class NewCallOp : public Op {
}
bool ParseOp(Handle<Value> value, grpc_op *out,
std::vector<unique_ptr<NanUtf8String> > strings,
std::vector<unique_ptr<PersistentHolder> > handles) {
std::vector<unique_ptr<NanUtf8String> > *strings,
std::vector<unique_ptr<PersistentHolder> > *handles) {
return true;
}
grpc_call *call;
grpc_call_details details;
grpc_metadata_array request_metadata;
}
protected:
std::string GetTypeString() const {
return "new call";
}
};
Server::Server(grpc_server *server) : wrapped_server(server) {}
@ -217,12 +223,13 @@ NAN_METHOD(Server::RequestCall) {
return NanThrowTypeError("requestCall can only be called on a Server");
}
Server *server = ObjectWrap::Unwrap<Server>(args.This());
Op *op = new NewCallOp();
std::vector<unique_ptr<Op> > *ops = { unique_ptr<Op>(op) };
NewCallOp *op = new NewCallOp();
std::vector<unique_ptr<Op> > *ops = new std::vector<unique_ptr<Op> >();
ops->push_back(unique_ptr<Op>(op));
grpc_call_error error = grpc_server_request_call(
server->wrapped_server, &op->call, &op->details, &op->metadata,
server->wrapped_server, &op->call, &op->details, &op->request_metadata,
CompletionQueueAsyncWorker::GetQueue(),
new struct tag(args[0].As<Function>(), ops, NULL, NULL));
new struct tag(new NanCallback(args[0].As<Function>()), ops, NULL, NULL));
if (error != GRPC_CALL_OK) {
return NanThrowError("requestCall failed", error);
}

@ -98,104 +98,80 @@ describe('call', function() {
}, TypeError);
});
});
describe('addMetadata', function() {
it('should succeed with a map from strings to string arrays', function() {
describe('startBatch', function() {
it('should fail without an object and a function', function() {
var call = new grpc.Call(channel, 'method', getDeadline(1));
assert.doesNotThrow(function() {
call.addMetadata({'key': ['value']});
assert.throws(function() {
call.startBatch();
});
assert.doesNotThrow(function() {
call.addMetadata({'key1': ['value1'], 'key2': ['value2']});
assert.throws(function() {
call.startBatch({});
});
assert.throws(function() {
call.startBatch(null, function(){});
});
});
it('should succeed with a map from strings to buffer arrays', function() {
it.skip('should succeed with an empty object', function(done) {
var call = new grpc.Call(channel, 'method', getDeadline(1));
assert.doesNotThrow(function() {
call.addMetadata({'key': [new Buffer('value')]});
});
assert.doesNotThrow(function() {
call.addMetadata({'key1': [new Buffer('value1')],
'key2': [new Buffer('value2')]});
call.startBatch({}, function(err) {
assert.ifError(err);
done();
});
});
});
it('should fail with other parameter types', function() {
});
describe('startBatch with metadata', function() {
it('should succeed with a map of strings to string arrays', function(done) {
var call = new grpc.Call(channel, 'method', getDeadline(1));
assert.throws(function() {
call.addMetadata();
assert.doesNotThrow(function() {
var batch = {};
batch[grpc.opType.SEND_INITIAL_METADATA] = {'key1': ['value1'],
'key2': ['value2']};
call.startBatch(batch, function(err, resp) {
assert.ifError(err);
assert.deepEqual(resp, {'send metadata': true});
done();
});
});
assert.throws(function() {
call.addMetadata(null);
}, TypeError);
assert.throws(function() {
call.addMetadata('value');
}, TypeError);
assert.throws(function() {
call.addMetadata(5);
}, TypeError);
});
it('should fail if invoke was already called', function(done) {
it('should succeed with a map of strings to buffer arrays', function(done) {
var call = new grpc.Call(channel, 'method', getDeadline(1));
call.invoke(function() {},
function() {done();},
0);
assert.throws(function() {
call.addMetadata({'key': ['value']});
}, function(err) {
return err.code === grpc.callError.ALREADY_INVOKED;
assert.doesNotThrow(function() {
var batch = {};
batch[grpc.opType.SEND_INITIAL_METADATA] = {
'key1': [new Buffer('value1')],
'key2': [new Buffer('value2')]
};
call.startBatch(batch, function(err, resp) {
assert.ifError(err);
assert.deepEqual(resp, {'send metadata': true});
done();
});
});
// Cancel to speed up the test
call.cancel();
});
});
describe('invoke', function() {
it('should fail with fewer than 3 arguments', function() {
it('should fail with other parameter types', function() {
var call = new grpc.Call(channel, 'method', getDeadline(1));
assert.throws(function() {
call.invoke();
}, TypeError);
assert.throws(function() {
call.invoke(function() {});
}, TypeError);
assert.throws(function() {
call.invoke(function() {},
function() {});
}, TypeError);
});
it('should work with 2 args and an int', function(done) {
assert.doesNotThrow(function() {
var call = new grpc.Call(channel, 'method', getDeadline(1));
call.invoke(function() {},
function() {done();},
0);
// Cancel to speed up the test
call.cancel();
var batch = {};
batch[grpc.opType.SEND_INITIAL_METADATA] = undefined;
call.startBatch(batch, function(){});
});
});
it('should reject incorrectly typed arguments', function() {
var call = new grpc.Call(channel, 'method', getDeadline(1));
assert.throws(function() {
call.invoke(0, 0, 0);
var batch = {};
batch[grpc.opType.SEND_INITIAL_METADATA] = null;
call.startBatch(batch, function(){});
}, TypeError);
assert.throws(function() {
call.invoke(function() {},
function() {}, 'test');
});
});
});
describe('serverAccept', function() {
it('should fail with fewer than 1 argument1', function() {
var call = new grpc.Call(channel, 'method', getDeadline(1));
assert.throws(function() {
call.serverAccept();
var batch = {};
batch[grpc.opType.SEND_INITIAL_METADATA] = 'value';
call.startBatch(batch, function(){});
}, TypeError);
});
it('should return an error when called on a client Call', function() {
var call = new grpc.Call(channel, 'method', getDeadline(1));
assert.throws(function() {
call.serverAccept(function() {});
}, function(err) {
return err.code === grpc.callError.NOT_ON_CLIENT;
});
var batch = {};
batch[grpc.opType.SEND_INITIAL_METADATA] = 5;
call.startBatch(batch, function(){});
}, TypeError);
});
});
describe('cancel', function() {

@ -76,31 +76,6 @@ var callErrorNames = [
'INVALID_FLAGS'
];
/**
* List of all op error names
* @const
* @type {Array.<string>}
*/
var opErrorNames = [
'OK',
'ERROR'
];
/**
* List of all completion type names
* @const
* @type {Array.<string>}
*/
var completionTypeNames = [
'QUEUE_SHUTDOWN',
'READ',
'WRITE_ACCEPTED',
'FINISH_ACCEPTED',
'CLIENT_METADATA_READ',
'FINISHED',
'SERVER_RPC_NEW'
];
describe('constants', function() {
it('should have all of the status constants', function() {
for (var i = 0; i < statusNames.length; i++) {
@ -114,16 +89,4 @@ describe('constants', function() {
'call error missing: ' + callErrorNames[i]);
}
});
it('should have all of the op errors', function() {
for (var i = 0; i < opErrorNames.length; i++) {
assert(grpc.opError.hasOwnProperty(opErrorNames[i]),
'op error missing: ' + opErrorNames[i]);
}
});
it('should have all of the completion types', function() {
for (var i = 0; i < completionTypeNames.length; i++) {
assert(grpc.completionType.hasOwnProperty(completionTypeNames[i]),
'completion type missing: ' + completionTypeNames[i]);
}
});
});

@ -110,52 +110,61 @@ describe('end-to-end', function() {
assert.strictEqual(event.data, grpc.opError.OK);
});
});
it('should successfully send and receive metadata', function(complete) {
var done = multiDone(complete, 2);
it.only('should successfully send and receive metadata', function(done) {
debugger;
var deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 3);
var status_text = 'xyz';
var call = new grpc.Call(channel,
'dummy_method',
deadline);
call.addMetadata({'client_key': ['client_value']});
call.invoke(function(event) {
assert.strictEqual(event.type,
grpc.completionType.CLIENT_METADATA_READ);
assert.strictEqual(event.data.server_key[0].toString(), 'server_value');
},function(event) {
assert.strictEqual(event.type, grpc.completionType.FINISHED);
var status = event.data;
assert.strictEqual(status.code, grpc.status.OK);
assert.strictEqual(status.details, status_text);
var client_batch = {};
client_batch[grpc.opType.SEND_INITIAL_METADATA] = {
'client_key': ['client_value']
};
client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(client_batch, function(err, response) {
assert.ifError(err);
assert.deepEqual(response, {
'send metadata': true,
'client close': true,
'metadata': {'server_key': [new Buffer('server_value')]},
'status': {
'code': grpc.status.OK,
'details': status_text
}
});
done();
}, 0);
});
server.requestCall(function(event) {
assert.strictEqual(event.type, grpc.completionType.SERVER_RPC_NEW);
assert.strictEqual(event.data.metadata.client_key[0].toString(),
server.requestCall(function(err, call_details) {
var new_call = call_details['new call'];
assert.notEqual(new_call, null);
assert.strictEqual(new_call.metadata.client_key[0].toString(),
'client_value');
var server_call = event.call;
var server_call = new_call.call;
assert.notEqual(server_call, null);
server_call.serverAccept(function(event) {
assert.strictEqual(event.type, grpc.completionType.FINISHED);
}, 0);
server_call.addMetadata({'server_key': ['server_value']});
server_call.serverEndInitialMetadata(0);
server_call.startWriteStatus(
grpc.status.OK,
status_text,
function(event) {
assert.strictEqual(event.type,
grpc.completionType.FINISH_ACCEPTED);
assert.strictEqual(event.data, grpc.opError.OK);
done();
});
});
call.writesDone(function(event) {
assert.strictEqual(event.type,
grpc.completionType.FINISH_ACCEPTED);
assert.strictEqual(event.data, grpc.opError.OK);
var server_batch = {};
server_batch[grpc.opType.SEND_INITIAL_METADATA] = {
'server_key': ['server_value']
};
server_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
'metadata': {},
'code': grpc.status.OK,
'details': status_text
};
server_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
console.log(server_batch);
server_call.startBatch(server_batch, function(err, response) {
assert.ifError(err);
assert.deepEqual(response, {
'send metadata': true,
'send status': true,
'cancelled': false
});
});
});
});
it('should send and receive data without error', function(complete) {

Loading…
Cancel
Save