Merge pull request #407 from realm/sk-chrome-migration
All tests now pass in Chrome debug mode (including migration)
This commit is contained in:
commit
741db2037f
|
@ -28,11 +28,33 @@ import * as rpc from './rpc';
|
|||
import * as util from './util';
|
||||
|
||||
const {debugHosts, debugPort} = NativeModules.Realm;
|
||||
const listenersKey = Symbol();
|
||||
|
||||
rpc.registerTypeConverter(objectTypes.LIST, createList);
|
||||
rpc.registerTypeConverter(objectTypes.RESULTS, createResults);
|
||||
rpc.registerTypeConverter(objectTypes.OBJECT, createObject);
|
||||
rpc.registerTypeConverter(objectTypes.REALM, createRealm);
|
||||
|
||||
function createRealm(_, info) {
|
||||
let realm = Object.create(Realm.prototype);
|
||||
|
||||
setupRealm(realm, info.id);
|
||||
return realm;
|
||||
}
|
||||
|
||||
function setupRealm(realm, realmId) {
|
||||
realm[keys.id] = realmId;
|
||||
realm[keys.realm] = realmId;
|
||||
realm[keys.type] = objectTypes.REALM;
|
||||
|
||||
[
|
||||
'path',
|
||||
'readOnly',
|
||||
'schema',
|
||||
'schemaVersion',
|
||||
].forEach((name) => {
|
||||
Object.defineProperty(realm, name, {get: util.getterForProperty(name)});
|
||||
});
|
||||
}
|
||||
|
||||
export default class Realm {
|
||||
constructor(config) {
|
||||
|
@ -63,20 +85,7 @@ export default class Realm {
|
|||
let realmId = rpc.createRealm(Array.from(arguments));
|
||||
|
||||
registerConstructors(realmId, constructors);
|
||||
|
||||
this[keys.id] = realmId;
|
||||
this[keys.realm] = realmId;
|
||||
this[keys.type] = objectTypes.REALM;
|
||||
this[listenersKey] = new Set();
|
||||
|
||||
[
|
||||
'path',
|
||||
'readOnly',
|
||||
'schema',
|
||||
'schemaVersion',
|
||||
].forEach((name) => {
|
||||
Object.defineProperty(this, name, {get: util.getterForProperty(name)});
|
||||
});
|
||||
setupRealm(this, realmId);
|
||||
}
|
||||
|
||||
create(type, ...args) {
|
||||
|
@ -96,62 +105,13 @@ export default class Realm {
|
|||
let method = util.createMethod(objectTypes.REALM, 'objects');
|
||||
return method.apply(this, [type, ...args]);
|
||||
}
|
||||
|
||||
addListener(name, callback) {
|
||||
if (typeof callback != 'function') {
|
||||
throw new Error('Realm.addListener must be passed a function!');
|
||||
}
|
||||
if (name != 'change') {
|
||||
throw new Error("Only 'change' notification is supported.");
|
||||
}
|
||||
this[listenersKey].add(callback);
|
||||
}
|
||||
|
||||
removeListener(name, callback) {
|
||||
if (typeof callback != 'function') {
|
||||
throw new Error('Realm.removeListener must be passed a function!');
|
||||
}
|
||||
if (name != 'change') {
|
||||
throw new Error("Only 'change' notification is supported.");
|
||||
}
|
||||
this[listenersKey].delete(callback);
|
||||
}
|
||||
|
||||
removeAllListeners(name) {
|
||||
if (name != undefined && name != 'change') {
|
||||
throw new Error("Only 'change' notification is supported.");
|
||||
}
|
||||
this[listenersKey].clear();
|
||||
}
|
||||
|
||||
write(callback) {
|
||||
let realmId = this[keys.realm];
|
||||
|
||||
if (!realmId) {
|
||||
throw new TypeError('write method was not called on a Realm object!');
|
||||
}
|
||||
if (typeof callback != 'function') {
|
||||
throw new TypeError('Realm.write() must be passed a function!');
|
||||
}
|
||||
|
||||
rpc.beginTransaction(realmId);
|
||||
|
||||
try {
|
||||
callback();
|
||||
} catch (e) {
|
||||
rpc.cancelTransaction(realmId);
|
||||
collections.fireMutationListeners(realmId);
|
||||
throw e;
|
||||
}
|
||||
|
||||
rpc.commitTransaction(realmId);
|
||||
|
||||
this[listenersKey].forEach((cb) => cb(this, 'change'));
|
||||
}
|
||||
}
|
||||
|
||||
// Non-mutating methods:
|
||||
util.createMethods(Realm.prototype, objectTypes.REALM, [
|
||||
'addListener',
|
||||
'removeListener',
|
||||
'removeAllListeners',
|
||||
'close',
|
||||
]);
|
||||
|
||||
|
@ -159,6 +119,7 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [
|
|||
util.createMethods(Realm.prototype, objectTypes.REALM, [
|
||||
'delete',
|
||||
'deleteAll',
|
||||
'write',
|
||||
], true);
|
||||
|
||||
Object.defineProperties(Realm, {
|
||||
|
|
|
@ -22,6 +22,7 @@ import * as base64 from './base64';
|
|||
import { keys, objectTypes } from './constants';
|
||||
|
||||
const {id: idKey, realm: realmKey} = keys;
|
||||
const registeredCallbacks = [];
|
||||
const typeConverters = {};
|
||||
|
||||
let XMLHttpRequest = global.originalXMLHttpRequest || global.XMLHttpRequest;
|
||||
|
@ -78,20 +79,16 @@ export function setProperty(realmId, id, name, value) {
|
|||
sendRequest('set_property', {realmId, id, name, value});
|
||||
}
|
||||
|
||||
export function beginTransaction(realmId) {
|
||||
sendRequest('begin_transaction', {realmId});
|
||||
}
|
||||
|
||||
export function cancelTransaction(realmId) {
|
||||
sendRequest('cancel_transaction', {realmId});
|
||||
}
|
||||
|
||||
export function commitTransaction(realmId) {
|
||||
sendRequest('commit_transaction', {realmId});
|
||||
}
|
||||
|
||||
export function clearTestState() {
|
||||
sendRequest('clear_test_state');
|
||||
|
||||
// Clear all registered callbacks.
|
||||
registeredCallbacks.length = 0;
|
||||
}
|
||||
|
||||
function registerCallback(callback) {
|
||||
let key = registeredCallbacks.indexOf(callback);
|
||||
return key >= 0 ? key : (registeredCallbacks.push(callback) - 1);
|
||||
}
|
||||
|
||||
function serialize(realmId, value) {
|
||||
|
@ -99,7 +96,7 @@ function serialize(realmId, value) {
|
|||
return {type: objectTypes.UNDEFINED};
|
||||
}
|
||||
if (typeof value == 'function') {
|
||||
return {type: objectTypes.FUNCTION};
|
||||
return {type: objectTypes.FUNCTION, value: registerCallback(value)};
|
||||
}
|
||||
if (!value || typeof value != 'object') {
|
||||
return {value: value};
|
||||
|
@ -189,5 +186,20 @@ function sendRequest(command, data, host = sessionHost) {
|
|||
throw new Error(error || `Invalid response for "${command}"`);
|
||||
}
|
||||
|
||||
let callback = response.callback;
|
||||
if (callback != null) {
|
||||
let result;
|
||||
let error;
|
||||
try {
|
||||
let realmId = data.realmId;
|
||||
let args = deserialize(realmId, response.arguments);
|
||||
result = registeredCallbacks[callback].apply(null, args);
|
||||
result = serialize(realmId, result);
|
||||
} catch (e) {
|
||||
error = e.message || ('' + e);
|
||||
}
|
||||
return sendRequest('callback_result', {callback, result, error});
|
||||
}
|
||||
|
||||
return response.result;
|
||||
}
|
||||
|
|
|
@ -46,13 +46,13 @@ export function createMethod(type, name, mutates) {
|
|||
throw new TypeError(name + ' method was called on an object of the wrong type!');
|
||||
}
|
||||
|
||||
let result = rpc.callMethod(realmId, id, name, Array.from(arguments));
|
||||
|
||||
if (mutates) {
|
||||
fireMutationListeners(realmId);
|
||||
try {
|
||||
return rpc.callMethod(realmId, id, name, Array.from(arguments));
|
||||
} finally {
|
||||
if (mutates) {
|
||||
fireMutationListeners(realmId);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package io.realm.react;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
@ -19,7 +17,6 @@ import java.util.Enumeration;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
|
||||
|
@ -28,7 +25,6 @@ public class RealmReactModule extends ReactContextBaseJavaModule {
|
|||
private static boolean sentAnalytics = false;
|
||||
|
||||
private AndroidWebServer webServer;
|
||||
private Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
static {
|
||||
SoLoader.loadLibrary("realmreact");
|
||||
|
@ -156,25 +152,11 @@ public class RealmReactModule extends ReactContextBaseJavaModule {
|
|||
e.printStackTrace();
|
||||
}
|
||||
final String json = map.get("postData");
|
||||
final String[] jsonResponse = new String[1];
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
// Process the command on the UI thread
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
jsonResponse[0] = processChromeDebugCommand(cmdUri, json);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
try {
|
||||
latch.await();
|
||||
Response response = newFixedLengthResponse(jsonResponse[0]);
|
||||
response.addHeader("Access-Control-Allow-Origin", "http://localhost:8081");
|
||||
return response;
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
final String jsonResponse = processChromeDebugCommand(cmdUri, json);
|
||||
|
||||
Response response = newFixedLengthResponse(jsonResponse);
|
||||
response.addHeader("Access-Control-Allow-Origin", "http://localhost:8081");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -213,23 +213,24 @@ RCT_REMAP_METHOD(emit, emitEvent:(NSString *)eventName withObject:(id)object) {
|
|||
[_webServer addDefaultHandlerForMethod:@"POST"
|
||||
requestClass:[GCDWebServerDataRequest class]
|
||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
__typeof__(self) self = weakSelf;
|
||||
RPCServer *rpcServer = self ? self->_rpcServer.get() : nullptr;
|
||||
GCDWebServerResponse *response;
|
||||
|
||||
try {
|
||||
// perform all realm ops on the main thread
|
||||
__block NSData *responseData;
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
RealmReact *self = weakSelf;
|
||||
if (self) {
|
||||
if (_rpcServer) {
|
||||
json args = json::parse([[(GCDWebServerDataRequest *)request text] UTF8String]);
|
||||
std::string responseText = _rpcServer->perform_request(request.path.UTF8String, args).dump();
|
||||
responseData = [NSData dataWithBytes:responseText.c_str() length:responseText.length()];
|
||||
return;
|
||||
}
|
||||
}
|
||||
NSData *responseData;
|
||||
|
||||
if (rpcServer) {
|
||||
json args = json::parse([[(GCDWebServerDataRequest *)request text] UTF8String]);
|
||||
std::string responseText = rpcServer->perform_request(request.path.UTF8String, args).dump();
|
||||
|
||||
responseData = [NSData dataWithBytes:responseText.c_str() length:responseText.length()];
|
||||
}
|
||||
else {
|
||||
// we have been deallocated
|
||||
responseData = [NSData data];
|
||||
});
|
||||
}
|
||||
|
||||
response = [[GCDWebServerDataResponse alloc] initWithData:responseData contentType:@"application/json"];
|
||||
}
|
||||
catch(std::exception &ex) {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2016 Realm Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
|
||||
namespace realm {
|
||||
|
||||
class ConcurrentDequeTimeout : public std::exception {
|
||||
public:
|
||||
ConcurrentDequeTimeout() : std::exception() {}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class ConcurrentDeque {
|
||||
public:
|
||||
T pop_front(size_t timeout = 0) {
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
while (m_deque.empty()) {
|
||||
wait(lock, timeout);
|
||||
}
|
||||
T item = std::move(m_deque.front());
|
||||
m_deque.pop_front();
|
||||
return item;
|
||||
}
|
||||
|
||||
T pop_back(size_t timeout = 0) {
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
while (m_deque.empty()) {
|
||||
wait(lock, timeout);
|
||||
}
|
||||
T item = std::move(m_deque.back());
|
||||
m_deque.pop_back();
|
||||
return item;
|
||||
}
|
||||
|
||||
void push_front(T&& item) {
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
m_deque.push_front(std::move(item));
|
||||
lock.unlock();
|
||||
m_condition.notify_one();
|
||||
}
|
||||
|
||||
void push_back(T&& item) {
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
m_deque.push_back(std::move(item));
|
||||
lock.unlock();
|
||||
m_condition.notify_one();
|
||||
}
|
||||
|
||||
bool empty() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_deque.empty();
|
||||
}
|
||||
|
||||
void wait(std::unique_lock<std::mutex> &lock, size_t timeout = 0) {
|
||||
if (!timeout) {
|
||||
m_condition.wait(lock);
|
||||
}
|
||||
else if (m_condition.wait_for(lock, std::chrono::milliseconds(timeout)) == std::cv_status::timeout) {
|
||||
throw ConcurrentDequeTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::condition_variable m_condition;
|
||||
std::mutex m_mutex;
|
||||
std::deque<T> m_deque;
|
||||
};
|
||||
|
||||
} // realm
|
|
@ -157,6 +157,7 @@
|
|||
F60103141CC4CC8C00EC01BA /* jsc_return_value.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = jsc_return_value.hpp; sourceTree = "<group>"; };
|
||||
F60103151CC4CCFD00EC01BA /* node_return_value.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = node_return_value.hpp; sourceTree = "<group>"; };
|
||||
F60103161CC4CD2F00EC01BA /* node_string.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = node_string.hpp; sourceTree = "<group>"; };
|
||||
F6079B181CD3EB9000BD2401 /* concurrent_deque.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = concurrent_deque.hpp; sourceTree = "<group>"; };
|
||||
F61378781C18EAAC008BFC51 /* js */ = {isa = PBXFileReference; lastKnownFileType = folder; path = js; sourceTree = "<group>"; };
|
||||
F620F0521CAF0B600082977B /* js_class.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = js_class.hpp; sourceTree = "<group>"; };
|
||||
F620F0531CAF2EF70082977B /* jsc_class.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = jsc_class.hpp; sourceTree = "<group>"; };
|
||||
|
@ -259,6 +260,7 @@
|
|||
029048351C042A3C00ABDED4 /* platform.hpp */,
|
||||
0290480F1C0428DF00ABDED4 /* rpc.cpp */,
|
||||
029048101C0428DF00ABDED4 /* rpc.hpp */,
|
||||
F6079B181CD3EB9000BD2401 /* concurrent_deque.hpp */,
|
||||
);
|
||||
name = RealmJS;
|
||||
path = ..;
|
||||
|
|
|
@ -253,7 +253,7 @@ struct Exception : public std::runtime_error {
|
|||
const Protected<ValueType> m_value;
|
||||
|
||||
Exception(ContextType ctx, const std::string &message)
|
||||
: std::runtime_error(message), m_value(value(ctx, message)) {}
|
||||
: std::runtime_error(message), m_value(ctx, value(ctx, message)) {}
|
||||
Exception(ContextType ctx, const ValueType &val)
|
||||
: std::runtime_error(std::string(Value<T>::to_string(ctx, val))), m_value(ctx, val) {}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ class Protected<JSGlobalContextRef> {
|
|||
JSGlobalContextRef m_context;
|
||||
|
||||
public:
|
||||
Protected() : m_context(nullptr) {}
|
||||
Protected(const Protected<JSGlobalContextRef> &other) : Protected(other.m_context) {}
|
||||
Protected(Protected<JSGlobalContextRef> &&other) : m_context(other.m_context) {
|
||||
other.m_context = nullptr;
|
||||
|
@ -43,6 +44,9 @@ class Protected<JSGlobalContextRef> {
|
|||
operator JSGlobalContextRef() const {
|
||||
return m_context;
|
||||
}
|
||||
operator bool() const {
|
||||
return m_context != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
|
@ -51,6 +55,7 @@ class Protected<JSValueRef> {
|
|||
JSValueRef m_value;
|
||||
|
||||
public:
|
||||
Protected() {}
|
||||
Protected(const Protected<JSValueRef> &other) : Protected(other.m_context, other.m_value) {}
|
||||
Protected(Protected<JSValueRef> &&other) : m_context(other.m_context), m_value(other.m_value) {
|
||||
other.m_context = nullptr;
|
||||
|
@ -67,11 +72,15 @@ class Protected<JSValueRef> {
|
|||
operator JSValueRef() const {
|
||||
return m_value;
|
||||
}
|
||||
operator bool() const {
|
||||
return m_value != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
class Protected<JSObjectRef> : public Protected<JSValueRef> {
|
||||
public:
|
||||
Protected() : Protected<JSValueRef>() {}
|
||||
Protected(const Protected<JSObjectRef> &other) : Protected<JSValueRef>(other) {}
|
||||
Protected(Protected<JSObjectRef> &&other) : Protected<JSValueRef>(std::move(other)) {}
|
||||
Protected(JSContextRef ctx, JSObjectRef value) : Protected<JSValueRef>(ctx, value) {}
|
||||
|
|
|
@ -29,11 +29,15 @@ class Protected {
|
|||
Nan::Persistent<MemberType, v8::CopyablePersistentTraits<MemberType>> m_value;
|
||||
|
||||
public:
|
||||
Protected() {}
|
||||
Protected(v8::Local<MemberType> value) : m_value(value) {}
|
||||
|
||||
operator v8::Local<MemberType>() const {
|
||||
return Nan::New(m_value);
|
||||
}
|
||||
operator bool() const {
|
||||
return m_value.isEmpty();
|
||||
}
|
||||
bool operator==(const v8::Local<MemberType> &other) const {
|
||||
return m_value == other;
|
||||
}
|
||||
|
@ -55,6 +59,7 @@ namespace js {
|
|||
template<>
|
||||
class Protected<node::Types::GlobalContext> : public node::Protected<v8::Context> {
|
||||
public:
|
||||
Protected() : node::Protected<v8::Context>() {}
|
||||
Protected(v8::Local<v8::Context> ctx) : node::Protected<v8::Context>(ctx) {}
|
||||
|
||||
operator v8::Isolate*() const {
|
||||
|
@ -65,18 +70,21 @@ class Protected<node::Types::GlobalContext> : public node::Protected<v8::Context
|
|||
template<>
|
||||
class Protected<node::Types::Value> : public node::Protected<v8::Value> {
|
||||
public:
|
||||
Protected() : node::Protected<v8::Value>() {}
|
||||
Protected(v8::Isolate* isolate, v8::Local<v8::Value> value) : node::Protected<v8::Value>(value) {}
|
||||
};
|
||||
|
||||
template<>
|
||||
class Protected<node::Types::Object> : public node::Protected<v8::Object> {
|
||||
public:
|
||||
Protected() : node::Protected<v8::Object>() {}
|
||||
Protected(v8::Isolate* isolate, v8::Local<v8::Object> object) : node::Protected<v8::Object>(object) {}
|
||||
};
|
||||
|
||||
template<>
|
||||
class Protected<node::Types::Function> : public node::Protected<v8::Function> {
|
||||
public:
|
||||
Protected() : node::Protected<v8::Function>() {}
|
||||
Protected(v8::Isolate* isolate, v8::Local<v8::Function> object) : node::Protected<v8::Function>(object) {}
|
||||
};
|
||||
|
||||
|
|
179
src/rpc.cpp
179
src/rpc.cpp
|
@ -22,9 +22,7 @@
|
|||
#include <string>
|
||||
|
||||
#include "rpc.hpp"
|
||||
|
||||
#include "jsc_init.hpp"
|
||||
#include "jsc_types.hpp"
|
||||
|
||||
#include "base64.hpp"
|
||||
#include "object_accessor.hpp"
|
||||
|
@ -43,10 +41,57 @@ static const char * const RealmObjectTypesFunction = "function";
|
|||
static const char * const RealmObjectTypesList = "list";
|
||||
static const char * const RealmObjectTypesObject = "object";
|
||||
static const char * const RealmObjectTypesResults = "results";
|
||||
static const char * const RealmObjectTypesRealm = "realm";
|
||||
static const char * const RealmObjectTypesUndefined = "undefined";
|
||||
|
||||
static RPCServer*& get_rpc_server(JSGlobalContextRef ctx) {
|
||||
static std::map<JSGlobalContextRef, RPCServer*> s_map;
|
||||
return s_map[ctx];
|
||||
}
|
||||
|
||||
RPCWorker::RPCWorker() {
|
||||
m_thread = std::thread([this]() {
|
||||
// TODO: Create ALooper/CFRunLoop to support async calls.
|
||||
while (!m_stop) {
|
||||
try_run_task();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RPCWorker::~RPCWorker() {
|
||||
m_stop = true;
|
||||
m_thread.join();
|
||||
}
|
||||
|
||||
void RPCWorker::add_task(std::function<json()> task) {
|
||||
m_tasks.push_back(std::packaged_task<json()>(task));
|
||||
}
|
||||
|
||||
json RPCWorker::pop_task_result() {
|
||||
// This might block until a future has been added.
|
||||
auto future = m_futures.pop_back();
|
||||
|
||||
// This will block until a return value (or exception) is available.
|
||||
return future.get();
|
||||
}
|
||||
|
||||
void RPCWorker::try_run_task() {
|
||||
try {
|
||||
// Use a 10 millisecond timeout to keep this thread unblocked.
|
||||
auto task = m_tasks.pop_back(10);
|
||||
task();
|
||||
|
||||
// Since this can be called recursively, it must be pushed to the front of the queue *after* running the task.
|
||||
m_futures.push_front(task.get_future());
|
||||
}
|
||||
catch (ConcurrentDequeTimeout &) {
|
||||
// We tried.
|
||||
}
|
||||
}
|
||||
|
||||
RPCServer::RPCServer() {
|
||||
m_context = JSGlobalContextCreate(NULL);
|
||||
get_rpc_server(m_context) = this;
|
||||
|
||||
// JavaScriptCore crashes when trying to walk up the native stack to print the stacktrace.
|
||||
// FIXME: Avoid having to do this!
|
||||
|
@ -65,7 +110,7 @@ RPCServer::RPCServer() {
|
|||
return (json){{"result", m_session_id}};
|
||||
};
|
||||
m_requests["/create_realm"] = [this](const json dict) {
|
||||
JSObjectRef realm_constructor = m_session_id ? m_objects[m_session_id] : NULL;
|
||||
JSObjectRef realm_constructor = m_session_id ? JSObjectRef(m_objects[m_session_id]) : NULL;
|
||||
if (!realm_constructor) {
|
||||
throw std::runtime_error("Realm constructor not found!");
|
||||
}
|
||||
|
@ -82,27 +127,6 @@ RPCServer::RPCServer() {
|
|||
RPCObjectID realm_id = store_object(realm_object);
|
||||
return (json){{"result", realm_id}};
|
||||
};
|
||||
m_requests["/begin_transaction"] = [this](const json dict) {
|
||||
RPCObjectID realm_id = dict["realmId"].get<RPCObjectID>();
|
||||
SharedRealm realm = *jsc::Object::get_internal<js::RealmClass<jsc::Types>>(m_objects[realm_id]);
|
||||
|
||||
realm->begin_transaction();
|
||||
return json::object();
|
||||
};
|
||||
m_requests["/cancel_transaction"] = [this](const json dict) {
|
||||
RPCObjectID realm_id = dict["realmId"].get<RPCObjectID>();
|
||||
SharedRealm realm = *jsc::Object::get_internal<js::RealmClass<jsc::Types>>(m_objects[realm_id]);
|
||||
|
||||
realm->cancel_transaction();
|
||||
return json::object();
|
||||
};
|
||||
m_requests["/commit_transaction"] = [this](const json dict) {
|
||||
RPCObjectID realm_id = dict["realmId"].get<RPCObjectID>();
|
||||
SharedRealm realm = *jsc::Object::get_internal<js::RealmClass<jsc::Types>>(m_objects[realm_id]);
|
||||
|
||||
realm->commit_transaction();
|
||||
return json::object();
|
||||
};
|
||||
m_requests["/call_method"] = [this](const json dict) {
|
||||
JSObjectRef object = m_objects[dict["id"].get<RPCObjectID>()];
|
||||
std::string method_string = dict["name"].get<std::string>();
|
||||
|
@ -148,20 +172,17 @@ RPCServer::RPCServer() {
|
|||
};
|
||||
m_requests["/dispose_object"] = [this](const json dict) {
|
||||
RPCObjectID oid = dict["id"].get<RPCObjectID>();
|
||||
JSValueUnprotect(m_context, m_objects[oid]);
|
||||
m_objects.erase(oid);
|
||||
return json::object();
|
||||
};
|
||||
m_requests["/clear_test_state"] = [this](const json dict) {
|
||||
for (auto object : m_objects) {
|
||||
// The session ID points to the Realm constructor object, which should remain.
|
||||
if (object.first == m_session_id) {
|
||||
continue;
|
||||
if (object.first != m_session_id) {
|
||||
m_objects.erase(object.first);
|
||||
}
|
||||
|
||||
JSValueUnprotect(m_context, object.second);
|
||||
m_objects.erase(object.first);
|
||||
}
|
||||
m_callbacks.clear();
|
||||
JSGarbageCollect(m_context);
|
||||
js::delete_all_realms();
|
||||
return json::object();
|
||||
|
@ -169,24 +190,74 @@ RPCServer::RPCServer() {
|
|||
}
|
||||
|
||||
RPCServer::~RPCServer() {
|
||||
for (auto item : m_objects) {
|
||||
JSValueUnprotect(m_context, item.second);
|
||||
}
|
||||
|
||||
get_rpc_server(m_context) = nullptr;
|
||||
JSGlobalContextRelease(m_context);
|
||||
}
|
||||
|
||||
json RPCServer::perform_request(std::string name, json &args) {
|
||||
try {
|
||||
void RPCServer::run_callback(JSContextRef ctx, JSObjectRef this_object, size_t argc, const JSValueRef arguments[], jsc::ReturnValue &return_value) {
|
||||
RPCServer* server = get_rpc_server(JSContextGetGlobalContext(ctx));
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The first argument was curried to be the callback id.
|
||||
RPCObjectID callback_id = jsc::Value::to_number(ctx, arguments[0]);
|
||||
JSObjectRef arguments_array = jsc::Object::create_array(ctx, uint32_t(argc - 1), argc == 1 ? nullptr : arguments + 1);
|
||||
json arguments_json = server->serialize_json_value(arguments_array);
|
||||
|
||||
// The next task on the stack will instruct the JS to run this callback.
|
||||
// This captures references since it will be executed before exiting this function.
|
||||
server->m_worker.add_task([&]() -> json {
|
||||
return {
|
||||
{"callback", callback_id},
|
||||
{"arguments", arguments_json},
|
||||
};
|
||||
});
|
||||
|
||||
// Wait for the next callback result to come off the result stack.
|
||||
while (server->m_callback_results.empty()) {
|
||||
// This may recursively bring us into another callback, hence the callback results being a stack.
|
||||
server->m_worker.try_run_task();
|
||||
}
|
||||
|
||||
json results = server->m_callback_results.pop_back();
|
||||
json error = results["error"];
|
||||
|
||||
// The callback id should be identical!
|
||||
assert(callback_id == results["callback"].get<RPCObjectID>());
|
||||
|
||||
if (!error.is_null()) {
|
||||
throw jsc::Exception(ctx, error.get<std::string>());
|
||||
}
|
||||
|
||||
return_value.set(server->deserialize_json_value(results["result"]));
|
||||
}
|
||||
|
||||
json RPCServer::perform_request(std::string name, const json &args) {
|
||||
std::lock_guard<std::mutex> lock(m_request_mutex);
|
||||
|
||||
// Only create_session is allowed without the correct session id (since it creates the session id).
|
||||
if (name != "/create_session" && m_session_id != args["sessionId"].get<RPCObjectID>()) {
|
||||
return {{"error", "Invalid session ID"}};
|
||||
}
|
||||
|
||||
// The callback_result message contains the return value (or exception) of a callback ran by run_callback().
|
||||
if (name == "/callback_result") {
|
||||
json results(args);
|
||||
m_callback_results.push_back(std::move(results));
|
||||
}
|
||||
else {
|
||||
RPCRequest action = m_requests[name];
|
||||
assert(action);
|
||||
|
||||
if (name == "/create_session" || m_session_id == args["sessionId"].get<RPCObjectID>()) {
|
||||
m_worker.add_task([=] {
|
||||
return action(args);
|
||||
}
|
||||
else {
|
||||
return {{"error", "Invalid session ID"}};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// This will either be the return value (or exception) of the action perform, OR an instruction to run a callback.
|
||||
return m_worker.pop_task_result();
|
||||
} catch (std::exception &exception) {
|
||||
return {{"error", exception.what()}};
|
||||
}
|
||||
|
@ -194,9 +265,9 @@ json RPCServer::perform_request(std::string name, json &args) {
|
|||
|
||||
RPCObjectID RPCServer::store_object(JSObjectRef object) {
|
||||
static RPCObjectID s_next_id = 1;
|
||||
|
||||
RPCObjectID next_id = s_next_id++;
|
||||
JSValueProtect(m_context, object);
|
||||
m_objects[next_id] = object;
|
||||
m_objects.emplace(next_id, js::Protected<JSObjectRef>(m_context, object));
|
||||
return next_id;
|
||||
}
|
||||
|
||||
|
@ -244,6 +315,12 @@ json RPCServer::serialize_json_value(JSValueRef js_value) {
|
|||
{"schema", serialize_object_schema(results->get_object_schema())}
|
||||
};
|
||||
}
|
||||
else if (jsc::Object::is_instance<js::RealmClass<jsc::Types>>(m_context, js_object)) {
|
||||
return {
|
||||
{"type", RealmObjectTypesRealm},
|
||||
{"id", store_object(js_object)},
|
||||
};
|
||||
}
|
||||
else if (jsc::Value::is_array(m_context, js_object)) {
|
||||
uint32_t length = jsc::Object::validated_get_length(m_context, js_object);
|
||||
std::vector<json> array;
|
||||
|
@ -313,8 +390,20 @@ JSValueRef RPCServer::deserialize_json_value(const json dict) {
|
|||
std::string type_string = type.get<std::string>();
|
||||
|
||||
if (type_string == RealmObjectTypesFunction) {
|
||||
// FIXME: Make this actually call the function by its id once we need it to.
|
||||
return JSObjectMakeFunction(m_context, NULL, 0, NULL, jsc::String(""), NULL, 1, NULL);
|
||||
RPCObjectID callback_id = value.get<RPCObjectID>();
|
||||
|
||||
if (!m_callbacks.count(callback_id)) {
|
||||
JSObjectRef callback = JSObjectMakeFunctionWithCallback(m_context, nullptr, js::wrap<run_callback>);
|
||||
|
||||
// Curry the first argument to be the callback id.
|
||||
JSValueRef bind_args[2] = {jsc::Value::from_null(m_context), jsc::Value::from_number(m_context, callback_id)};
|
||||
JSValueRef bound_callback = jsc::Object::call_method(m_context, callback, "bind", 2, bind_args);
|
||||
|
||||
callback = jsc::Value::to_function(m_context, bound_callback);
|
||||
m_callbacks.emplace(callback_id, js::Protected<JSObjectRef>(m_context, callback));
|
||||
}
|
||||
|
||||
return m_callbacks.at(callback_id);
|
||||
}
|
||||
else if (type_string == RealmObjectTypesDictionary) {
|
||||
JSObjectRef js_object = jsc::Object::create_empty(m_context);
|
||||
|
|
33
src/rpc.hpp
33
src/rpc.hpp
|
@ -18,8 +18,14 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <thread>
|
||||
|
||||
#include "concurrent_deque.hpp"
|
||||
#include "json.hpp"
|
||||
#include "jsc_types.hpp"
|
||||
#include "jsc_protected.hpp"
|
||||
|
||||
namespace realm {
|
||||
|
||||
|
@ -28,20 +34,43 @@ class ObjectSchema;
|
|||
namespace rpc {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
using RPCObjectID = u_int64_t;
|
||||
using RPCRequest = std::function<json(const json)>;
|
||||
|
||||
class RPCWorker {
|
||||
public:
|
||||
RPCWorker();
|
||||
~RPCWorker();
|
||||
|
||||
void add_task(std::function<json()>);
|
||||
json pop_task_result();
|
||||
void try_run_task();
|
||||
|
||||
private:
|
||||
bool m_stop = false;
|
||||
std::thread m_thread;
|
||||
ConcurrentDeque<std::packaged_task<json()>> m_tasks;
|
||||
ConcurrentDeque<std::future<json>> m_futures;
|
||||
};
|
||||
|
||||
class RPCServer {
|
||||
public:
|
||||
RPCServer();
|
||||
~RPCServer();
|
||||
json perform_request(std::string name, json &args);
|
||||
json perform_request(std::string name, const json &args);
|
||||
|
||||
private:
|
||||
JSGlobalContextRef m_context;
|
||||
std::mutex m_request_mutex;
|
||||
std::map<std::string, RPCRequest> m_requests;
|
||||
std::map<RPCObjectID, JSObjectRef> m_objects;
|
||||
std::map<RPCObjectID, js::Protected<JSObjectRef>> m_objects;
|
||||
std::map<RPCObjectID, js::Protected<JSObjectRef>> m_callbacks;
|
||||
ConcurrentDeque<json> m_callback_results;
|
||||
RPCObjectID m_session_id;
|
||||
RPCWorker m_worker;
|
||||
|
||||
static void run_callback(JSContextRef, JSObjectRef, size_t, const JSValueRef[], jsc::ReturnValue &);
|
||||
|
||||
RPCObjectID store_object(JSObjectRef object);
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ module.exports = {
|
|||
}
|
||||
catch (e) {
|
||||
caught = true;
|
||||
if (e != expectedException) {
|
||||
if (e.message != expectedException.message) {
|
||||
throw new TestFailureError('Expected exception "' + expectedException + '" not thrown - instead caught: "' + e + '"');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ module.exports = BaseTest.extend({
|
|||
});
|
||||
|
||||
// migration function exceptions should propogate
|
||||
var exception = 'expected exception';
|
||||
var exception = new Error('expected exception');
|
||||
realm = undefined;
|
||||
TestCase.assertThrowsException(function() {
|
||||
realm = new Realm({schema: [], schemaVersion: 3, migration: function() {
|
||||
|
|
Loading…
Reference in New Issue