#include "rx/Rx.h" #include "platform_event/PlatformEvent.h" #include #include #include namespace eve::rx { // --------------------------------------------------------------------------- // Value conversion helpers. // --------------------------------------------------------------------------- int64_t Value::toInt() const { switch (type) { case Type::Int: return i; case Type::Float: return static_cast(f); case Type::Bool: return b ? 1 : 0; case Type::String: try { return static_cast(std::stoll(s)); } catch (...) { return 0; } default: return 0; } } double Value::toFloat() const { switch (type) { case Type::Int: return static_cast(i); case Type::Float: return f; case Type::Bool: return b ? 1.0 : 0.0; case Type::String: try { return std::stod(s); } catch (...) { return 0.0; } default: return 0.0; } } bool Value::toBool() const { switch (type) { case Type::Int: return i != 0; case Type::Float: return f != 0.0; case Type::Bool: return b; case Type::String: return !s.empty(); case Type::Ptr: return p != nullptr; default: return false; } } std::string Value::toString() const { switch (type) { case Type::Int: return std::to_string(i); case Type::Float: return std::to_string(f); case Type::Bool: return b ? "true" : "false"; case Type::String: return s; case Type::Ptr: return "ptr"; default: return ""; } } bool Value::equals(const Value& o) const { if (type != o.type) return false; switch (type) { case Type::Int: return i == o.i; case Type::Float: return f == o.f; case Type::Bool: return b == o.b; case Type::String: return s == o.s; case Type::Ptr: return p == o.p; default: return true; } } } // namespace eve::rx // --------------------------------------------------------------------------- // ssq::detail specializations so Value can cross the Squirrel boundary in // bound method parameters / return values. Declared here so all bindings in // this TU instantiate against them. // --------------------------------------------------------------------------- namespace ssq { namespace detail { template <> inline void pushValue(HSQUIRRELVM vm, const eve::rx::Value& v) { switch (v.type) { case eve::rx::Value::Type::Nil: sq_pushnull(vm); break; case eve::rx::Value::Type::Int: sq_pushinteger(vm, static_cast(v.i)); break; case eve::rx::Value::Type::Float: sq_pushfloat(vm, static_cast(v.f)); break; case eve::rx::Value::Type::Bool: sq_pushbool(vm, v.b ? SQTrue : SQFalse); break; case eve::rx::Value::Type::String: sq_pushstring(vm, v.s.c_str(), static_cast(v.s.size())); break; case eve::rx::Value::Type::Ptr: sq_pushuserpointer(vm, v.p); break; } } template <> inline eve::rx::Value popValue(HSQUIRRELVM vm, SQInteger index) { switch (sq_gettype(vm, index)) { case OT_NULL: return eve::rx::Value::makeNil(); case OT_INTEGER: { SQInteger i = 0; sq_getinteger(vm, index, &i); return eve::rx::Value::makeInt(static_cast(i)); } case OT_FLOAT: { SQFloat f = 0; sq_getfloat(vm, index, &f); return eve::rx::Value::makeFloat(static_cast(f)); } case OT_BOOL: { SQBool b = SQFalse; sq_getbool(vm, index, &b); return eve::rx::Value::makeBool(b == SQTrue); } case OT_STRING: { const SQChar* s = nullptr; sq_getstring(vm, index, &s); return eve::rx::Value::makeString(s ? s : ""); } case OT_USERPOINTER: { SQUserPointer p = nullptr; sq_getuserpointer(vm, index, &p); return eve::rx::Value::makePtr(p); } default: return eve::rx::Value::makeNil(); } } } // namespace detail } // namespace ssq namespace eve::rx { // --------------------------------------------------------------------------- // Script-facing aliases (Value-typed). // --------------------------------------------------------------------------- using SubjectV = Subject; using BehaviorSubjectV = BehaviorSubject; using ReplaySubjectV = ReplaySubject; using ObservableV = Observable; using ReactivePropertyV = ReactiveProperty; namespace { // Call a script function with the given Value args; returns its Value result. Value callScript(const ssq::Function& fn, const std::vector& args, bool wantResult) { if (fn.isEmpty()) return Value::makeNil(); HSQUIRRELVM raw = fn.getHandle(); SQInteger top = sq_gettop(raw); sq_pushobject(raw, fn.getRaw()); sq_pushroottable(raw); for (const auto& a : args) ssq::detail::pushValue(raw, a); Value result = Value::makeNil(); if (SQ_SUCCEEDED(sq_call(raw, static_cast(args.size() + 1), wantResult ? SQTrue : SQFalse, SQTrue))) { if (wantResult && sq_gettop(raw) > top) result = ssq::detail::popValue(raw, -1); } sq_settop(raw, top); return result; } std::function scriptCompleted(ssq::Function fn) { return [fn]() { callScript(fn, {}, false); }; } std::function scriptNext(ssq::Function fn) { return [fn](const Value& v) { callScript(fn, {v}, false); }; } std::function scriptError(ssq::Function fn) { return [fn](const std::string& e) { callScript(fn, {Value::makeString(e)}, false); }; } bool isClosure(const ssq::Object& obj) { if (obj.isEmpty()) return false; auto t = obj.getType(); return t == ssq::Type::CLOSURE || t == ssq::Type::NATIVECLOSURE; } // Return a copy of obj if it is a usable closure, else an empty Object. ssq::Object asClosure(const ssq::Object& obj) { if (isClosure(obj)) return obj; return ssq::Object(); } Observer makeObserver(ssq::Object next, ssq::Object error, ssq::Object done) { Observer o; ssq::Object n = asClosure(next); ssq::Object e = asClosure(error); ssq::Object c = asClosure(done); if (!n.isEmpty()) o.onNext = scriptNext(n.toFunction()); if (!e.isEmpty()) o.onError = scriptError(e.toFunction()); if (!c.isEmpty()) o.onCompleted = scriptCompleted(c.toFunction()); return o; } Subscription* doSubscribe(ObservableV* obs, ssq::Object next, ssq::Object error, ssq::Object done) { if (!obs) throw eve::Exception("Rx.subscribe: null observable"); return new Subscription(obs->subscribe(makeObserver(next, error, done))); } // map(fn) : ObservableV -> ObservableV via script function returning Value. ObservableV* doMap(ObservableV* self, ssq::Function fn) { if (!self) throw eve::Exception("Rx.map: null observable"); return new AnonymousObservable([self, fn](Observer out) { Observer in; in.onNext = [out, fn](const Value& v) { out.next(callScript(fn, {v}, true)); }; in.onError = [out](const std::string& e) { out.error(e); }; in.onCompleted = [out]() { out.completed(); }; return self->subscribe(std::move(in)); }); } ObservableV* doFilter(ObservableV* self, ssq::Function pred) { if (!self) throw eve::Exception("Rx.filter: null observable"); return new AnonymousObservable([self, pred](Observer out) { Observer in; in.onNext = [out, pred](const Value& v) { if (callScript(pred, {v}, true).toBool()) out.next(v); }; in.onError = [out](const std::string& e) { out.error(e); }; in.onCompleted = [out]() { out.completed(); }; return self->subscribe(std::move(in)); }); } ObservableV* doTake(ObservableV* self, int n) { if (!self) throw eve::Exception("Rx.take: null observable"); return new AnonymousObservable([self, n](Observer out) { auto remaining = std::make_shared(n); Observer in; in.onNext = [out, remaining](const Value& v) { if (*remaining <= 0) return; *remaining -= 1; out.next(v); if (*remaining == 0) out.completed(); }; in.onError = [out](const std::string& e) { out.error(e); }; in.onCompleted = [out]() { out.completed(); }; return self->subscribe(std::move(in)); }); } ObservableV* doSkip(ObservableV* self, int n) { if (!self) throw eve::Exception("Rx.skip: null observable"); return new AnonymousObservable([self, n](Observer out) { auto remaining = std::make_shared(n); Observer in; in.onNext = [out, remaining](const Value& v) { if (*remaining > 0) { *remaining -= 1; return; } out.next(v); }; in.onError = [out](const std::string& e) { out.error(e); }; in.onCompleted = [out]() { out.completed(); }; return self->subscribe(std::move(in)); }); } ObservableV* doFirst(ObservableV* self) { if (!self) throw eve::Exception("Rx.first: null observable"); return new AnonymousObservable([self](Observer out) { auto done = std::make_shared(false); Observer in; in.onNext = [out, done](const Value& v) { if (*done) return; *done = true; out.next(v); out.completed(); }; in.onError = [out](const std::string& e) { out.error(e); }; in.onCompleted = [out]() { out.completed(); }; return self->subscribe(std::move(in)); }); } ObservableV* doDistinctUntilChanged(ObservableV* self) { if (!self) throw eve::Exception("Rx.distinctUntilChanged: null observable"); return new AnonymousObservable([self](Observer out) { auto last = std::make_shared(); auto has = std::make_shared(false); Observer in; in.onNext = [out, last, has](const Value& v) { if (*has && last->equals(v)) return; *has = true; *last = v; out.next(v); }; in.onError = [out](const std::string& e) { out.error(e); }; in.onCompleted = [out]() { out.completed(); }; return self->subscribe(std::move(in)); }); } } // namespace // --------------------------------------------------------------------------- // Rx module. // --------------------------------------------------------------------------- class Rx : public Module { public: Module_REG(Rx); SubjectV* newSubject() { return new SubjectV(); } BehaviorSubjectV* newBehaviorSubject(const Value& initial) { return new BehaviorSubjectV(initial); } ReplaySubjectV* newReplaySubject(int capacity = 0) { return new ReplaySubjectV(capacity); } ReactivePropertyV* newProperty(const Value& initial) { return new ReactivePropertyV(initial); } // Event bridge: fromEvent(name) returns an Observable that forwards matching // messages pushed by pump() (fed from an eve::platform_event::PlatformEvent queue). ObservableV* fromEvent(const std::string& eventName) { auto subject = std::make_shared(); { std::lock_guard lock(mu_); bridges_[eventName] = subject; } // Return a fresh observable that subscribes to the shared subject. The // returned AnonymousObservable is owned by the script; the shared subject // stays alive in bridges_ so pump() can keep feeding it. return new AnonymousObservable([subject](Observer out) { return subject->subscribe(std::move(out)); }); } void pump(platform_event::PlatformEvent* ev) { if (!ev) return; while (auto msg = ev->pollOwned()) { std::string eventName = msg->name; std::string data; for (const auto& a : msg->args) { if (a.type == platform_event::Variant::Type::String) { data = a.s; break; } } std::shared_ptr subject; { std::lock_guard lock(mu_); auto it = bridges_.find(eventName); if (it != bridges_.end()) subject = it->second; } if (subject) subject->onNext(Value::makeString(data)); } } private: std::mutex mu_; std::unordered_map> bridges_; }; Module_IMPL(Rx, new Rx()); // --------------------------------------------------------------------------- // Squirrel binding. // --------------------------------------------------------------------------- namespace { using ObsFn = std::function; void bindCommon(ssq::Class& cls) { cls.addFunc("subscribe", std::function( [](ObservableV* self, ssq::Object n) { return doSubscribe(self, n, {}, {}); })); cls.addFunc("subscribe3", std::function( [](ObservableV* self, ssq::Object n, ssq::Object e, ssq::Object d) { return doSubscribe(self, n, e, d); })); cls.addFunc("map", std::function( [](ObservableV* self, ssq::Function f) { return doMap(self, f); })); cls.addFunc("filter", std::function( [](ObservableV* self, ssq::Function p) { return doFilter(self, p); })); cls.addFunc("take", std::function( [](ObservableV* self, int n) { return doTake(self, n); })); cls.addFunc("skip", std::function( [](ObservableV* self, int n) { return doSkip(self, n); })); cls.addFunc("first", std::function([](ObservableV* self) { return doFirst(self); })); cls.addFunc("distinctUntilChanged", std::function( [](ObservableV* self) { return doDistinctUntilChanged(self); })); } } // namespace void Rx::expose(ssq::Table& table) { auto cls = table.addClass(name, Rx::create, false); expose(cls); auto obs = table.addClass( "Observable", std::function([]() -> ObservableV* { return nullptr; }), true); bindCommon(obs); auto subject = table.addClass( "Subject", std::function([]() -> SubjectV* { return nullptr; }), true); subject.addFunc("onNext", &SubjectV::onNext); subject.addFunc("onError", &SubjectV::onError); subject.addFunc("onCompleted", &SubjectV::onCompleted); subject.addFunc("hasObservers", &SubjectV::hasObservers); bindCommon(subject); auto behavior = table.addClass( "BehaviorSubject", std::function([]() -> BehaviorSubjectV* { return nullptr; }), true); behavior.addFunc("onNext", &BehaviorSubjectV::onNext); behavior.addFunc("onError", &BehaviorSubjectV::onError); behavior.addFunc("onCompleted", &BehaviorSubjectV::onCompleted); behavior.addFunc("hasObservers", &BehaviorSubjectV::hasObservers); behavior.addFunc("getValue", &BehaviorSubjectV::getValue); behavior.addFunc("setValue", &BehaviorSubjectV::setValue); bindCommon(behavior); auto replay = table.addClass( "ReplaySubject", std::function([]() -> ReplaySubjectV* { return nullptr; }), true); replay.addFunc("onNext", &ReplaySubjectV::onNext); replay.addFunc("onError", &ReplaySubjectV::onError); replay.addFunc("onCompleted", &ReplaySubjectV::onCompleted); replay.addFunc("hasObservers", &ReplaySubjectV::hasObservers); bindCommon(replay); auto prop = table.addClass( "ReactiveProperty", std::function([]() -> ReactivePropertyV* { return nullptr; }), true); prop.addFunc("get", &ReactivePropertyV::get); prop.addFunc("set", &ReactivePropertyV::set); prop.addFunc("subscribe", std::function( [](ReactivePropertyV* self, ssq::Object n) { if (!self) throw eve::Exception("Rx.subscribe: null property"); return doSubscribe(self->asObservable(), n, {}, {}); })); prop.addFunc("subscribe3", std::function( [](ReactivePropertyV* self, ssq::Object n, ssq::Object e, ssq::Object d) { if (!self) throw eve::Exception("Rx.subscribe: null property"); return doSubscribe(self->asObservable(), n, e, d); })); auto sub = table.addClass( "Subscription", std::function([]() -> Subscription* { return nullptr; }), true); sub.addFunc("dispose", &Subscription::dispose); sub.addFunc("isDisposed", &Subscription::isDisposed); } void Rx::expose(ssq::Class& cls) { cls.addFunc("getName", &Rx::getName); cls.addFunc("newSubject", &Rx::newSubject); cls.addFunc("newBehaviorSubject", &Rx::newBehaviorSubject); cls.addFunc("newReplaySubject", &Rx::newReplaySubject); cls.addFunc("newProperty", &Rx::newProperty); cls.addFunc("fromEvent", &Rx::fromEvent); cls.addFunc("pump", &Rx::pump); } } // namespace eve::rx