diff options
| author | Andrew Branson <andrew.branson@cern.ch> | 2016-02-11 23:55:16 +0100 |
|---|---|---|
| committer | Andrew Branson <andrew.branson@cern.ch> | 2016-02-11 23:55:16 +0100 |
| commit | 29aaea2d80a9eb1715b6cddfac2d2aacf76358bd (patch) | |
| tree | 012795b6bec16c72f38d33cff46324c9a0225868 /rockworkd/libpebble/jskit | |
launchpad ~mzanetti/rockwork/trunk r87
Diffstat (limited to 'rockworkd/libpebble/jskit')
20 files changed, 3094 insertions, 0 deletions
diff --git a/rockworkd/libpebble/jskit/cacheLocalStorage.js b/rockworkd/libpebble/jskit/cacheLocalStorage.js new file mode 100644 index 0000000..22588a9 --- /dev/null +++ b/rockworkd/libpebble/jskit/cacheLocalStorage.js @@ -0,0 +1,11 @@ +//Since we don't have JS 6 support, this hack will allow us to save changes to localStorage when using dot or square bracket notation + +for (var key in localStorage) { + _jskit.localstorage.setItem(key, localStorage.getItem(key)); +} + +for (var key in _jskit.localstorage.keys()) { + if (localStorage[key] === undefined) { + _jskit.localstorage.removeItem(key); + } +} diff --git a/rockworkd/libpebble/jskit/jsfiles.qrc b/rockworkd/libpebble/jskit/jsfiles.qrc new file mode 100644 index 0000000..4dfbc1d --- /dev/null +++ b/rockworkd/libpebble/jskit/jsfiles.qrc @@ -0,0 +1,7 @@ +<RCC> + <qresource prefix="/"> + <file>typedarray.js</file> + <file>jskitsetup.js</file> + <file>cacheLocalStorage.js</file> + </qresource> +</RCC> diff --git a/rockworkd/libpebble/jskit/jskitconsole.cpp b/rockworkd/libpebble/jskit/jskitconsole.cpp new file mode 100644 index 0000000..3d6c85c --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitconsole.cpp @@ -0,0 +1,29 @@ +#include <QDebug> + +#include "jskitconsole.h" + +JSKitConsole::JSKitConsole(QObject *parent) : + QObject(parent), + l(metaObject()->className()) +{ +} + +void JSKitConsole::log(const QString &msg) +{ + qCDebug(l) << msg; +} + +void JSKitConsole::warn(const QString &msg) +{ + qCWarning(l) << msg; +} + +void JSKitConsole::error(const QString &msg) +{ + qCCritical(l) << msg; +} + +void JSKitConsole::info(const QString &msg) +{ + qCDebug(l) << msg; +} diff --git a/rockworkd/libpebble/jskit/jskitconsole.h b/rockworkd/libpebble/jskit/jskitconsole.h new file mode 100644 index 0000000..3896ae3 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitconsole.h @@ -0,0 +1,20 @@ +#ifndef JSKITCONSOLE_H +#define JSKITCONSOLE_H + +#include <QLoggingCategory> + +class JSKitConsole : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitConsole(QObject *parent=0); + + Q_INVOKABLE void log(const QString &msg); + Q_INVOKABLE void warn(const QString &msg); + Q_INVOKABLE void error(const QString &msg); + Q_INVOKABLE void info(const QString &msg); +}; + +#endif // JSKITCONSOLE_H diff --git a/rockworkd/libpebble/jskit/jskitgeolocation.cpp b/rockworkd/libpebble/jskit/jskitgeolocation.cpp new file mode 100644 index 0000000..409cda1 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitgeolocation.cpp @@ -0,0 +1,302 @@ +#include <limits> + +#include "jskitgeolocation.h" + +JSKitGeolocation::JSKitGeolocation(QJSEngine *engine) : + QObject(engine), + l(metaObject()->className()), + m_engine(engine), + m_source(0), + m_lastWatcherId(0) +{ +} + +void JSKitGeolocation::getCurrentPosition(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options) +{ + setupWatcher(successCallback, errorCallback, options, true); +} + +int JSKitGeolocation::watchPosition(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options) +{ + return setupWatcher(successCallback, errorCallback, options, false); +} + +void JSKitGeolocation::clearWatch(int watcherId) +{ + removeWatcher(watcherId); +} + +void JSKitGeolocation::handleError(QGeoPositionInfoSource::Error error) +{ + qCWarning(l) << "positioning error: " << error; + + if (m_watchers.empty()) { + qCWarning(l) << "got position error but no one is watching"; + stopAndRemove(); + } + else { + QJSValue obj; + if (error == QGeoPositionInfoSource::AccessError) { + obj = buildPositionErrorObject(PERMISSION_DENIED, "permission denied"); + } else { + obj = buildPositionErrorObject(POSITION_UNAVAILABLE, "position unavailable"); + } + + for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) { + invokeCallback(it->errorCallback, obj); + + if (it->once) { + it = m_watchers.erase(it); + } else { + it->timer.restart(); + ++it; + } + } + } +} + +void JSKitGeolocation::handlePosition(const QGeoPositionInfo &pos) +{ + qCDebug(l) << "got position at" << pos.timestamp() << "type" << pos.coordinate().type(); + + if (m_watchers.empty()) { + qCWarning(l) << "got position update but no one is watching"; + stopAndRemove(); + } + else { + QJSValue obj = buildPositionObject(pos); + + for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) { + invokeCallback(it->successCallback, obj); + + if (it->once) { + it = m_watchers.erase(it); + } else { + it->timer.restart(); + ++it; + } + } + } +} + +void JSKitGeolocation::handleTimeout() +{ + qCDebug(l) << "positioning timeout"; + + if (m_watchers.empty()) { + qCWarning(l) << "got position timeout but no one is watching"; + stopAndRemove(); + } + else { + QJSValue obj = buildPositionErrorObject(TIMEOUT, "timeout"); + + for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) { + if (it->timer.hasExpired(it->timeout)) { + qCDebug(l) << "positioning timeout for watch" << it->watcherId + << ", watch is" << it->timer.elapsed() << "ms old, timeout is" << it->timeout; + invokeCallback(it->errorCallback, obj); + + if (it->once) { + it = m_watchers.erase(it); + } else { + it->timer.restart(); + ++it; + } + } else { + ++it; + } + } + + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); + } +} + +void JSKitGeolocation::updateTimeouts() +{ + int once_timeout = -1, updates_timeout = -1; + + Q_FOREACH(const Watcher &watcher, m_watchers) { + qint64 rem_timeout = watcher.timeout - watcher.timer.elapsed(); + qCDebug(l) << "watch" << watcher.watcherId << "rem timeout" << rem_timeout; + + if (rem_timeout >= 0) { + // Make sure the limits aren't too large + rem_timeout = qMin<qint64>(rem_timeout, std::numeric_limits<int>::max()); + + if (watcher.once) { + once_timeout = once_timeout >= 0 ? qMin<int>(once_timeout, rem_timeout) : rem_timeout; + } else { + updates_timeout = updates_timeout >= 0 ? qMin<int>(updates_timeout, rem_timeout) : rem_timeout; + } + } + } + + if (updates_timeout >= 0) { + qCDebug(l) << "setting location update interval to" << updates_timeout; + m_source->setUpdateInterval(updates_timeout); + m_source->startUpdates(); + } else { + qCDebug(l) << "stopping updates"; + m_source->stopUpdates(); + } + + if (once_timeout >= 0) { + qCDebug(l) << "requesting single location update with timeout" << once_timeout; + m_source->requestUpdate(once_timeout); + } + + if (once_timeout == 0 && updates_timeout == 0) { + stopAndRemove(); + } +} + +int JSKitGeolocation::setupWatcher(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options, bool once) +{ + Watcher watcher; + watcher.successCallback = successCallback; + watcher.errorCallback = errorCallback; + watcher.highAccuracy = options.value("enableHighAccuracy", false).toBool(); + watcher.timeout = options.value("timeout", std::numeric_limits<int>::max() - 1).toInt(); + watcher.maximumAge = options.value("maximumAge", 0).toLongLong(); + watcher.once = once; + watcher.watcherId = ++m_lastWatcherId; + + qCDebug(l) << "setting up watcher, gps=" << watcher.highAccuracy << "timeout=" << watcher.timeout << "maximumAge=" << watcher.maximumAge << "once=" << watcher.once; + + if (!m_source) { + m_source = QGeoPositionInfoSource::createDefaultSource(this); + + connect(m_source, static_cast<void (QGeoPositionInfoSource::*)(QGeoPositionInfoSource::Error)>(&QGeoPositionInfoSource::error), + this, &JSKitGeolocation::handleError); + connect(m_source, &QGeoPositionInfoSource::positionUpdated, + this, &JSKitGeolocation::handlePosition); + connect(m_source, &QGeoPositionInfoSource::updateTimeout, + this, &JSKitGeolocation::handleTimeout); + } + + if (watcher.maximumAge > 0) { + QDateTime threshold = QDateTime::currentDateTime().addMSecs(-qint64(watcher.maximumAge)); + QGeoPositionInfo pos = m_source->lastKnownPosition(watcher.highAccuracy); + qCDebug(l) << "got pos timestamp" << pos.timestamp() << " but we want" << threshold; + + if (pos.isValid() && pos.timestamp() >= threshold) { + invokeCallback(watcher.successCallback, buildPositionObject(pos)); + + if (once) { + return -1; + } + } else if (watcher.timeout == 0 && once) { + // If the timeout has already expired, and we have no cached data + // Do not even bother to turn on the GPS; return error object now. + invokeCallback(watcher.errorCallback, buildPositionErrorObject(TIMEOUT, "no cached position")); + return -1; + } + } + + watcher.timer.start(); + m_watchers.append(watcher); + + qCDebug(l) << "added new watcher" << watcher.watcherId; + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); + + return watcher.watcherId; +} + +void JSKitGeolocation::removeWatcher(int watcherId) +{ + Watcher watcher; + + qCDebug(l) << "removing watcherId" << watcher.watcherId; + + for (int i = 0; i < m_watchers.size(); i++) { + if (m_watchers[i].watcherId == watcherId) { + watcher = m_watchers.takeAt(i); + break; + } + } + + if (watcher.watcherId != watcherId) { + qCWarning(l) << "watcherId not found"; + return; + } + + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); +} + +QJSValue JSKitGeolocation::buildPositionObject(const QGeoPositionInfo &pos) +{ + QJSValue obj = m_engine->newObject(); + QJSValue coords = m_engine->newObject(); + QJSValue timestamp = m_engine->toScriptValue<quint64>(pos.timestamp().toMSecsSinceEpoch()); + + coords.setProperty("latitude", m_engine->toScriptValue(pos.coordinate().latitude())); + coords.setProperty("longitude", m_engine->toScriptValue(pos.coordinate().longitude())); + if (pos.coordinate().type() == QGeoCoordinate::Coordinate3D) { + coords.setProperty("altitude", m_engine->toScriptValue(pos.coordinate().altitude())); + } else { + coords.setProperty("altitude", m_engine->toScriptValue<void*>(0)); + } + + coords.setProperty("accuracy", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::HorizontalAccuracy))); + + if (pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) { + coords.setProperty("altitudeAccuracy", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::VerticalAccuracy))); + } else { + coords.setProperty("altitudeAccuracy", m_engine->toScriptValue<void*>(0)); + } + + if (pos.hasAttribute(QGeoPositionInfo::Direction)) { + coords.setProperty("heading", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::Direction))); + } else { + coords.setProperty("heading", m_engine->toScriptValue<void*>(0)); + } + + if (pos.hasAttribute(QGeoPositionInfo::GroundSpeed)) { + coords.setProperty("speed", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::GroundSpeed))); + } else { + coords.setProperty("speed", m_engine->toScriptValue<void*>(0)); + } + + obj.setProperty("coords", coords); + obj.setProperty("timestamp", timestamp); + + return obj; +} + +QJSValue JSKitGeolocation::buildPositionErrorObject(PositionError error, const QString &message) +{ + QJSValue obj = m_engine->newObject(); + + obj.setProperty("code", m_engine->toScriptValue<unsigned short>(error)); + obj.setProperty("message", m_engine->toScriptValue(message)); + + return obj; +} + +void JSKitGeolocation::invokeCallback(QJSValue callback, QJSValue event) +{ + if (callback.isCallable()) { + qCDebug(l) << "invoking callback" << callback.toString(); + QJSValue result = callback.call(QJSValueList({event})); + + if (result.isError()) { + qCWarning(l) << "error while invoking callback: " << QString("%1:%2: %3") + .arg(result.property("fileName").toString()) + .arg(result.property("lineNumber").toInt()) + .arg(result.toString()); + } + } else { + qCWarning(l) << "callback is not callable"; + } +} + +void JSKitGeolocation::stopAndRemove() +{ + if (m_source) { + qCDebug(l) << "removing source"; + + m_source->stopUpdates(); + m_source->deleteLater(); + m_source = 0; + } +} diff --git a/rockworkd/libpebble/jskit/jskitgeolocation.h b/rockworkd/libpebble/jskit/jskitgeolocation.h new file mode 100644 index 0000000..582ab32 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitgeolocation.h @@ -0,0 +1,66 @@ +#ifndef JSKITGEOLOCATION_H +#define JSKITGEOLOCATION_H + +#include <QElapsedTimer> +#include <QGeoPositionInfoSource> +#include <QJSValue> +#include <QLoggingCategory> +#include <QJSEngine> + +class JSKitGeolocation : public QObject +{ + Q_OBJECT + QLoggingCategory l; + + struct Watcher; + +public: + explicit JSKitGeolocation(QJSEngine *engine); + + enum PositionError { + PERMISSION_DENIED = 1, + POSITION_UNAVAILABLE = 2, + TIMEOUT = 3 + }; + Q_ENUMS(PositionError); + + Q_INVOKABLE void getCurrentPosition(const QJSValue &successCallback, const QJSValue &errorCallback = QJSValue(), const QVariantMap &options = QVariantMap()); + Q_INVOKABLE int watchPosition(const QJSValue &successCallback, const QJSValue &errorCallback = QJSValue(), const QVariantMap &options = QVariantMap()); + Q_INVOKABLE void clearWatch(int watcherId); + +private slots: + void handleError(const QGeoPositionInfoSource::Error error); + void handlePosition(const QGeoPositionInfo &pos); + void handleTimeout(); + void updateTimeouts(); + +private: + int setupWatcher(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options, bool once); + void removeWatcher(int watcherId); + + QJSValue buildPositionObject(const QGeoPositionInfo &pos); + QJSValue buildPositionErrorObject(PositionError error, const QString &message = QString()); + QJSValue buildPositionErrorObject(const QGeoPositionInfoSource::Error error); + void invokeCallback(QJSValue callback, QJSValue event); + void stopAndRemove(); + +private: + QJSEngine *m_engine; + QGeoPositionInfoSource *m_source; + + struct Watcher { + QJSValue successCallback; + QJSValue errorCallback; + int watcherId; + bool once; + bool highAccuracy; + int timeout; + QElapsedTimer timer; + qlonglong maximumAge; + }; + + QList<Watcher> m_watchers; + int m_lastWatcherId; +}; + +#endif // JSKITGEOLOCATION_H diff --git a/rockworkd/libpebble/jskit/jskitlocalstorage.cpp b/rockworkd/libpebble/jskit/jskitlocalstorage.cpp new file mode 100644 index 0000000..d69b6ad --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitlocalstorage.cpp @@ -0,0 +1,117 @@ +#include <QDesktopServices> +#include <QDir> +#include <QDebug> + +#include "jskitlocalstorage.h" + +JSKitLocalStorage::JSKitLocalStorage(QJSEngine *engine, const QString &storagePath, const QUuid &uuid): + QObject(engine), + m_engine(engine), + m_storage(new QSettings(getStorageFileFor(storagePath, uuid), QSettings::IniFormat, this)) +{ +} + +int JSKitLocalStorage::length() const +{ + return m_storage->allKeys().size(); +} + +QJSValue JSKitLocalStorage::getItem(const QJSValue &key) const +{ + QVariant value = m_storage->value(key.toString()); + + if (value.isValid()) { + return QJSValue(value.toString()); + } else { + return QJSValue(QJSValue::NullValue); + } +} + +bool JSKitLocalStorage::setItem(const QJSValue &key, const QJSValue &value) +{ + m_storage->setValue(key.toString(), QVariant::fromValue(value.toString())); + return true; +} + +bool JSKitLocalStorage::removeItem(const QJSValue &key) +{ + if (m_storage->contains(key.toString())) { + m_storage->remove(key.toString()); + return true; + } else { + return false; + } +} + +void JSKitLocalStorage::clear() +{ + m_storage->clear(); +} + +QJSValue JSKitLocalStorage::key(int index) +{ + QStringList allKeys = m_storage->allKeys(); + QJSValue key(QJSValue::NullValue); + + if (allKeys.size() > index) { + key = QJSValue(allKeys[index]); + } + + return key; +} + +QJSValue JSKitLocalStorage::get(const QJSValue &proxy, const QJSValue &key) const +{ + Q_UNUSED(proxy); + return getItem(key); +} + +bool JSKitLocalStorage::set(const QJSValue &proxy, const QJSValue &key, const QJSValue &value) +{ + Q_UNUSED(proxy); + return setItem(key, value); +} + +bool JSKitLocalStorage::has(const QJSValue &proxy, const QJSValue &key) +{ + Q_UNUSED(proxy); + return m_storage->contains(key.toString()); +} + +bool JSKitLocalStorage::deleteProperty(const QJSValue &proxy, const QJSValue &key) +{ + Q_UNUSED(proxy); + return removeItem(key); +} + +QJSValue JSKitLocalStorage::keys(const QJSValue &proxy) +{ + Q_UNUSED(proxy); + + QStringList allKeys = m_storage->allKeys(); + QJSValue keyArray = m_engine->newArray(allKeys.size()); + for (int i = 0; i < allKeys.size(); i++) { + keyArray.setProperty(i, allKeys[i]); + } + + return keyArray; +} + +QJSValue JSKitLocalStorage::enumerate() +{ + return keys(0); +} + +QString JSKitLocalStorage::getStorageFileFor(const QString &storageDir, const QUuid &uuid) +{ + QDir dataDir(storageDir + "/js-storage"); + if (!dataDir.exists() && !dataDir.mkpath(dataDir.absolutePath())) { + qWarning() << "Error creating jskit storage dir"; + return QString(); + } + + QString fileName = uuid.toString(); + fileName.remove('{'); + fileName.remove('}'); + return dataDir.absoluteFilePath(fileName + ".ini"); +} diff --git a/rockworkd/libpebble/jskit/jskitlocalstorage.h b/rockworkd/libpebble/jskit/jskitlocalstorage.h new file mode 100644 index 0000000..9719f83 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitlocalstorage.h @@ -0,0 +1,40 @@ +#ifndef JSKITLOCALSTORAGE_P_H +#define JSKITLOCALSTORAGE_P_H + +#include <QSettings> +#include <QJSEngine> +#include <QUuid> + +class JSKitLocalStorage : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int length READ length) + +public: + explicit JSKitLocalStorage(QJSEngine *engine, const QString &storagePath, const QUuid &uuid); + + int length() const; + + Q_INVOKABLE QJSValue getItem(const QJSValue &key) const; + Q_INVOKABLE bool setItem(const QJSValue &key, const QJSValue &value); + Q_INVOKABLE bool removeItem(const QJSValue &key); + Q_INVOKABLE void clear(); + Q_INVOKABLE QJSValue key(int index); + + Q_INVOKABLE QJSValue get(const QJSValue &proxy, const QJSValue &key) const; + Q_INVOKABLE bool set(const QJSValue &proxy, const QJSValue &key, const QJSValue &value); + Q_INVOKABLE bool has(const QJSValue &proxy, const QJSValue &key); + Q_INVOKABLE bool deleteProperty(const QJSValue &proxy, const QJSValue &key); + Q_INVOKABLE QJSValue keys(const QJSValue &proxy=0); + Q_INVOKABLE QJSValue enumerate(); + +private: + static QString getStorageFileFor(const QString &storageDir, const QUuid &uuid); + +private: + QJSEngine *m_engine; + QSettings *m_storage; +}; + +#endif // JSKITLOCALSTORAGE_P_H diff --git a/rockworkd/libpebble/jskit/jskitmanager.cpp b/rockworkd/libpebble/jskit/jskitmanager.cpp new file mode 100644 index 0000000..04bf674 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitmanager.cpp @@ -0,0 +1,240 @@ +#include <QFile> +#include <QDir> +#include <QUrl> + +#include "jskitmanager.h" +#include "jskitpebble.h" + +JSKitManager::JSKitManager(Pebble *pebble, WatchConnection *connection, AppManager *apps, AppMsgManager *appmsg, QObject *parent) : + QObject(parent), + l(metaObject()->className()), + m_pebble(pebble), + m_connection(connection), + m_apps(apps), + m_appmsg(appmsg), + m_engine(0), + m_configurationUuid(0) +{ + connect(m_appmsg, &AppMsgManager::appStarted, this, &JSKitManager::handleAppStarted); + connect(m_appmsg, &AppMsgManager::appStopped, this, &JSKitManager::handleAppStopped); +} + +JSKitManager::~JSKitManager() +{ + if (m_engine) { + stopJsApp(); + } +} + +QJSEngine * JSKitManager::engine() +{ + return m_engine; +} + +bool JSKitManager::isJSKitAppRunning() const +{ + return m_engine != 0; +} + +QString JSKitManager::describeError(QJSValue error) +{ + return QString("%1:%2: %3") + .arg(error.property("fileName").toString()) + .arg(error.property("lineNumber").toInt()) + .arg(error.toString()); +} + +void JSKitManager::showConfiguration() +{ + if (m_engine) { + qCDebug(l) << "requesting configuration"; + m_jspebble->invokeCallbacks("showConfiguration"); + } else { + qCWarning(l) << "requested to show configuration, but JS engine is not running"; + } +} + +void JSKitManager::handleWebviewClosed(const QString &result) +{ + if (m_engine) { + QJSValue eventObj = m_engine->newObject(); + eventObj.setProperty("response", QUrl::fromPercentEncoding(result.toUtf8())); + + qCDebug(l) << "Sending" << eventObj.property("response").toString(); + m_jspebble->invokeCallbacks("webviewclosed", QJSValueList({eventObj})); + + loadJsFile(":/cacheLocalStorage.js"); + } else { + qCWarning(l) << "webview closed event, but JS engine is not running"; + } +} + +void JSKitManager::setConfigurationId(const QUuid &uuid) +{ + m_configurationUuid = uuid; +} + +AppInfo JSKitManager::currentApp() +{ + return m_curApp; +} + +void JSKitManager::handleAppStarted(const QUuid &uuid) +{ + AppInfo info = m_apps->info(uuid); + if (!info.uuid().isNull() && info.isJSKit()) { + qCDebug(l) << "Preparing to start JSKit app" << info.uuid() << info.shortName(); + + m_curApp = info; + startJsApp(); + } +} + +void JSKitManager::handleAppStopped(const QUuid &uuid) +{ + if (!m_curApp.uuid().isNull()) { + if (m_curApp.uuid() != uuid) { + qCWarning(l) << "Closed app with invalid UUID"; + } + + stopJsApp(); + m_curApp = AppInfo(); + qCDebug(l) << "App stopped" << uuid; + } +} + +void JSKitManager::handleAppMessage(const QUuid &uuid, const QVariantMap &msg) +{ + if (m_curApp.uuid() == uuid) { + qCDebug(l) << "handling app message" << uuid << msg; + + if (m_engine) { + QJSValue eventObj = m_engine->newObject(); + eventObj.setProperty("payload", m_engine->toScriptValue(msg)); + + m_jspebble->invokeCallbacks("appmessage", QJSValueList({eventObj})); + + loadJsFile(":/cacheLocalStorage.js"); + } + else { + qCDebug(l) << "but engine is stopped"; + } + } +} + +bool JSKitManager::loadJsFile(const QString &filename) +{ + Q_ASSERT(m_engine); + + QFile file(filename); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCWarning(l) << "Failed to load JS file:" << file.fileName(); + return false; + } + + qCDebug(l) << "evaluating js file" << file.fileName(); + + QJSValue result = m_engine->evaluate(QString::fromUtf8(file.readAll()), file.fileName()); + if (result.isError()) { + qCWarning(l) << "error while evaluating JS script:" << describeError(result); + return false; + } + + qCDebug(l) << "JS script evaluated"; + return true; +} + +void JSKitManager::startJsApp() +{ + if (m_engine) stopJsApp(); + + if (m_curApp.uuid().isNull()) { + qCWarning(l) << "Attempting to start JS app with invalid UUID"; + return; + } + + m_engine = new QJSEngine(this); + m_jspebble = new JSKitPebble(m_curApp, this, m_engine); + m_jsconsole = new JSKitConsole(m_engine); + m_jsstorage = new JSKitLocalStorage(m_engine, m_pebble->storagePath(), m_curApp.uuid()); + m_jsgeo = new JSKitGeolocation(m_engine); + m_jstimer = new JSKitTimer(m_engine); + m_jsperformance = new JSKitPerformance(m_engine); + + qCDebug(l) << "starting JS app" << m_curApp.shortName(); + + QJSValue globalObj = m_engine->globalObject(); + QJSValue jskitObj = m_engine->newObject(); + + jskitObj.setProperty("pebble", m_engine->newQObject(m_jspebble)); + jskitObj.setProperty("console", m_engine->newQObject(m_jsconsole)); + jskitObj.setProperty("localstorage", m_engine->newQObject(m_jsstorage)); + jskitObj.setProperty("geolocation", m_engine->newQObject(m_jsgeo)); + jskitObj.setProperty("timer", m_engine->newQObject(m_jstimer)); + jskitObj.setProperty("performance", m_engine->newQObject(m_jsperformance)); + globalObj.setProperty("_jskit", jskitObj); + + QJSValue navigatorObj = m_engine->newObject(); + navigatorObj.setProperty("language", m_engine->toScriptValue(QLocale().name())); + globalObj.setProperty("navigator", navigatorObj); + + // Set this.window = this + globalObj.setProperty("window", globalObj); + + // Shims for compatibility... + loadJsFile(":/jskitsetup.js"); + + // Polyfills... + loadJsFile(":/typedarray.js"); + + // Now the actual script + QString jsApp = m_curApp.file(AppInfo::FileTypeJsApp, HardwarePlatformUnknown); + QFile f(jsApp); + if (!f.open(QFile::ReadOnly)) { + qCWarning(l) << "Error opening" << jsApp; + return; + } + QJSValue ret = m_engine->evaluate(QString::fromUtf8(f.readAll())); + qCDebug(l) << "loaded script" << ret.toString(); + + // Setup the message callback + QUuid uuid = m_curApp.uuid(); + m_appmsg->setMessageHandler(uuid, [this, uuid](const QVariantMap &msg) { + QMetaObject::invokeMethod(this, "handleAppMessage", Qt::QueuedConnection, + Q_ARG(QUuid, uuid), + Q_ARG(QVariantMap, msg)); + + // Invoke the slot as a queued connection to give time for the ACK message + // to go through first. + + return true; + }); + + // We try to invoke the callbacks even if script parsing resulted in error... + m_jspebble->invokeCallbacks("ready"); + + loadJsFile(":/cacheLocalStorage.js"); + + if (m_configurationUuid == m_curApp.uuid()) { + qCDebug(l) << "going to launch config for" << m_configurationUuid; + showConfiguration(); + } + + m_configurationUuid = QUuid(); +} + +void JSKitManager::stopJsApp() +{ + qCDebug(l) << "stop js app" << m_curApp.uuid(); + if (!m_engine) return; // Nothing to do! + + loadJsFile(":/cacheLocalStorage.js"); + + if (!m_curApp.uuid().isNull()) { + m_appmsg->clearMessageHandler(m_curApp.uuid()); + } + + m_engine->collectGarbage(); + m_engine->deleteLater(); + m_engine = 0; +} diff --git a/rockworkd/libpebble/jskit/jskitmanager.h b/rockworkd/libpebble/jskit/jskitmanager.h new file mode 100644 index 0000000..570948e --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitmanager.h @@ -0,0 +1,72 @@ +#ifndef JSKITMANAGER_H +#define JSKITMANAGER_H + +#include <QJSEngine> +#include <QPointer> +#include <QLoggingCategory> + +#include "../appmanager.h" +#include "../watchconnection.h" +#include "../pebble.h" +#include "../appmsgmanager.h" + +#include "jskitconsole.h" +#include "jskitgeolocation.h" +#include "jskitlocalstorage.h" +#include "jskittimer.h" +#include "jskitperformance.h" + +class JSKitPebble; + +class JSKitManager : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitManager(Pebble *pebble, WatchConnection *connection, AppManager *apps, AppMsgManager *appmsg, QObject *parent = 0); + ~JSKitManager(); + + QJSEngine * engine(); + bool isJSKitAppRunning() const; + + static QString describeError(QJSValue error); + + void showConfiguration(); + void handleWebviewClosed(const QString &result); + void setConfigurationId(const QUuid &uuid); + AppInfo currentApp(); + +signals: + void appNotification(const QUuid &uuid, const QString &title, const QString &body); + void openURL(const QString &uuid, const QString &url); + +private slots: + void handleAppStarted(const QUuid &uuid); + void handleAppStopped(const QUuid &uuid); + void handleAppMessage(const QUuid &uuid, const QVariantMap &msg); + +private: + bool loadJsFile(const QString &filename); + void startJsApp(); + void stopJsApp(); + +private: + friend class JSKitPebble; + + Pebble *m_pebble; + WatchConnection *m_connection; + AppManager *m_apps; + AppMsgManager *m_appmsg; + AppInfo m_curApp; + QJSEngine *m_engine; + QPointer<JSKitPebble> m_jspebble; + QPointer<JSKitConsole> m_jsconsole; + QPointer<JSKitLocalStorage> m_jsstorage; + QPointer<JSKitGeolocation> m_jsgeo; + QPointer<JSKitTimer> m_jstimer; + QPointer<JSKitPerformance> m_jsperformance; + QUuid m_configurationUuid; +}; + +#endif // JSKITMANAGER_H diff --git a/rockworkd/libpebble/jskit/jskitpebble.cpp b/rockworkd/libpebble/jskit/jskitpebble.cpp new file mode 100644 index 0000000..a300aef --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitpebble.cpp @@ -0,0 +1,355 @@ +#include <QUrl> +#include <QCryptographicHash> +#include <QSettings> + +#include "jskitpebble.h" +#include "jskitxmlhttprequest.h" + +static const char *token_salt = "0feeb7416d3c4546a19b04bccd8419b1"; + +JSKitPebble::JSKitPebble(const AppInfo &info, JSKitManager *mgr, QObject *parent) : + QObject(parent), + l(metaObject()->className()), + m_appInfo(info), + m_mgr(mgr) +{ +} + +void JSKitPebble::addEventListener(const QString &type, QJSValue function) +{ + m_listeners[type].append(function); +} + +void JSKitPebble::removeEventListener(const QString &type, QJSValue function) +{ + if (!m_listeners.contains(type)) return; + + QList<QJSValue> &callbacks = m_listeners[type]; + for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ) { + if (it->strictlyEquals(function)) { + it = callbacks.erase(it); + } else { + ++it; + } + } + + if (callbacks.empty()) { + m_listeners.remove(type); + } +} + +void JSKitPebble::showSimpleNotificationOnPebble(const QString &title, const QString &body) +{ + qCDebug(l) << "showSimpleNotificationOnPebble" << title << body; + emit m_mgr->appNotification(m_appInfo.uuid(), title, body); +} + +uint JSKitPebble::sendAppMessage(QJSValue message, QJSValue callbackForAck, QJSValue callbackForNack) +{ + QVariantMap data = message.toVariant().toMap(); + QPointer<JSKitPebble> pebbObj = this; + uint transactionId = m_mgr->m_appmsg->nextTransactionId(); + + qCDebug(l) << "sendAppMessage" << data; + + m_mgr->m_appmsg->send( + m_appInfo.uuid(), + data, + [this, pebbObj, transactionId, callbackForAck]() mutable { + if (pebbObj.isNull()) return; + + if (callbackForAck.isCallable()) { + QJSValue event = pebbObj->buildAckEventObject(transactionId); + QJSValue result = callbackForAck.call(QJSValueList({event})); + + if (result.isError()) { + qCWarning(l) << "error while invoking ACK callback" + << callbackForAck.toString() << ":" + << JSKitManager::describeError(result); + } + } + }, + [this, pebbObj, transactionId, callbackForNack]() mutable { + if (pebbObj.isNull()) return; + + if (callbackForNack.isCallable()) { + QJSValue event = pebbObj->buildAckEventObject(transactionId, "NACK from watch"); + QJSValue result = callbackForNack.call(QJSValueList({event})); + + if (result.isError()) { + qCWarning(l) << "error while invoking NACK callback" + << callbackForNack.toString() << ":" + << JSKitManager::describeError(result); + } + } + } + ); + + return transactionId; +} + +void JSKitPebble::getTimelineToken(QJSValue successCallback, QJSValue failureCallback) +{ + //TODO actually implement this + qCDebug(l) << "call to unsupported method Pebble.getTimelineToken"; + Q_UNUSED(successCallback); + + if (failureCallback.isCallable()) { + failureCallback.call(); + } +} + +void JSKitPebble::timelineSubscribe(const QString &topic, QJSValue successCallback, QJSValue failureCallback) +{ + //TODO actually implement this + qCDebug(l) << "call to unsupported method Pebble.timelineSubscribe"; + Q_UNUSED(topic); + Q_UNUSED(successCallback); + + if (failureCallback.isCallable()) { + failureCallback.call(); + } +} + +void JSKitPebble::timelineUnsubscribe(const QString &topic, QJSValue successCallback, QJSValue failureCallback) +{ + //TODO actually implement this + qCDebug(l) << "call to unsupported method Pebble.timelineUnsubscribe"; + Q_UNUSED(topic); + Q_UNUSED(successCallback); + + if (failureCallback.isCallable()) { + failureCallback.call(); + } +} + +void JSKitPebble::timelineSubscriptions(QJSValue successCallback, QJSValue failureCallback) +{ + //TODO actually implement this + qCDebug(l) << "call to unsupported method Pebble.timelineSubscriptions"; + Q_UNUSED(successCallback); + + if (failureCallback.isCallable()) { + failureCallback.call(); + } +} + + +QString JSKitPebble::getAccountToken() const +{ + // We do not have any account system, so we just fake something up. + QCryptographicHash hasher(QCryptographicHash::Md5); + + hasher.addData(token_salt, strlen(token_salt)); + hasher.addData(m_appInfo.uuid().toByteArray()); + + QSettings settings; + QString token = settings.value("accountToken").toString(); + + if (token.isEmpty()) { + token = QUuid::createUuid().toString(); + qCDebug(l) << "created new account token" << token; + settings.setValue("accountToken", token); + } + + hasher.addData(token.toLatin1()); + + QString hash = hasher.result().toHex(); + qCDebug(l) << "returning account token" << hash; + + return hash; +} + +QString JSKitPebble::getWatchToken() const +{ + QCryptographicHash hasher(QCryptographicHash::Md5); + + hasher.addData(token_salt, strlen(token_salt)); + hasher.addData(m_appInfo.uuid().toByteArray()); + hasher.addData(m_mgr->m_pebble->serialNumber().toLatin1()); + + QString hash = hasher.result().toHex(); + qCDebug(l) << "returning watch token" << hash; + + return hash; +} + +QJSValue JSKitPebble::getActiveWatchInfo() const +{ + QJSValue watchInfo = m_mgr->m_engine->newObject(); + + switch (m_mgr->m_pebble->hardwarePlatform()) { + case HardwarePlatformBasalt: + watchInfo.setProperty("platform", "basalt"); + break; + + case HardwarePlatformChalk: + watchInfo.setProperty("platform", "chalk"); + break; + + default: + watchInfo.setProperty("platform", "aplite"); + break; + } + + switch (m_mgr->m_pebble->model()) { + case ModelTintinWhite: + watchInfo.setProperty("model", "pebble_white"); + break; + + case ModelTintinRed: + watchInfo.setProperty("model", "pebble_red"); + break; + + case ModelTintinOrange: + watchInfo.setProperty("model", "pebble_orange"); + break; + + case ModelTintinGrey: + watchInfo.setProperty("model", "pebble_grey"); + break; + + case ModelBiancaSilver: + watchInfo.setProperty("model", "pebble_steel_silver"); + break; + + case ModelBiancaBlack: + watchInfo.setProperty("model", "pebble_steel_black"); + break; + + case ModelTintinBlue: + watchInfo.setProperty("model", "pebble_blue"); + break; + + case ModelTintinGreen: + watchInfo.setProperty("model", "pebble_green"); + break; + + case ModelTintinPink: + watchInfo.setProperty("model", "pebble_pink"); + break; + + case ModelSnowyWhite: + watchInfo.setProperty("model", "pebble_time_white"); + break; + + case ModelSnowyBlack: + watchInfo.setProperty("model", "pebble_time_black"); + break; + + case ModelSnowyRed: + watchInfo.setProperty("model", "pebble_time_read"); + break; + + case ModelBobbySilver: + watchInfo.setProperty("model", "pebble_time_steel_silver"); + break; + + case ModelBobbyBlack: + watchInfo.setProperty("model", "pebble_time_steel_black"); + break; + + case ModelBobbyGold: + watchInfo.setProperty("model", "pebble_time_steel_gold"); + break; + + case ModelSpalding14Silver: + watchInfo.setProperty("model", "pebble_time_round_silver_14mm"); + break; + + case ModelSpalding14Black: + watchInfo.setProperty("model", "pebble_time_round_black_14mm"); + break; + + case ModelSpalding20Silver: + watchInfo.setProperty("model", "pebble_time_round_silver_20mm"); + break; + + case ModelSpalding20Black: + watchInfo.setProperty("model", "pebble_time_round_black_20mm"); + break; + + case ModelSpalding14RoseGold: + watchInfo.setProperty("model", "pebble_time_round_rose_gold_14mm"); + break; + + default: + watchInfo.setProperty("model", "pebble_black"); + break; + } + + watchInfo.setProperty("language", m_mgr->m_pebble->language()); + + QJSValue firmware = m_mgr->m_engine->newObject(); + QString version = m_mgr->m_pebble->softwareVersion().remove("v"); + QStringList versionParts = version.split("."); + + if (versionParts.count() >= 1) { + firmware.setProperty("major", versionParts[0].toInt()); + } + + if (versionParts.count() >= 2) { + firmware.setProperty("minor", versionParts[1].toInt()); + } + + if (versionParts.count() >= 3) { + if (versionParts[2].contains("-")) { + QStringList patchParts = version.split("-"); + firmware.setProperty("patch", patchParts[0].toInt()); + firmware.setProperty("suffix", patchParts[1]); + } else { + firmware.setProperty("patch", versionParts[2].toInt()); + firmware.setProperty("suffix", ""); + } + } + + watchInfo.setProperty("firmware", firmware); + return watchInfo; +} + +void JSKitPebble::openURL(const QUrl &url) +{ + emit m_mgr->openURL(m_appInfo.uuid().toString(), url.toString()); +} + +QJSValue JSKitPebble::createXMLHttpRequest() +{ + JSKitXMLHttpRequest *xhr = new JSKitXMLHttpRequest(m_mgr->engine()); + // Should be deleted by JS engine. + return m_mgr->engine()->newQObject(xhr); +} + +QJSValue JSKitPebble::buildAckEventObject(uint transaction, const QString &message) const +{ + QJSEngine *engine = m_mgr->engine(); + QJSValue eventObj = engine->newObject(); + QJSValue dataObj = engine->newObject(); + + dataObj.setProperty("transactionId", engine->toScriptValue(transaction)); + eventObj.setProperty("data", dataObj); + + if (!message.isEmpty()) { + QJSValue errorObj = engine->newObject(); + + errorObj.setProperty("message", engine->toScriptValue(message)); + eventObj.setProperty("error", errorObj); + } + + return eventObj; +} + +void JSKitPebble::invokeCallbacks(const QString &type, const QJSValueList &args) +{ + if (!m_listeners.contains(type)) return; + QList<QJSValue> &callbacks = m_listeners[type]; + + for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ++it) { + qCDebug(l) << "invoking callback" << type << it->toString(); + QJSValue result = it->call(args); + if (result.isError()) { + qCWarning(l) << "error while invoking callback" + << type << it->toString() << ":" + << JSKitManager::describeError(result); + } + } +} diff --git a/rockworkd/libpebble/jskit/jskitpebble.h b/rockworkd/libpebble/jskit/jskitpebble.h new file mode 100644 index 0000000..d9cd670 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitpebble.h @@ -0,0 +1,47 @@ +#ifndef JSKITPEBBLE_P_H +#define JSKITPEBBLE_P_H + +#include <QLoggingCategory> + +#include "jskitmanager.h" +#include "../appinfo.h" + +class JSKitPebble : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitPebble(const AppInfo &appInfo, JSKitManager *mgr, QObject *parent=0); + + Q_INVOKABLE void addEventListener(const QString &type, QJSValue function); + Q_INVOKABLE void removeEventListener(const QString &type, QJSValue function); + + Q_INVOKABLE void showSimpleNotificationOnPebble(const QString &title, const QString &body); + Q_INVOKABLE uint sendAppMessage(QJSValue message, QJSValue callbackForAck = QJSValue(), QJSValue callbackForNack = QJSValue()); + + Q_INVOKABLE void getTimelineToken(QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue()); + Q_INVOKABLE void timelineSubscribe(const QString &topic, QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue()); + Q_INVOKABLE void timelineUnsubscribe(const QString &topic, QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue()); + Q_INVOKABLE void timelineSubscriptions(QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue()); + + Q_INVOKABLE QString getAccountToken() const; + Q_INVOKABLE QString getWatchToken() const; + Q_INVOKABLE QJSValue getActiveWatchInfo() const; + + Q_INVOKABLE void openURL(const QUrl &url); + + Q_INVOKABLE QJSValue createXMLHttpRequest(); + + void invokeCallbacks(const QString &type, const QJSValueList &args = QJSValueList()); + +private: + QJSValue buildAckEventObject(uint transaction, const QString &message = QString()) const; + +private: + AppInfo m_appInfo; + JSKitManager *m_mgr; + QHash<QString, QList<QJSValue>> m_listeners; +}; + +#endif // JSKITPEBBLE_P_H diff --git a/rockworkd/libpebble/jskit/jskitperformance.cpp b/rockworkd/libpebble/jskit/jskitperformance.cpp new file mode 100644 index 0000000..23b0e08 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitperformance.cpp @@ -0,0 +1,13 @@ +#include "jskitperformance.h" + +JSKitPerformance::JSKitPerformance(QObject *parent) : + QObject(parent), + m_start(QTime::currentTime()) +{ +} + +int JSKitPerformance::now() +{ + QTime now = QTime::currentTime(); + return m_start.msecsTo(now); +} diff --git a/rockworkd/libpebble/jskit/jskitperformance.h b/rockworkd/libpebble/jskit/jskitperformance.h new file mode 100644 index 0000000..5f118be --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitperformance.h @@ -0,0 +1,20 @@ +#ifndef JSKITPERFORMANCE_H +#define JSKITPERFORMANCE_H + +#include <QObject> +#include <QTime> + +class JSKitPerformance : public QObject +{ + Q_OBJECT + +public: + explicit JSKitPerformance(QObject *parent=0); + + Q_INVOKABLE int now(); + +private: + QTime m_start; +}; + +#endif // JSKITPERFORMANCE_H diff --git a/rockworkd/libpebble/jskit/jskitsetup.js b/rockworkd/libpebble/jskit/jskitsetup.js new file mode 100644 index 0000000..340c4f1 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitsetup.js @@ -0,0 +1,196 @@ +//Borrowed from https://github.com/pebble/pypkjs/blob/master/pypkjs/javascript/runtime.py#L17 +_jskit.make_proxies = function(proxy, origin, names) { + names.forEach(function(name) { + proxy[name] = eval("(function " + name + "() { return origin[name].apply(origin, arguments); })"); + }); + + return proxy; +} + +_jskit.make_properties = function(proxy, origin, names) { + names.forEach(function(name) { + Object.defineProperty(proxy, name, { + configurable: false, + enumerable: true, + get: function() { + return origin[name]; + }, + set: function(value) { + origin[name] = value; + } + }); + }); + + return proxy; +} + +Pebble = new (function() { + _jskit.make_proxies(this, _jskit.pebble, + ['sendAppMessage', 'showSimpleNotificationOnPebble', 'getAccountToken', 'getWatchToken', + 'addEventListener', 'removeEventListener', 'openURL', 'getTimelineToken', 'timelineSubscribe', + 'timelineUnsubscribe', 'timelineSubscriptions', 'getActiveWatchInfo'] + ); +})(); + +performance = new (function() { + _jskit.make_proxies(this, _jskit.performance, ['now']); +})(); + +function XMLHttpRequest() { + var xhr = _jskit.pebble.createXMLHttpRequest(); + _jskit.make_proxies(this, xhr, + ['open', 'setRequestHeader', 'overrideMimeType', 'send', 'getResponseHeader', + 'getAllResponseHeaders', 'abort', 'addEventListener', 'removeEventListener']); + _jskit.make_properties(this, xhr, + ['readyState', 'response', 'responseText', 'responseType', 'status', + 'statusText', 'timeout', 'onreadystatechange', 'ontimeout', 'onload', + 'onloadstart', 'onloadend', 'onprogress', 'onerror', 'onabort']); + + this.UNSENT = 0; + this.OPENED = 1; + this.HEADERS_RECEIVED = 2; + this.LOADING = 3; + this.DONE = 4; +} + +function setInterval(func, time) { + return _jskit.timer.setInterval(func, time); +} + +function clearInterval(id) { + _jskit.timer.clearInterval(id); +} + +function setTimeout(func, time) { + return _jskit.timer.setTimeout(func, time); +} + +function clearTimeout(id) { + _jskit.timer.clearTimeout(id); +} + +navigator.geolocation = new (function() { + _jskit.make_proxies(this, _jskit.geolocation, + ['getCurrentPosition', 'watchPosition', 'clearWatch'] + ); +})(); + +console = new (function() { + _jskit.make_proxies(this, _jskit.console, + ['log', 'warn', 'error', 'info'] + ); +})(); + +/*localStorage = new (function() { + _jskit.make_proxies(this, _jskit.localstorage, + ['clear', 'getItem', 'setItem', 'removeItem', 'key'] + ); + + _jskit.make_properties(this, _jskit.localstorage, + ['length'] + ); +})();*/ + +//It appears that Proxy is not available since Qt is using Javascript v5 +/*(function() { + var proxy = _jskit.make_proxies({}, _jskit.localstorage, ['set', 'has', 'deleteProperty', 'keys', 'enumerate']); + var methods = _jskit.make_proxies({}, _jskit.localstorage, ['clear', 'getItem', 'setItem', 'removeItem', 'key']); + proxy.get = function get(p, name) { return methods[name] || _jskit.localstorage.get(p, name); } + this.localStorage = Proxy.create(proxy); +})();*/ + +//inspired by https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage +Object.defineProperty(window, "localStorage", new (function () { + var storage = {}; + Object.defineProperty(storage, "getItem", { + value: function (key) { + var value = null; + if (key !== undefined && key !== null && storage[key] !== undefined) { + value = storage[key]; + } + + return value; + }, + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "key", { + value: function (index) { + return Object.keys(storage)[index]; + }, + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "setItem", { + value: function (key, value) { + if (key !== undefined && key !== null) { + _jskit.localstorage.setItem(key, value); + storage[key] = (value && value.toString) ? value.toString() : value; + return true; + } + else { + return false; + } + }, + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "length", { + get: function () { + return Object.keys(storage).length; + }, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "removeItem", { + value: function (key) { + if (key && storage[key]) { + _jskit.localstorage.removeItem(key); + delete storage[key]; + + return true; + } + else { + return false; + } + }, + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "clear", { + value: function (key) { + for (var key in storage) { + storage.removeItem(key); + } + + return true; + }, + writable: false, + configurable: false, + enumerable: false + }); + + this.get = function () { + return storage; + }; + + this.configurable = false; + this.enumerable = true; +})()); + +(function() { + var keys = _jskit.localstorage.keys(); + for (var index in keys) { + var value = _jskit.localstorage.getItem(keys[index]); + localStorage.setItem(keys[index], value); + } +})(); diff --git a/rockworkd/libpebble/jskit/jskittimer.cpp b/rockworkd/libpebble/jskit/jskittimer.cpp new file mode 100644 index 0000000..6ab5b4a --- /dev/null +++ b/rockworkd/libpebble/jskit/jskittimer.cpp @@ -0,0 +1,77 @@ +#include <QTimerEvent> + +#include "jskittimer.h" + +JSKitTimer::JSKitTimer(QJSEngine *engine) : + QObject(engine), + l(metaObject()->className()), + m_engine(engine) +{ +} + +int JSKitTimer::setInterval(QJSValue expression, int delay) //TODO support optional parameters +{ + qCDebug(l) << "Setting interval for " << delay << "ms: " << expression.toString(); + + if (expression.isString() || expression.isCallable()) { + int timerId = startTimer(delay); + m_intervals.insert(timerId, expression); + + return timerId; + } + + return -1; +} + +void JSKitTimer::clearInterval(int timerId) +{ + qCDebug(l) << "Killing interval " << timerId ; + killTimer(timerId); + m_intervals.remove(timerId); +} + +int JSKitTimer::setTimeout(QJSValue expression, int delay) //TODO support optional parameters +{ + qCDebug(l) << "Setting timeout for " << delay << "ms: " << expression.toString(); + + if (expression.isString() || expression.isCallable()) { + int timerId = startTimer(delay); + m_timeouts.insert(timerId, expression); + + return timerId; + } + + return -1; +} + +void JSKitTimer::clearTimeout(int timerId) +{ + qCDebug(l) << "Killing timeout " << timerId ; + killTimer(timerId); + m_timeouts.remove(timerId); +} + +void JSKitTimer::timerEvent(QTimerEvent *event) +{ + int id = event->timerId(); + + QJSValue expression; // find in either intervals or timeouts + if (m_intervals.contains(id)) { + expression = m_intervals.value(id); + } else if (m_timeouts.contains(id)) { + expression = m_timeouts.value(id); + killTimer(id); // timeouts don't repeat + } else { + qCWarning(l) << "Unknown timer event"; + killTimer(id); // interval nor timeout exist. kill the timer + + return; + } + + if (expression.isCallable()) { // call it if it's a function + expression.call().toString(); + } + else { // otherwise evaluate it + m_engine->evaluate(expression.toString()); + } +} diff --git a/rockworkd/libpebble/jskit/jskittimer.h b/rockworkd/libpebble/jskit/jskittimer.h new file mode 100644 index 0000000..50b394d --- /dev/null +++ b/rockworkd/libpebble/jskit/jskittimer.h @@ -0,0 +1,31 @@ +#ifndef JSKITTIMER_P_H +#define JSKITTIMER_P_H + +#include <QLoggingCategory> +#include <QJSValue> +#include <QJSEngine> + +class JSKitTimer : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitTimer(QJSEngine *engine); + + Q_INVOKABLE int setInterval(QJSValue expression, int delay); + Q_INVOKABLE void clearInterval(int timerId); + + Q_INVOKABLE int setTimeout(QJSValue expression, int delay); + Q_INVOKABLE void clearTimeout(int timerId); + +protected: + void timerEvent(QTimerEvent *event); + +private: + QJSEngine *m_engine; + QHash<int, QJSValue> m_intervals; + QHash<int, QJSValue> m_timeouts; +}; + +#endif // JSKITTIMER_P_H diff --git a/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp b/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp new file mode 100644 index 0000000..5948683 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp @@ -0,0 +1,318 @@ +#include <QBuffer> +#include <QAuthenticator> +#include <QEventLoop> + +#include "jskitxmlhttprequest.h" +#include "jskitmanager.h" + +JSKitXMLHttpRequest::JSKitXMLHttpRequest(QJSEngine *engine) : + QObject(engine), + l(metaObject()->className()), + m_engine(engine), + m_net(new QNetworkAccessManager(this)), + m_timeout(0), + m_reply(0) +{ + connect(m_net, &QNetworkAccessManager::authenticationRequired, + this, &JSKitXMLHttpRequest::handleAuthenticationRequired); +} + +void JSKitXMLHttpRequest::open(const QString &method, const QString &url, bool async, const QString &username, const QString &password) +{ + if (m_reply) { + m_reply->deleteLater(); + m_reply = 0; + } + + m_username = username; + m_password = password; + m_request = QNetworkRequest(QUrl(url)); + m_verb = method; + m_async = async; + + qCDebug(l) << "opened to URL" << m_request.url().toString() << "Async:" << async; +} + +void JSKitXMLHttpRequest::setRequestHeader(const QString &header, const QString &value) +{ + qCDebug(l) << "setRequestHeader" << header << value; + m_request.setRawHeader(header.toLatin1(), value.toLatin1()); +} + +void JSKitXMLHttpRequest::send(const QJSValue &data) +{ + QByteArray byteData; + + if (data.isUndefined() || data.isNull()) { + // Do nothing, byteData is empty. + } else if (data.isString()) { + byteData = data.toString().toUtf8(); + } else if (data.isObject()) { + if (data.hasProperty("byteLength")) { + // Looks like an ArrayView or an ArrayBufferView! + QJSValue buffer = data.property("buffer"); + if (buffer.isUndefined()) { + // We must assume we've been passed an ArrayBuffer directly + buffer = data; + } + + QJSValue array = data.property("_bytes"); + int byteLength = data.property("byteLength").toInt(); + + if (array.isArray()) { + byteData.reserve(byteLength); + + for (int i = 0; i < byteLength; i++) { + byteData.append(array.property(i).toInt()); + } + + qCDebug(l) << "passed an ArrayBufferView of" << byteData.length() << "bytes"; + } else { + qCWarning(l) << "passed an unknown/invalid ArrayBuffer" << data.toString(); + } + } else { + qCWarning(l) << "passed an unknown object" << data.toString(); + } + + } + + QBuffer *buffer; + if (!byteData.isEmpty()) { + buffer = new QBuffer; + buffer->setData(byteData); + } else { + buffer = 0; + } + + qCDebug(l) << "sending" << m_verb << "to" << m_request.url() << "with" << QString::fromUtf8(byteData); + m_reply = m_net->sendCustomRequest(m_request, m_verb.toLatin1(), buffer); + + connect(m_reply, &QNetworkReply::finished, + this, &JSKitXMLHttpRequest::handleReplyFinished); + connect(m_reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), + this, &JSKitXMLHttpRequest::handleReplyError); + + if (buffer) { + // So that it gets deleted alongside the reply object. + buffer->setParent(m_reply); + } + + if (!m_async) { + QEventLoop loop; //Hacky way to get QNetworkReply be synchronous + + connect(m_reply, &QNetworkReply::finished, + &loop, &QEventLoop::quit); + connect(m_reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), + &loop, &QEventLoop::quit); + + loop.exec(); + } +} + +void JSKitXMLHttpRequest::abort() +{ + if (m_reply) { + m_reply->deleteLater(); + m_reply = 0; + } +} + +QJSValue JSKitXMLHttpRequest::onload() const +{ + return m_onload; +} + +void JSKitXMLHttpRequest::setOnload(const QJSValue &value) +{ + m_onload = value; +} + +QJSValue JSKitXMLHttpRequest::onreadystatechange() const +{ + return m_onreadystatechange; +} + +void JSKitXMLHttpRequest::setOnreadystatechange(const QJSValue &value) +{ + m_onreadystatechange = value; +} + +QJSValue JSKitXMLHttpRequest::ontimeout() const +{ + return m_ontimeout; +} + +void JSKitXMLHttpRequest::setOntimeout(const QJSValue &value) +{ + m_ontimeout = value; +} + +QJSValue JSKitXMLHttpRequest::onerror() const +{ + return m_onerror; +} + +void JSKitXMLHttpRequest::setOnerror(const QJSValue &value) +{ + m_onerror = value; +} + +uint JSKitXMLHttpRequest::readyState() const +{ + if (!m_reply) { + return UNSENT; + } else if (m_reply->isFinished()) { + return DONE; + } else { + return LOADING; + } +} + +uint JSKitXMLHttpRequest::timeout() const +{ + return m_timeout; +} + +void JSKitXMLHttpRequest::setTimeout(uint value) +{ + m_timeout = value; + // TODO Handle fetch in-progress. +} + +uint JSKitXMLHttpRequest::status() const +{ + if (!m_reply || !m_reply->isFinished()) { + return 0; + } else { + return m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt(); + } +} + +QString JSKitXMLHttpRequest::statusText() const +{ + if (!m_reply || !m_reply->isFinished()) { + return QString(); + } else { + return m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + } +} + +QString JSKitXMLHttpRequest::responseType() const +{ + return m_responseType; +} + +void JSKitXMLHttpRequest::setResponseType(const QString &type) +{ + qCDebug(l) << "response type set to" << type; + m_responseType = type; +} + +QJSValue JSKitXMLHttpRequest::response() const +{ + if (m_responseType.isEmpty() || m_responseType == "text") { + return m_engine->toScriptValue(QString::fromUtf8(m_response)); + } else if (m_responseType == "arraybuffer") { + QJSValue arrayBufferProto = m_engine->globalObject().property("ArrayBuffer").property("prototype"); + QJSValue arrayBuf = m_engine->newObject(); + + if (!arrayBufferProto.isUndefined()) { + arrayBuf.setPrototype(arrayBufferProto); + arrayBuf.setProperty("byteLength", m_engine->toScriptValue<uint>(m_response.size())); + + QJSValue array = m_engine->newArray(m_response.size()); + for (int i = 0; i < m_response.size(); i++) { + array.setProperty(i, m_engine->toScriptValue<int>(m_response[i])); + } + + arrayBuf.setProperty("_bytes", array); + qCDebug(l) << "returning ArrayBuffer of" << m_response.size() << "bytes"; + } else { + qCWarning(l) << "Cannot find proto of ArrayBuffer"; + } + + return arrayBuf; + } else { + qCWarning(l) << "unsupported responseType:" << m_responseType; + return m_engine->toScriptValue<void*>(0); + } +} + +QString JSKitXMLHttpRequest::responseText() const +{ + return QString::fromUtf8(m_response); +} + +void JSKitXMLHttpRequest::handleReplyFinished() +{ + if (!m_reply) { + qCDebug(l) << "reply finished too late"; + return; + } + + m_response = m_reply->readAll(); + qCDebug(l) << "reply finished, reply text:" << QString::fromUtf8(m_response) << "status:" << status(); + + emit readyStateChanged(); + emit statusChanged(); + emit statusTextChanged(); + emit responseChanged(); + emit responseTextChanged(); + + if (m_onload.isCallable()) { + qCDebug(l) << "going to call onload handler:" << m_onload.toString(); + + QJSValue result = m_onload.callWithInstance(m_engine->newQObject(this)); + if (result.isError()) { + qCWarning(l) << "JS error on onload handler:" << JSKitManager::describeError(result); + } + } else { + qCDebug(l) << "No onload set"; + } + + if (m_onreadystatechange.isCallable()) { + qCDebug(l) << "going to call onreadystatechange handler:" << m_onreadystatechange.toString(); + QJSValue result = m_onreadystatechange.callWithInstance(m_engine->newQObject(this)); + if (result.isError()) { + qCWarning(l) << "JS error on onreadystatechange handler:" << JSKitManager::describeError(result); + } + } +} + +void JSKitXMLHttpRequest::handleReplyError(QNetworkReply::NetworkError code) +{ + if (!m_reply) { + qCDebug(l) << "reply error too late"; + return; + } + + qCDebug(l) << "reply error" << code; + + emit readyStateChanged(); + emit statusChanged(); + emit statusTextChanged(); + + if (m_onerror.isCallable()) { + qCDebug(l) << "going to call onerror handler:" << m_onload.toString(); + QJSValue result = m_onerror.callWithInstance(m_engine->newQObject(this)); + if (result.isError()) { + qCWarning(l) << "JS error on onerror handler:" << JSKitManager::describeError(result); + } + } +} + +void JSKitXMLHttpRequest::handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth) +{ + if (m_reply == reply) { + qCDebug(l) << "authentication required"; + + if (!m_username.isEmpty() || !m_password.isEmpty()) { + qCDebug(l) << "using provided authorization:" << m_username; + + auth->setUser(m_username); + auth->setPassword(m_password); + } else { + qCDebug(l) << "no username or password provided"; + } + } +} diff --git a/rockworkd/libpebble/jskit/jskitxmlhttprequest.h b/rockworkd/libpebble/jskit/jskitxmlhttprequest.h new file mode 100644 index 0000000..70b8136 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitxmlhttprequest.h @@ -0,0 +1,96 @@ +#ifndef JSKITXMLHTTPREQUEST_P_H +#define JSKITXMLHTTPREQUEST_P_H + +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QJSEngine> +#include <QLoggingCategory> + +class JSKitXMLHttpRequest : public QObject +{ + Q_OBJECT + QLoggingCategory l; + + Q_PROPERTY(QJSValue onload READ onload WRITE setOnload) + Q_PROPERTY(QJSValue onreadystatechange READ onreadystatechange WRITE setOnreadystatechange) + Q_PROPERTY(QJSValue ontimeout READ ontimeout WRITE setOntimeout) + Q_PROPERTY(QJSValue onerror READ onerror WRITE setOnerror) + Q_PROPERTY(uint readyState READ readyState NOTIFY readyStateChanged) + Q_PROPERTY(uint timeout READ timeout WRITE setTimeout) + Q_PROPERTY(uint status READ status NOTIFY statusChanged) + Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(QString responseType READ responseType WRITE setResponseType) + Q_PROPERTY(QJSValue response READ response NOTIFY responseChanged) + Q_PROPERTY(QString responseText READ responseText NOTIFY responseTextChanged) + +public: + explicit JSKitXMLHttpRequest(QJSEngine *engine); + + enum ReadyStates { + UNSENT = 0, + OPENED = 1, + HEADERS_RECEIVED = 2, + LOADING = 3, + DONE = 4 + }; + Q_ENUMS(ReadyStates) + + Q_INVOKABLE void open(const QString &method, const QString &url, bool async = true, const QString &username = QString(), const QString &password = QString()); + Q_INVOKABLE void setRequestHeader(const QString &header, const QString &value); + Q_INVOKABLE void send(const QJSValue &data = QJSValue(QJSValue::NullValue)); + Q_INVOKABLE void abort(); + + QJSValue onload() const; + void setOnload(const QJSValue &value); + QJSValue onreadystatechange() const; + void setOnreadystatechange(const QJSValue &value); + QJSValue ontimeout() const; + void setOntimeout(const QJSValue &value); + QJSValue onerror() const; + void setOnerror(const QJSValue &value); + + uint readyState() const; + + uint timeout() const; + void setTimeout(uint value); + + uint status() const; + QString statusText() const; + + QString responseType() const; + void setResponseType(const QString& type); + + QJSValue response() const; + QString responseText() const; + +signals: + void readyStateChanged(); + void statusChanged(); + void statusTextChanged(); + void responseChanged(); + void responseTextChanged(); + +private slots: + void handleReplyFinished(); + void handleReplyError(QNetworkReply::NetworkError code); + void handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth); + +private: + QJSEngine *m_engine; + QNetworkAccessManager *m_net; + QString m_verb; + bool m_async = true; + uint m_timeout; + QString m_username; + QString m_password; + QNetworkRequest m_request; + QNetworkReply *m_reply; + QString m_responseType; + QByteArray m_response; + QJSValue m_onload; + QJSValue m_onreadystatechange; + QJSValue m_ontimeout; + QJSValue m_onerror; +}; + +#endif // JSKITXMLHTTPREQUEST_P_H diff --git a/rockworkd/libpebble/jskit/typedarray.js b/rockworkd/libpebble/jskit/typedarray.js new file mode 100644 index 0000000..d4e00c6 --- /dev/null +++ b/rockworkd/libpebble/jskit/typedarray.js @@ -0,0 +1,1037 @@ +/* + Copyright (c) 2010, Linden Research, Inc. + Copyright (c) 2014, Joshua Bell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + $/LicenseInfo$ + */ + +// Original can be found at: +// https://bitbucket.org/lindenlab/llsd +// Modifications by Joshua Bell inexorabletash@gmail.com +// https://github.com/inexorabletash/polyfill + +// ES3/ES5 implementation of the Krhonos Typed Array Specification +// Ref: http://www.khronos.org/registry/typedarray/specs/latest/ +// Date: 2011-02-01 +// +// Variations: +// * Allows typed_array.get/set() as alias for subscripts (typed_array[]) +// * Gradually migrating structure from Khronos spec to ES6 spec +(function(global) { + 'use strict'; + var undefined = (void 0); // Paranoia + + // Beyond this value, index getters/setters (i.e. array[0], array[1]) are so slow to + // create, and consume so much memory, that the browser appears frozen. + var MAX_ARRAY_LENGTH = 1e5; + + // Approximations of internal ECMAScript conversion functions + function Type(v) { + switch(typeof v) { + case 'undefined': return 'undefined'; + case 'boolean': return 'boolean'; + case 'number': return 'number'; + case 'string': return 'string'; + default: return v === null ? 'null' : 'object'; + } + } + + // Class returns internal [[Class]] property, used to avoid cross-frame instanceof issues: + function Class(v) { return Object.prototype.toString.call(v).replace(/^\[object *|\]$/g, ''); } + function IsCallable(o) { return typeof o === 'function'; } + function ToObject(v) { + if (v === null || v === undefined) throw TypeError(); + return Object(v); + } + function ToInt32(v) { return v >> 0; } + function ToUint32(v) { return v >> 0; } //ROCKWORK HACK ALERT: it appears that QT doesn't do the >>> properly, using >> here instead (should be close enough) + + // Snapshot intrinsics + var LN2 = Math.LN2, + abs = Math.abs, + floor = Math.floor, + log = Math.log, + max = Math.max, + min = Math.min, + pow = Math.pow, + round = Math.round; + + // emulate ES5 getter/setter API using legacy APIs + // http://blogs.msdn.com/b/ie/archive/2010/09/07/transitioning-existing-code-to-the-es5-getter-setter-apis.aspx + // (second clause tests for Object.defineProperty() in IE<9 that only supports extending DOM prototypes, but + // note that IE<9 does not support __defineGetter__ or __defineSetter__ so it just renders the method harmless) + + (function() { + var orig = Object.defineProperty; + var dom_only = !(function(){try{return Object.defineProperty({},'x',{});}catch(_){return false;}}()); + + if (!orig || dom_only) { + Object.defineProperty = function (o, prop, desc) { + // In IE8 try built-in implementation for defining properties on DOM prototypes. + if (orig) + try { return orig(o, prop, desc); } catch (_) {} + if (o !== Object(o)) + throw TypeError('Object.defineProperty called on non-object'); + if (Object.prototype.__defineGetter__ && ('get' in desc)) + Object.prototype.__defineGetter__.call(o, prop, desc.get); + if (Object.prototype.__defineSetter__ && ('set' in desc)) + Object.prototype.__defineSetter__.call(o, prop, desc.set); + if ('value' in desc) + o[prop] = desc.value; + return o; + }; + } + }()); + + // ES5: Make obj[index] an alias for obj._getter(index)/obj._setter(index, value) + // for index in 0 ... obj.length + function makeArrayAccessors(obj) { + if (obj.length > MAX_ARRAY_LENGTH) throw RangeError('Array too large for polyfill'); + + function makeArrayAccessor(index) { + Object.defineProperty(obj, index, { + 'get': function() { return obj._getter(index); }, + 'set': function(v) { obj._setter(index, v); }, + enumerable: true, + configurable: false + }); + } + + var i; + for (i = 0; i < obj.length; i += 1) { + makeArrayAccessor(i); + } + } + + // Internal conversion functions: + // pack<Type>() - take a number (interpreted as Type), output a byte array + // unpack<Type>() - take a byte array, output a Type-like number + + function as_signed(value, bits) { var s = 32 - bits; return (value << s) >> s; } + function as_unsigned(value, bits) { var s = 32 - bits; return (value << s) >>> s; } + + function packI8(n) { return [n & 0xff]; } + function unpackI8(bytes) { return as_signed(bytes[0], 8); } + + function packU8(n) { return [n & 0xff]; } + function unpackU8(bytes) { return as_unsigned(bytes[0], 8); } + + function packU8Clamped(n) { n = round(Number(n)); return [n < 0 ? 0 : n > 0xff ? 0xff : n & 0xff]; } + + function packI16(n) { return [n & 0xff, (n >> 8) & 0xff]; } + function unpackI16(bytes) { return as_signed(bytes[1] << 8 | bytes[0], 16); } + + function packU16(n) { return [n & 0xff, (n >> 8) & 0xff]; } + function unpackU16(bytes) { return as_unsigned(bytes[1] << 8 | bytes[0], 16); } + + function packI32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; } + function unpackI32(bytes) { return as_signed(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); } + + function packU32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; } + function unpackU32(bytes) { return as_unsigned(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); } + + function packIEEE754(v, ebits, fbits) { + + var bias = (1 << (ebits - 1)) - 1; + + function roundToEven(n) { + var w = floor(n), f = n - w; + if (f < 0.5) + return w; + if (f > 0.5) + return w + 1; + return w % 2 ? w + 1 : w; + } + + // Compute sign, exponent, fraction + var s, e, f; + if (v !== v) { + // NaN + // http://dev.w3.org/2006/webapi/WebIDL/#es-type-mapping + e = (1 << ebits) - 1; f = pow(2, fbits - 1); s = 0; + } else if (v === Infinity || v === -Infinity) { + e = (1 << ebits) - 1; f = 0; s = (v < 0) ? 1 : 0; + } else if (v === 0) { + e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0; + } else { + s = v < 0; + v = abs(v); + + if (v >= pow(2, 1 - bias)) { + // Normalized + e = min(floor(log(v) / LN2), 1023); + var significand = v / pow(2, e); + if (significand < 1) { + e -= 1; + significand *= 2; + } + if (significand >= 2) { + e += 1; + significand /= 2; + } + var d = pow(2, fbits); + f = roundToEven(significand * d) - d; + e += bias; + if (f / d >= 1) { + e += 1; + f = 0; + } + if (e > 2 * bias) { + // Overflow + e = (1 << ebits) - 1; + f = 0; + } + } else { + // Denormalized + e = 0; + f = roundToEven(v / pow(2, 1 - bias - fbits)); + } + } + + // Pack sign, exponent, fraction + var bits = [], i; + for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = floor(f / 2); } + for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = floor(e / 2); } + bits.push(s ? 1 : 0); + bits.reverse(); + var str = bits.join(''); + + // Bits to bytes + var bytes = []; + while (str.length) { + bytes.unshift(parseInt(str.substring(0, 8), 2)); + str = str.substring(8); + } + return bytes; + } + + function unpackIEEE754(bytes, ebits, fbits) { + // Bytes to bits + var bits = [], i, j, b, str, + bias, s, e, f; + + for (i = 0; i < bytes.length; ++i) { + b = bytes[i]; + for (j = 8; j; j -= 1) { + bits.push(b % 2 ? 1 : 0); b = b >> 1; + } + } + bits.reverse(); + str = bits.join(''); + + // Unpack sign, exponent, fraction + bias = (1 << (ebits - 1)) - 1; + s = parseInt(str.substring(0, 1), 2) ? -1 : 1; + e = parseInt(str.substring(1, 1 + ebits), 2); + f = parseInt(str.substring(1 + ebits), 2); + + // Produce number + if (e === (1 << ebits) - 1) { + return f !== 0 ? NaN : s * Infinity; + } else if (e > 0) { + // Normalized + return s * pow(2, e - bias) * (1 + f / pow(2, fbits)); + } else if (f !== 0) { + // Denormalized + return s * pow(2, -(bias - 1)) * (f / pow(2, fbits)); + } else { + return s < 0 ? -0 : 0; + } + } + + function unpackF64(b) { return unpackIEEE754(b, 11, 52); } + function packF64(v) { return packIEEE754(v, 11, 52); } + function unpackF32(b) { return unpackIEEE754(b, 8, 23); } + function packF32(v) { return packIEEE754(v, 8, 23); } + + // + // 3 The ArrayBuffer Type + // + + (function() { + + function ArrayBuffer(length) { + length = ToInt32(length); + if (length < 0) throw RangeError('ArrayBuffer size is not a small enough positive integer.'); + Object.defineProperty(this, 'byteLength', {value: length}); + Object.defineProperty(this, '_bytes', {value: Array(length)}); + + for (var i = 0; i < length; i += 1) + this._bytes[i] = 0; + } + + global.ArrayBuffer = global.ArrayBuffer || ArrayBuffer; + + // + // 5 The Typed Array View Types + // + + function $TypedArray$() { + + // %TypedArray% ( length ) + if (!arguments.length || typeof arguments[0] !== 'object') { + return (function(length) { + length = ToInt32(length); + if (length < 0) throw RangeError('length is not a small enough positive integer.'); + Object.defineProperty(this, 'length', {value: length}); + Object.defineProperty(this, 'byteLength', {value: length * this.BYTES_PER_ELEMENT}); + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(this.byteLength)}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + + }).apply(this, arguments); + } + + // %TypedArray% ( typedArray ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + arguments[0] instanceof $TypedArray$) { + return (function(typedArray){ + if (this.constructor !== typedArray.constructor) throw TypeError(); + + var byteLength = typedArray.length * this.BYTES_PER_ELEMENT; + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + Object.defineProperty(this, 'length', {value: typedArray.length}); + + for (var i = 0; i < this.length; i += 1) + this._setter(i, typedArray._getter(i)); + + }).apply(this, arguments); + } + + // %TypedArray% ( array ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + !(arguments[0] instanceof $TypedArray$) && + !(arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) { + return (function(array) { + + var byteLength = array.length * this.BYTES_PER_ELEMENT; + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + Object.defineProperty(this, 'length', {value: array.length}); + + for (var i = 0; i < this.length; i += 1) { + var s = array[i]; + this._setter(i, Number(s)); + } + }).apply(this, arguments); + } + + // %TypedArray% ( buffer, byteOffset=0, length=undefined ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + (arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) { + return (function(buffer, byteOffset, length) { + + byteOffset = ToUint32(byteOffset); + if (byteOffset > buffer.byteLength) + throw RangeError('byteOffset out of range'); + + // The given byteOffset must be a multiple of the element + // size of the specific type, otherwise an exception is raised. + if (byteOffset % this.BYTES_PER_ELEMENT) + throw RangeError('buffer length minus the byteOffset is not a multiple of the element size.'); + + if (length === undefined) { + var byteLength = buffer.byteLength - byteOffset; + if (byteLength % this.BYTES_PER_ELEMENT) + throw RangeError('length of buffer minus byteOffset not a multiple of the element size'); + length = byteLength / this.BYTES_PER_ELEMENT; + + } else { + length = ToUint32(length); + byteLength = length * this.BYTES_PER_ELEMENT; + } + + if ((byteOffset + byteLength) > buffer.byteLength) + throw RangeError('byteOffset and length reference an area beyond the end of the buffer'); + + Object.defineProperty(this, 'buffer', {value: buffer}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: byteOffset}); + Object.defineProperty(this, 'length', {value: length}); + + }).apply(this, arguments); + } + + // %TypedArray% ( all other argument combinations ) + throw TypeError(); + } + + // Properties of the %TypedArray Instrinsic Object + + // %TypedArray%.from ( source , mapfn=undefined, thisArg=undefined ) + Object.defineProperty($TypedArray$, 'from', {value: function(iterable) { + return new this(iterable); + }}); + + // %TypedArray%.of ( ...items ) + Object.defineProperty($TypedArray$, 'of', {value: function(/*...items*/) { + return new this(arguments); + }}); + + // %TypedArray%.prototype + var $TypedArrayPrototype$ = {}; + $TypedArray$.prototype = $TypedArrayPrototype$; + + // WebIDL: getter type (unsigned long index); + Object.defineProperty($TypedArray$.prototype, '_getter', {value: function(index) { + if (arguments.length < 1) throw SyntaxError('Not enough arguments'); + + index = ToUint32(index); + if (index >= this.length) + return undefined; + + var bytes = [], i, o; + for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT; + i < this.BYTES_PER_ELEMENT; + i += 1, o += 1) { + bytes.push(this.buffer._bytes[o]); + } + return this._unpack(bytes); + }}); + + // NONSTANDARD: convenience alias for getter: type get(unsigned long index); + Object.defineProperty($TypedArray$.prototype, 'get', {value: $TypedArray$.prototype._getter}); + + // WebIDL: setter void (unsigned long index, type value); + Object.defineProperty($TypedArray$.prototype, '_setter', {value: function(index, value) { + if (arguments.length < 2) throw SyntaxError('Not enough arguments'); + + index = ToUint32(index); + if (index >= this.length) + return; + + var bytes = this._pack(value), i, o; + for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT; + i < this.BYTES_PER_ELEMENT; + i += 1, o += 1) { + this.buffer._bytes[o] = bytes[i]; + } + }}); + + // get %TypedArray%.prototype.buffer + // get %TypedArray%.prototype.byteLength + // get %TypedArray%.prototype.byteOffset + // -- applied directly to the object in the constructor + + // %TypedArray%.prototype.constructor + Object.defineProperty($TypedArray$.prototype, 'constructor', {value: $TypedArray$}); + + // %TypedArray%.prototype.copyWithin (target, start, end = this.length ) + Object.defineProperty($TypedArray$.prototype, 'copyWithin', {value: function(target, start) { + var end = arguments[2]; + + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + len = max(len, 0); + var relativeTarget = ToInt32(target); + var to; + if (relativeTarget < 0) + to = max(len + relativeTarget, 0); + else + to = min(relativeTarget, len); + var relativeStart = ToInt32(start); + var from; + if (relativeStart < 0) + from = max(len + relativeStart, 0); + else + from = min(relativeStart, len); + var relativeEnd; + if (end === undefined) + relativeEnd = len; + else + relativeEnd = ToInt32(end); + var final; + if (relativeEnd < 0) + final = max(len + relativeEnd, 0); + else + final = min(relativeEnd, len); + var count = min(final - from, len - to); + var direction; + if (from < to && to < from + count) { + direction = -1; + from = from + count - 1; + to = to + count - 1; + } else { + direction = 1; + } + while (count > 0) { + o._setter(to, o._getter(from)); + from = from + direction; + to = to + direction; + count = count - 1; + } + return o; + }}); + + // %TypedArray%.prototype.entries ( ) + // -- defined in es6.js to shim browsers w/ native TypedArrays + + // %TypedArray%.prototype.every ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'every', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisArg = arguments[1]; + for (var i = 0; i < len; i++) { + if (!callbackfn.call(thisArg, t._getter(i), i, t)) + return false; + } + return true; + }}); + + // %TypedArray%.prototype.fill (value, start = 0, end = this.length ) + Object.defineProperty($TypedArray$.prototype, 'fill', {value: function(value) { + var start = arguments[1], + end = arguments[2]; + + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + len = max(len, 0); + var relativeStart = ToInt32(start); + var k; + if (relativeStart < 0) + k = max((len + relativeStart), 0); + else + k = min(relativeStart, len); + var relativeEnd; + if (end === undefined) + relativeEnd = len; + else + relativeEnd = ToInt32(end); + var final; + if (relativeEnd < 0) + final = max((len + relativeEnd), 0); + else + final = min(relativeEnd, len); + while (k < final) { + o._setter(k, value); + k += 1; + } + return o; + }}); + + // %TypedArray%.prototype.filter ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'filter', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var res = []; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + var val = t._getter(i); // in case fun mutates this + if (callbackfn.call(thisp, val, i, t)) + res.push(val); + } + return new this.constructor(res); + }}); + + // %TypedArray%.prototype.find (predicate, thisArg = undefined) + Object.defineProperty($TypedArray$.prototype, 'find', {value: function(predicate) { + var o = ToObject(this); + var lenValue = o.length; + var len = ToUint32(lenValue); + if (!IsCallable(predicate)) throw TypeError(); + var t = arguments.length > 1 ? arguments[1] : undefined; + var k = 0; + while (k < len) { + var kValue = o._getter(k); + var testResult = predicate.call(t, kValue, k, o); + if (Boolean(testResult)) + return kValue; + ++k; + } + return undefined; + }}); + + // %TypedArray%.prototype.findIndex ( predicate, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'findIndex', {value: function(predicate) { + var o = ToObject(this); + var lenValue = o.length; + var len = ToUint32(lenValue); + if (!IsCallable(predicate)) throw TypeError(); + var t = arguments.length > 1 ? arguments[1] : undefined; + var k = 0; + while (k < len) { + var kValue = o._getter(k); + var testResult = predicate.call(t, kValue, k, o); + if (Boolean(testResult)) + return k; + ++k; + } + return -1; + }}); + + // %TypedArray%.prototype.forEach ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'forEach', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + callbackfn.call(thisp, t._getter(i), i, t); + }}); + + // %TypedArray%.prototype.indexOf (searchElement, fromIndex = 0 ) + Object.defineProperty($TypedArray$.prototype, 'indexOf', {value: function(searchElement) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (len === 0) return -1; + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { + n = 0; + } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * floor(abs(n)); + } + } + if (n >= len) return -1; + var k = n >= 0 ? n : max(len - abs(n), 0); + for (; k < len; k++) { + if (t._getter(k) === searchElement) { + return k; + } + } + return -1; + }}); + + // %TypedArray%.prototype.join ( separator ) + Object.defineProperty($TypedArray$.prototype, 'join', {value: function(separator) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var tmp = Array(len); + for (var i = 0; i < len; ++i) + tmp[i] = t._getter(i); + return tmp.join(separator === undefined ? ',' : separator); // Hack for IE7 + }}); + + // %TypedArray%.prototype.keys ( ) + // -- defined in es6.js to shim browsers w/ native TypedArrays + + // %TypedArray%.prototype.lastIndexOf ( searchElement, fromIndex = this.length-1 ) + Object.defineProperty($TypedArray$.prototype, 'lastIndexOf', {value: function(searchElement) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (len === 0) return -1; + var n = len; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n !== n) { + n = 0; + } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * floor(abs(n)); + } + } + var k = n >= 0 ? min(n, len - 1) : len - abs(n); + for (; k >= 0; k--) { + if (t._getter(k) === searchElement) + return k; + } + return -1; + }}); + + // get %TypedArray%.prototype.length + // -- applied directly to the object in the constructor + + // %TypedArray%.prototype.map ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'map', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var res = []; res.length = len; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + res[i] = callbackfn.call(thisp, t._getter(i), i, t); + return new this.constructor(res); + }}); + + // %TypedArray%.prototype.reduce ( callbackfn [, initialValue] ) + Object.defineProperty($TypedArray$.prototype, 'reduce', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + // no value to return if no initial value and an empty array + if (len === 0 && arguments.length === 1) throw TypeError(); + var k = 0; + var accumulator; + if (arguments.length >= 2) { + accumulator = arguments[1]; + } else { + accumulator = t._getter(k++); + } + while (k < len) { + accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t); + k++; + } + return accumulator; + }}); + + // %TypedArray%.prototype.reduceRight ( callbackfn [, initialValue] ) + Object.defineProperty($TypedArray$.prototype, 'reduceRight', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + // no value to return if no initial value, empty array + if (len === 0 && arguments.length === 1) throw TypeError(); + var k = len - 1; + var accumulator; + if (arguments.length >= 2) { + accumulator = arguments[1]; + } else { + accumulator = t._getter(k--); + } + while (k >= 0) { + accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t); + k--; + } + return accumulator; + }}); + + // %TypedArray%.prototype.reverse ( ) + Object.defineProperty($TypedArray$.prototype, 'reverse', {value: function() { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var half = floor(len / 2); + for (var i = 0, j = len - 1; i < half; ++i, --j) { + var tmp = t._getter(i); + t._setter(i, t._getter(j)); + t._setter(j, tmp); + } + return t; + }}); + + // %TypedArray%.prototype.set(array, offset = 0 ) + // %TypedArray%.prototype.set(typedArray, offset = 0 ) + // WebIDL: void set(TypedArray array, optional unsigned long offset); + // WebIDL: void set(sequence<type> array, optional unsigned long offset); + Object.defineProperty($TypedArray$.prototype, 'set', {value: function(index, value) { + if (arguments.length < 1) throw SyntaxError('Not enough arguments'); + var array, sequence, offset, len, + i, s, d, + byteOffset, byteLength, tmp; + + if (typeof arguments[0] === 'object' && arguments[0].constructor === this.constructor) { + // void set(TypedArray array, optional unsigned long offset); + array = arguments[0]; + offset = ToUint32(arguments[1]); + + if (offset + array.length > this.length) { + throw RangeError('Offset plus length of array is out of range'); + } + + byteOffset = this.byteOffset + offset * this.BYTES_PER_ELEMENT; + byteLength = array.length * this.BYTES_PER_ELEMENT; + + if (array.buffer === this.buffer) { + tmp = []; + for (i = 0, s = array.byteOffset; i < byteLength; i += 1, s += 1) { + tmp[i] = array.buffer._bytes[s]; + } + for (i = 0, d = byteOffset; i < byteLength; i += 1, d += 1) { + this.buffer._bytes[d] = tmp[i]; + } + } else { + for (i = 0, s = array.byteOffset, d = byteOffset; + i < byteLength; i += 1, s += 1, d += 1) { + this.buffer._bytes[d] = array.buffer._bytes[s]; + } + } + } else if (typeof arguments[0] === 'object' && typeof arguments[0].length !== 'undefined') { + // void set(sequence<type> array, optional unsigned long offset); + sequence = arguments[0]; + len = ToUint32(sequence.length); + offset = ToUint32(arguments[1]); + + if (offset + len > this.length) { + throw RangeError('Offset plus length of array is out of range'); + } + + for (i = 0; i < len; i += 1) { + s = sequence[i]; + this._setter(offset + i, Number(s)); + } + } else { + throw TypeError('Unexpected argument type(s)'); + } + }}); + + // %TypedArray%.prototype.slice ( start, end ) + Object.defineProperty($TypedArray$.prototype, 'slice', {value: function(start, end) { + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + var relativeStart = ToInt32(start); + var k = (relativeStart < 0) ? max(len + relativeStart, 0) : min(relativeStart, len); + var relativeEnd = (end === undefined) ? len : ToInt32(end); + var final = (relativeEnd < 0) ? max(len + relativeEnd, 0) : min(relativeEnd, len); + var count = final - k; + var c = o.constructor; + var a = new c(count); + var n = 0; + while (k < final) { + var kValue = o._getter(k); + a._setter(n, kValue); + ++k; + ++n; + } + return a; + }}); + + // %TypedArray%.prototype.some ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'some', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (callbackfn.call(thisp, t._getter(i), i, t)) { + return true; + } + } + return false; + }}); + + // %TypedArray%.prototype.sort ( comparefn ) + Object.defineProperty($TypedArray$.prototype, 'sort', {value: function(comparefn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var tmp = Array(len); + for (var i = 0; i < len; ++i) + tmp[i] = t._getter(i); + if (comparefn) tmp.sort(comparefn); else tmp.sort(); // Hack for IE8/9 + for (i = 0; i < len; ++i) + t._setter(i, tmp[i]); + return t; + }}); + + // %TypedArray%.prototype.subarray(begin = 0, end = this.length ) + // WebIDL: TypedArray subarray(long begin, optional long end); + Object.defineProperty($TypedArray$.prototype, 'subarray', {value: function(start, end) { + function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } + + start = ToInt32(start); + end = ToInt32(end); + + if (arguments.length < 1) { start = 0; } + if (arguments.length < 2) { end = this.length; } + + if (start < 0) { start = this.length + start; } + if (end < 0) { end = this.length + end; } + + start = clamp(start, 0, this.length); + end = clamp(end, 0, this.length); + + var len = end - start; + if (len < 0) { + len = 0; + } + + return new this.constructor( + this.buffer, this.byteOffset + start * this.BYTES_PER_ELEMENT, len); + }}); + + // %TypedArray%.prototype.toLocaleString ( ) + // %TypedArray%.prototype.toString ( ) + // %TypedArray%.prototype.values ( ) + // %TypedArray%.prototype [ @@iterator ] ( ) + // get %TypedArray%.prototype [ @@toStringTag ] + // -- defined in es6.js to shim browsers w/ native TypedArrays + + function makeTypedArray(elementSize, pack, unpack) { + // Each TypedArray type requires a distinct constructor instance with + // identical logic, which this produces. + var TypedArray = function() { + Object.defineProperty(this, 'constructor', {value: TypedArray}); + $TypedArray$.apply(this, arguments); + makeArrayAccessors(this); + }; + if ('__proto__' in TypedArray) { + TypedArray.__proto__ = $TypedArray$; + } else { + TypedArray.from = $TypedArray$.from; + TypedArray.of = $TypedArray$.of; + } + + TypedArray.BYTES_PER_ELEMENT = elementSize; + + var TypedArrayPrototype = function() {}; + TypedArrayPrototype.prototype = $TypedArrayPrototype$; + + TypedArray.prototype = new TypedArrayPrototype(); + + Object.defineProperty(TypedArray.prototype, 'BYTES_PER_ELEMENT', {value: elementSize}); + Object.defineProperty(TypedArray.prototype, '_pack', {value: pack}); + Object.defineProperty(TypedArray.prototype, '_unpack', {value: unpack}); + + return TypedArray; + } + + var Int8Array = makeTypedArray(1, packI8, unpackI8); + var Uint8Array = makeTypedArray(1, packU8, unpackU8); + var Uint8ClampedArray = makeTypedArray(1, packU8Clamped, unpackU8); + var Int16Array = makeTypedArray(2, packI16, unpackI16); + var Uint16Array = makeTypedArray(2, packU16, unpackU16); + var Int32Array = makeTypedArray(4, packI32, unpackI32); + var Uint32Array = makeTypedArray(4, packU32, unpackU32); + var Float32Array = makeTypedArray(4, packF32, unpackF32); + var Float64Array = makeTypedArray(8, packF64, unpackF64); + + global.Int8Array = global.Int8Array || Int8Array; + global.Uint8Array = global.Uint8Array || Uint8Array; + global.Uint8ClampedArray = global.Uint8ClampedArray || Uint8ClampedArray; + global.Int16Array = global.Int16Array || Int16Array; + global.Uint16Array = global.Uint16Array || Uint16Array; + global.Int32Array = global.Int32Array || Int32Array; + global.Uint32Array = global.Uint32Array || Uint32Array; + global.Float32Array = global.Float32Array || Float32Array; + global.Float64Array = global.Float64Array || Float64Array; + }()); + + // + // 6 The DataView View Type + // + + (function() { + function r(array, index) { + return IsCallable(array.get) ? array.get(index) : array[index]; + } + + var IS_BIG_ENDIAN = (function() { + var u16array = new Uint16Array([0x1234]), + u8array = new Uint8Array(u16array.buffer); + return r(u8array, 0) === 0x12; + }()); + + // DataView(buffer, byteOffset=0, byteLength=undefined) + // WebIDL: Constructor(ArrayBuffer buffer, + // optional unsigned long byteOffset, + // optional unsigned long byteLength) + function DataView(buffer, byteOffset, byteLength) { + if (!(buffer instanceof ArrayBuffer || Class(buffer) === 'ArrayBuffer')) throw TypeError(); + + byteOffset = ToUint32(byteOffset); + if (byteOffset > buffer.byteLength) + throw RangeError('byteOffset out of range'); + + if (byteLength === undefined) + byteLength = buffer.byteLength - byteOffset; + else + byteLength = ToUint32(byteLength); + + if ((byteOffset + byteLength) > buffer.byteLength) + throw RangeError('byteOffset and length reference an area beyond the end of the buffer'); + + Object.defineProperty(this, 'buffer', {value: buffer}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: byteOffset}); + }; + + // get DataView.prototype.buffer + // get DataView.prototype.byteLength + // get DataView.prototype.byteOffset + // -- applied directly to instances by the constructor + + function makeGetter(arrayType) { + return function GetViewValue(byteOffset, littleEndian) { + byteOffset = ToUint32(byteOffset); + + if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength) + throw RangeError('Array index out of range'); + + byteOffset += this.byteOffset; + + var uint8Array = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT), + bytes = []; + for (var i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1) + bytes.push(r(uint8Array, i)); + + if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN)) + bytes.reverse(); + + return r(new arrayType(new Uint8Array(bytes).buffer), 0); + }; + } + + Object.defineProperty(DataView.prototype, 'getUint8', {value: makeGetter(Uint8Array)}); + Object.defineProperty(DataView.prototype, 'getInt8', {value: makeGetter(Int8Array)}); + Object.defineProperty(DataView.prototype, 'getUint16', {value: makeGetter(Uint16Array)}); + Object.defineProperty(DataView.prototype, 'getInt16', {value: makeGetter(Int16Array)}); + Object.defineProperty(DataView.prototype, 'getUint32', {value: makeGetter(Uint32Array)}); + Object.defineProperty(DataView.prototype, 'getInt32', {value: makeGetter(Int32Array)}); + Object.defineProperty(DataView.prototype, 'getFloat32', {value: makeGetter(Float32Array)}); + Object.defineProperty(DataView.prototype, 'getFloat64', {value: makeGetter(Float64Array)}); + + function makeSetter(arrayType) { + return function SetViewValue(byteOffset, value, littleEndian) { + byteOffset = ToUint32(byteOffset); + if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength) + throw RangeError('Array index out of range'); + + // Get bytes + var typeArray = new arrayType([value]), + byteArray = new Uint8Array(typeArray.buffer), + bytes = [], i, byteView; + + for (i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1) + bytes.push(r(byteArray, i)); + + // Flip if necessary + if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN)) + bytes.reverse(); + + // Write them + byteView = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT); + byteView.set(bytes); + }; + } + + Object.defineProperty(DataView.prototype, 'setUint8', {value: makeSetter(Uint8Array)}); + Object.defineProperty(DataView.prototype, 'setInt8', {value: makeSetter(Int8Array)}); + Object.defineProperty(DataView.prototype, 'setUint16', {value: makeSetter(Uint16Array)}); + Object.defineProperty(DataView.prototype, 'setInt16', {value: makeSetter(Int16Array)}); + Object.defineProperty(DataView.prototype, 'setUint32', {value: makeSetter(Uint32Array)}); + Object.defineProperty(DataView.prototype, 'setInt32', {value: makeSetter(Int32Array)}); + Object.defineProperty(DataView.prototype, 'setFloat32', {value: makeSetter(Float32Array)}); + Object.defineProperty(DataView.prototype, 'setFloat64', {value: makeSetter(Float64Array)}); + + global.DataView = global.DataView || DataView; + + }()); + +}(this)); |
