Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

asynchronous APIs #13

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions __tests__/async.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const { readFile, writeFileSync, unlinkSync, mkdirSync, rmdirSync, chmodSync } = require('../')

function getScriptFolder(context) {
const parts = context.scriptPath.split('/')
parts.pop()
return parts.join('/')
}

test('failed to read', (context) => {
const targetFile = `${getScriptFolder(context)}/test.js`
const targetDir = `${getScriptFolder(context)}/test`
return Promise.all([
new Promise((resolve) => {
readFile(targetFile, 'utf8', (err, res) => {
if (err) {
resolve(err)
return
}
resolve(res)
})
}).then(err => {
expect(err.errno).toEqual(-2) // no such a file
}),
new Promise((resolve) => {
mkdirSync(targetDir)
readFile(targetDir, 'utf8', (err, res) => {
if (err) {
resolve(err)
return
}
resolve(res)
})
}).then(err => {
expect(err.errno).toEqual(-21) // operations on directories
rmdirSync(targetDir)
}),
new Promise((resolve) => {
writeFileSync(targetFile, 'test')
chmodSync(targetFile, 0o000)
readFile(targetFile, 'utf8', (err, res) => {
if (err) {
resolve(err)
return
}
resolve(res)
})
}).then(err => {
expect(err.errno).toEqual(-13) // permission denied
chmodSync(targetFile, 0o777)
unlinkSync(targetFile)
})
])
})

test('should read a file', (context) => {
const targetFile = `${getScriptFolder(context)}/test.js`
writeFileSync(targetFile, 'test')
return new Promise((resolve, reject) => {
readFile(targetFile, 'utf8', (err, res) => {
if (err) {
reject(err)
return
}
resolve(res)
})
}).then((res) => {
expect(res).toBe('test')
unlinkSync(targetFile)
})
})
69 changes: 69 additions & 0 deletions delegate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// based on https://github.com/matt-curtis/MochaJSDelegate

module.exports = function(selectorHandlerDict) {
var uniqueClassName = 'MochaJSDelegate_DynamicClass_' + NSUUID.UUID().UUIDString()

var delegateClassDesc = MOClassDescription.allocateDescriptionForClassWithName_superclass_(uniqueClassName, NSObject)

delegateClassDesc.registerClass()

// Handler storage
var handlers = {}

// Define interface
this.setHandlerForSelector = function(selectorString, func) {
var handlerHasBeenSet = (selectorString in handlers)
var selector = NSSelectorFromString(selectorString)

handlers[selectorString] = func

if (!handlerHasBeenSet) {
/*
For some reason, Mocha acts weird about arguments:
https://github.com/logancollins/Mocha/issues/28
We have to basically create a dynamic handler with a likewise dynamic number of predefined arguments.
*/
var dynamicHandler = function() {
var functionToCall = handlers[selectorString]

if(!functionToCall) return

return functionToCall.apply(delegateClassDesc, arguments)
}

var args = [], regex = /:/g
while(match = regex.exec(selectorString)) args.push('arg' + args.length)

dynamicFunction = eval('(function(' + args.join(',') + '){ return dynamicHandler.apply(this, arguments) })')

delegateClassDesc.addInstanceMethodWithSelector_function_(selector, dynamicFunction)
}
}

this.removeHandlerForSelector = function(selectorString) {
delete handlers[selectorString]
}

this.getHandlerForSelector = function(selectorString) {
return handlers[selectorString]
}

this.getAllHandlers = function() {
return handlers
}

this.getClass = function() {
return NSClassFromString(uniqueClassName)
}

this.getClassInstance = function() {
return NSClassFromString(uniqueClassName).new()
}

// Convenience
if (typeof selectorHandlerDict === 'object') {
for (var selectorString in selectorHandlerDict) {
this.setHandlerForSelector(selectorString, selectorHandlerDict[selectorString])
}
}
}
110 changes: 100 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
// TODO: async. Should probably be done with NSFileHandle and some notifications
// TODO: file descriptor. Needs to be done with NSFileHandle
var Buffer = require('buffer').Buffer
var Delegate = require('./delegate')
var notificationCenter = NSNotificationCenter.defaultCenter()

function dispatchToNSData(dispatchData) {
return NSString.alloc().initWithData_encoding(dispatchData, NSISOLatin1StringEncoding)
.dataUsingEncoding(NSISOLatin1StringEncoding)
}

function addObserver(name, notificationType, callback) {
var option = {}
var observerInstance
option[name] = function(notification) {
// need to unregister the observer after callback
notificationCenter.removeObserver_name_object(observerInstance, notificationType, nil)
callback && callback(dispatchToNSData(notification.userInfo().NSFileHandleNotificationDataItem))
}

observerInstance = new Delegate(option).getClassInstance()
notificationCenter.addObserver_selector_name_object(observerInstance, NSSelectorFromString(name), notificationType, nil)
}

function fsError(options) {
var ERROR_MESSAGES = {
'-2': 'no such file or directory',
'-13': 'permission denied',
'-21': 'illegal operation on a directory'
}

return Object.assign(new Error(
options.code + ': '
+ ERROR_MESSAGES[options.errno] + ', '
+ options.syscall
+ (options.path ? ' \'' + options.path + '\'' : '')
), options)
}

function encodingFromOptions(options, defaultValue) {
return options && options.encoding
Expand Down Expand Up @@ -145,21 +180,76 @@ module.exports.readdirSync = function(path) {
return arr
}

module.exports.readFileSync = function(path, options) {
var encoding = encodingFromOptions(options, 'buffer')
var fns = ['readFile', 'readFileSync']
var fn = function(sync, path, options, callback) {
// handle data
var handler = function(data, options) {
var encoding = encodingFromOptions(options, 'buffer')
var buffer = Buffer.from(data)

if (encoding === 'buffer') {
return buffer
} else if (encoding === 'NSData') {
return buffer.toNSData()
} else {
return buffer.toString(encoding)
}
}
// read data
var fileManager = NSFileManager.defaultManager()
var data = fileManager.contentsAtPath(path)
var buffer = Buffer.from(data)

if (encoding === 'buffer') {
return buffer
} else if (encoding === 'NSData') {
return buffer.toNSData()
if (sync) {
return handler(fileManager.contentsAtPath(path), options)
} else {
return buffer.toString(encoding)
var fileInstance = NSFileHandle.fileHandleForReadingAtPath(path)
if (fileInstance) {
addObserver('onReadCompleted:', NSFileHandleReadCompletionNotification, function (data) {
callback && callback(null, handler(data, options))
})
fileInstance.readInBackgroundAndNotify()
} else {
var isExisted = fileManager.fileExistsAtPath(path)
var isDirectory = fileManager.fileExistsAtPath_isDirectory(path, true)
var isReadable = fileManager.isReadableFileAtPath(path)
// need to use NSFileManager to detect whether it is existed or readable
if (callback) {
if (!isExisted) {
// not existed
callback(fsError({
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: path
}))
} else if (!isReadable) {
// permission denied
callback(fsError({
errno: -13,
code: 'EACCES',
syscall: 'open',
path: path
}))
} else if (isDirectory) {
// directory
callback(fsError({
errno: -21,
code: 'EISDIR',
syscall: 'read'
}))
}
}
}
}
}

for (var i = 0; i < fns.length; i++) {
var name = fns[i]
var isSync = /sync$/i.test(name)

module.exports[name] = eval('(function(' + ['path', 'options'].concat(isSync ? [] : ['callback']).join(',') + '){'
+ 'return fn.apply(this, [' + isSync + '].concat([].slice.call(arguments)))'
+ '})')
}

module.exports.readlinkSync = function(path) {
var err = MOPointer.alloc().init()
var fileManager = NSFileManager.defaultManager()
Expand Down