UNPKG

liveview

Version:

Titanium Live Realtime App Development

765 lines (671 loc) 19 kB
(function () { 'use strict'; /* * Event Emitters */ /** * Initialize a new `Emitter`. * * @param {Object} obj Object to be mixed in to emitter * @returns {Emitter} * @public */ function Emitter(obj) { if (obj) { return mixin(obj); } } /** * Mixin the emitter properties. * * @param {Object} obj object to be mixed in * @return {Object} object with Emitter properties mixed in * @private */ function mixin(obj) { for (const key in Emitter.prototype) { obj[key] = Emitter.prototype[key]; } return obj; } /** * Listen on the given `event` with `fn`. * * @param {string} event event name to hook callback to * @param {Function} fn callback function * @return {Emitter} this * @public */ Emitter.prototype.on = function (event, fn) { this._callbacks = this._callbacks || {}; (this._callbacks[event] = this._callbacks[event] || []) .push(fn); return this; }; /** * Adds an `event` listener that will be invoked a single * time then automatically removed. * * @param {string} event event name to hook callback to * @param {Function} fn callback function * @return {Emitter} this * @public */ Emitter.prototype.once = function (event, fn) { const self = this; this._callbacks = this._callbacks || {}; /** * single-fire callback for event */ function on() { self.off(event, on); fn.apply(this, arguments); } fn._off = on; this.on(event, on); return this; }; /** * Remove the given callback for `event` or all * registered callbacks. * * @param {string} event event name to remove callback from * @param {Function} fn callback function * @return {Emitter} this * @public */ Emitter.prototype.off = function (event, fn) { this._callbacks = this._callbacks || {}; let callbacks = this._callbacks[event]; if (!callbacks) { return this; } // remove all handlers if (arguments.length === 1) { delete this._callbacks[event]; return this; } // remove specific handler const i = callbacks.indexOf(fn._off || fn); if (~i) { callbacks.splice(i, 1); } return this; }; /** * Emit `event` with the given args. * * @param {string} event event name * @return {Emitter} * @public */ Emitter.prototype.emit = function (event) { this._callbacks = this._callbacks || {}; const args = [].slice.call(arguments, 1); let callbacks = this._callbacks[event]; if (callbacks) { callbacks = callbacks.slice(0); for (let i = 0, len = callbacks.length; i < len; ++i) { callbacks[i].apply(this, args); } } return this; }; /** * Return array of callbacks for `event`. * * @param {string} event event name * @return {Array} array of callbacks registered for that event * @public */ Emitter.prototype.listeners = function (event) { this._callbacks = this._callbacks || {}; return this._callbacks[event] || []; }; /** * Check if this emitter has `event` handlers. * * @param {string} event event name * @return {boolean} * @public */ Emitter.prototype.hasListeners = function (event) { return !!this.listeners(event).length; }; /** * Initialize a new `Process`. * @returns {Process} * @public */ function Process() { if (!(this instanceof Process)) { return new Process(); } this.title = 'titanium'; this.version = ''; this.moduleLoadList = []; this.versions = {}; this.arch = Ti.Platform.architecture; this.platform = Ti.Platform.osname; this.hardware = ('' + Ti.Platform.model).replace('google_'); } // inherit from EventEmitter Object.setPrototypeOf(Process.prototype, Emitter.prototype); /** * [Socket description] * @param {Object} opts [description] * @returns {Socket} */ function Socket(opts) { if (!(this instanceof Socket)) { return new Socket(opts); } opts = opts || {}; this.timeout = 5000; this.host = opts.host; this.port = opts.port; this.retry = opts.retry; this.bytesRead = 0; this.bytesWritten = 0; this.ignore = []; } /** * Inherit from `Emitter.prototype`. */ Object.setPrototypeOf(Socket.prototype, Emitter.prototype); /** * [connect description] * @param {Object} opts [description] * @param {Function} fn [description] */ Socket.prototype.connect = function (opts, fn) { opts = opts || {}; if (typeof opts === 'function') { fn = opts; opts = {}; } const self = this; self.host = opts.host || self.host || '127.0.0.1'; self.port = opts.port || self.port; self.retry = opts.retry || self.retry; const reConnect = !!opts.reConnect; this._proxy = Ti.Network.Socket.createTCP({ host: self.host, port: self.port, /** * [description] * @param {Object} e [description] */ connected: function (e) { self.connected = true; self._connection = e.socket; fn && fn(e); self.emit(((reConnect) ? 'reconnect' : 'connect'), e); Ti.Stream.pump(e.socket, function (e) { if (e.bytesProcessed < 0 || !!e.errorStatus) { self._proxy.close(); self.close(true); return; } else { self.emit('data', '' + e.buffer); } }, 1024, true); }, /** * [description] * @param {Object} e [description] * @returns {undefined} */ error: function (e) { if (!~self.ignore.indexOf(e.code)) { return self.emit('error', e); } self.emit('error ignored', e); } }); this._proxy.connect(); }; /** * [close description] * @param {boolean} serverEnded [description] */ Socket.prototype.close = function (serverEnded) { const self = this; self.connected = false; self.closing = !serverEnded; if (self.closing) { self.write(function () { self._proxy.close(); self.emit('close'); }); return; } const retry = ~~self.retry; self.emit('end'); if (!retry) { return; } setTimeout(function () { self.emit('reconnecting'); self.connect({ reConnect: true }); }, retry); }; /** * [description] * @param {string} data [description] * @param {Function} fn [description] */ Socket.prototype.write = function (data, fn) { if (typeof data === 'function') { fn = data; data = null; } data = (data) ? ('' + data) : ''; const msg = Ti.createBuffer({ value: data }); const callback = fn || function () {}; Ti.Stream.write(this._connection, msg, function () { callback([].slice(arguments)); }); }; /** * [setKeepAlive description] * @param {boolean} enable [description] * @param {number} initialDelay [description] */ Socket.prototype.setKeepAlive = function (enable, initialDelay) { const self = this; if (!enable) { self._keepAlive && clearInterval(self._keepAlive); self._keepAlive = null; return; } self._keepAlive = setInterval(function () { self.write('ping'); }, initialDelay || 300000); }; /** * Initialize a new `Module`. * @param {string} id The module identifier * @public */ function Module(id) { this.filename = id + '.js'; this.id = id; if (process.platform === 'ipad') { this.platform = 'iphone'; } else if (process.platform === 'windowsphone' || process.platform === 'windowsstore') { this.platform = 'windows'; } else { this.platform = process.platform; } this.exports = {}; this.loaded = false; } function L(name, filler) { return (Module._globalCtx.localeStrings[Ti.Locale.currentLanguage] || {})[name] || filler || name; } // global namespace const global$1 = Module._global = Module.global = {}; // main process const process = global$1.process = new Process(); process.on('uncaughtException', function (err) { console.log('[LiveView] Error Evaluating', err.module, '@ Line:', err.error.line); // console.error('Line ' + err.error.line, ':', err.source[err.error.line]); console.error('' + err.error); console.error('File:', err.module); console.error('Line:', err.error.line); console.error('SourceId:', err.error.sourceId); console.error('Backtrace:\n', ('' + err.error.backtrace).replace(/'\n'/g, '\n')); }); // set environment type global$1.ENV = 'liveview'; // set logging global$1.logging = false; // catch uncaught errors global$1.CATCH_ERRORS = true; // module cache Module._cache = {}; /** * place holder for native require until patched * * @private */ Module._requireNative = function () { throw new Error('Module.patch must be run first'); }; /** * place holder for native require until patched * * @private */ Module._includeNative = function () { throw new Error('Module.patch must be run first'); }; /** * replace built in `require` function * * @param {Object} globalCtx Global context * @param {string} url The URL to use (default is '127.0.0.1', or '10.0.2.2' on android emulator) * @param {number} port The port to use (default is 8324) * @private */ Module.patch = function (globalCtx, url, port) { const defaultURL = (process.platform === 'android' && process.hardware === 'sdk') ? '10.0.2.2' : (Ti.Platform.model === 'Simulator' ? '127.0.0.1' : 'FSERVER_HOST'); Module._globalCtx = globalCtx; global$1._globalCtx = globalCtx; Module._url = url || defaultURL; Module._port = parseInt(port, 10) || 8324; Module._requireNative = require; Module.evtServer && Module.evtServer.close(); Module._compileList = []; // FIX for android bug try { Ti.App.Properties.setBool('ti.android.bug2373.finishfalseroot', false); } catch (e) { // ignore } globalCtx.localeStrings = Module.require('localeStrings'); Module.connectServer(); }; /** * [reload description] */ Module.global.reload = function () { try { Module.evtServer._proxy.close(); console.log('[LiveView] Reloading App'); Ti.App._restart(); } catch (e) { console.log('[LiveView] Reloading App via Legacy Method'); Module.require('app'); } }; /** * [description] */ Module.connectServer = function () { let retryInterval = null; const client = Module.evtServer = new Socket({ host: Module._url, port: parseInt('ESERVER_PORT', 10) }, function () { console.log('[LiveView]', 'Connected to Event Server'); }); client.on('close', function () { console.log('[LiveView]', 'Closed Previous Event Server client'); }); client.on('connect', function () { if (retryInterval !== null) { clearInterval(retryInterval); console.log('[LiveView]', 'Reconnected to Event Server'); } }); client.on('data', function (data) { if (!data) { return; } try { const evt = JSON.parse('' + data); if (evt.type === 'event' && evt.name === 'reload') { Module._cache = {}; Module.global.reload(); } } catch (e) { /* discard non JSON data for now */ } }); client.on('end', function () { console.error('[LiveView]', 'Disconnected from Event Server'); retryInterval = setInterval(function () { console.log('[LiveView]', 'Attempting reconnect to Event Server'); client.connect(); }, 2000); }); client.on('error', function (e) { let err = e.error; const code = ~~e.code; if (retryInterval !== null && code === 61) { return; } if (code === 61) { err = 'Event Server unavailable. Connection Refused @ ' + Module._url + ':' + Module._port + '\n[LiveView] Please ensure your device and computer are on the same network and the port is not blocked.'; } throw new Error('[LiveView] ' + err); }); client.connect(); Module.require('app'); }; /** * include script loader * @param {string} ctx context * @param {string} id module identifier * @public */ Module.include = function (ctx, id) { const file = id.replace('.js', ''), src = Module.prototype._getRemoteSource(file, 10000); eval.call(ctx, src); // eslint-disable-line no-eval }; /** * convert relative to absolute path * @param {string} parent parent file path * @param {string} relative relative path in require * @return {string} absolute path of the required file * @public */ Module.toAbsolute = function (parent, relative) { let newPath = parent.split('/'), parts = relative.split('/'); newPath.pop(); for (let i = 0; i < parts.length; i++) { if (parts[i] === '.') { continue; } if (parts[i] === '..') { newPath.pop(); } else { newPath.push(parts[i]); } } return newPath.join('/'); }; /** * commonjs module loader * @param {string} id module identifier * @returns {Object} * @public */ Module.require = function (id) { let fullPath = id; if (fullPath.indexOf('./') === 0 || fullPath.indexOf('../') === 0) { const parent = Module._compileList[Module._compileList.length - 1]; fullPath = Module.toAbsolute(parent, fullPath); } const cached = Module.getCached(fullPath) || Module.getCached(fullPath.replace('/index', '')) || Module.getCached(fullPath + '/index'); if (cached) { return cached.exports; } if (!Module.exists(fullPath)) { if (fullPath.indexOf('/') === 0 && Module.exists(fullPath + '/index')) { fullPath += '/index'; } else { const hlDir = '/hyperloop/'; if (fullPath.indexOf('.*') !== -1) { fullPath = id.slice(0, id.length - 2); } const modLowerCase = fullPath.toLowerCase(); if (Module.exists(hlDir + fullPath)) { fullPath = hlDir + fullPath; } else if (Module.exists(hlDir + modLowerCase)) { fullPath = hlDir + modLowerCase; } else if (fullPath.indexOf('.') === -1 && Module.exists(hlDir + fullPath + '/' + fullPath)) { fullPath = hlDir + fullPath + '/' + fullPath; } else if (fullPath.indexOf('.') === -1 && Module.exists(hlDir + modLowerCase + '/' + modLowerCase)) { fullPath = hlDir + modLowerCase + '/' + modLowerCase; } else { const lastIndex = fullPath.lastIndexOf('.'); const tempPath = hlDir + fullPath.slice(0, lastIndex) + '$' + fullPath.slice(lastIndex + 1); if (Module.exists(fullPath)) { fullPath = tempPath; } } } } const freshModule = new Module(fullPath); freshModule.cache(); freshModule._compile(); return freshModule.exports; }; /** * [getCached description] * @param {string} id moduel identifier * @return {Module} cached module * * @public */ Module.getCached = function (id) { return Module._cache[id]; }; /** * check if module file exists * * @param {string} id module identifier * @return {boolean} whether the module exists * @public */ Module.exists = function (id) { const path = Ti.Filesystem.resourcesDirectory + id + '.js', file = Ti.Filesystem.getFile(path); if (file.exists()) { return true; } if (!this.platform) { return false; } const pFolderPath = Ti.Filesystem.resourcesDirectory + '/' + this.platform + '/' + id + '.js'; const pFile = Ti.Filesystem.getFile(pFolderPath); return pFile.exists(); }; /** * shady xhrSync request * * @param {string} file file to load * @param {number} timeout in milliseconds * @return {(string|boolean)} file contents if successful, false if not * @private */ Module.prototype._getRemoteSource = function (file, timeout) { const expireTime = new Date().getTime() + timeout; const request = Ti.Network.createHTTPClient({ waitsForConnectivity: true }); let rsp = null; let done = false; const url = 'http://' + Module._url + ':' + Module._port + '/' + (file || this.id) + '.js'; request.cache = false; request.open('GET', url); request.setRequestHeader('x-platform', this.platform); request.send(); // // Windows only private API: _waitForResponse() waits for the response from the server. // if (this.platform === 'windows' && request._waitForResponse) { request._waitForResponse(); if (request.readyState === 4 || request.status === 404) { rsp = request.status === 200 ? request.responseText : false; } else { throw new Error('[LiveView] File Server unavailable. Host Unreachable @ ' + Module._url + ':' + Module._port + '\n[LiveView] Please ensure your device and computer are on the same network and the port is not blocked.'); } done = true; } while (!done) { if (request.readyState === 4 || request.status === 404) { rsp = (request.status === 200) ? request.responseText : false; done = true; } else if ((expireTime - (new Date()).getTime()) <= 0) { rsp = false; done = true; throw new Error('[LiveView] File Server unavailable. Host Unreachable @ ' + Module._url + ':' + Module._port + '\n[LiveView] Please ensure your device and computer are on the same network and the port is not blocked.'); } } return rsp; }; /** * get module file source text * @return {string} * @private */ Module.prototype._getSource = function () { let id = this.id; const isRemote = /^(http|https)$/.test(id) || (global$1.ENV === 'liveview'); if (isRemote) { return this._getRemoteSource(null, 10000); } else { if (id === 'app') { id = '_app'; } const file = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, id + '.js'); return (file.read() || {}).text; } }; /** * wrap module source text in commonjs anon function wrapper * * @param {string} source The raw source we're wrapping in an IIFE * @return {string} * @private */ Module._wrap = function (source) { return (global$1.CATCH_ERRORS) ? Module._errWrapper[0] + source + Module._errWrapper[1] : source; }; // uncaught exception handler wrapper Module._errWrapper = [ 'try {\n', '\n} catch (err) {\nlvGlobal.process.emit("uncaughtException", {module: __filename, error: err, source: module.source});\n}' ]; /** * compile commonjs module and string to js * * @private */ Module.prototype._compile = function () { const src = this._getSource(); if (!src) { this.exports = Module._requireNative(this.id); this.loaded = true; return; } Module._compileList.push(this.id); this.source = Module._wrap(src); try { const fn = new Function('exports, require, module, __filename, __dirname, lvGlobal, L', this.source); // eslint-disable-line no-new-func fn(this.exports, Module.require, this, this.filename, this.__dirname, global$1, L); } catch (err) { process.emit('uncaughtException', { module: this.id, error: err, source: ('' + this.source).split('\n') }); } Module._compileList.pop(); this.loaded = true; }; /** * cache current module * * @public */ Module.prototype.cache = function () { this.timestamp = (new Date()).getTime(); Module._cache[this.id] = this; }; /** * liveview Titanium CommonJS require with some Node.js love and dirty hacks * Copyright (c) 2013-2017 Appcelerator */ Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) { // eslint-disable-next-line no-proto obj.__proto__ = proto; return obj; }; Module.patch(global, 'FSERVER_HOST', 'FSERVER_PORT'); // Prevent display from sleeping Titanium.App.idleTimerDisabled = true; }());