@derec-alliance/react-native 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/DeRec.podspec +25 -0
  2. package/LICENSE +201 -0
  3. package/README.md +222 -0
  4. package/android/CMakeLists.txt +38 -0
  5. package/android/build.gradle +79 -0
  6. package/android/src/main/AndroidManifest.xml +12 -0
  7. package/android/src/main/java/org/derec/DeRecModule.kt +56 -0
  8. package/android/src/main/java/org/derec/DeRecPackage.kt +24 -0
  9. package/android/src/main/jni/OnLoad.cpp +39 -0
  10. package/android/src/main/jniLibs/arm64-v8a/libderec_library.a +0 -0
  11. package/android/src/main/jniLibs/armeabi-v7a/libderec_library.a +0 -0
  12. package/android/src/main/jniLibs/x86_64/libderec_library.a +0 -0
  13. package/cpp/Convert.cpp +50 -0
  14. package/cpp/Convert.h +38 -0
  15. package/cpp/DeRecInstaller.cpp +186 -0
  16. package/cpp/DeRecInstaller.h +28 -0
  17. package/cpp/Invoker.h +20 -0
  18. package/cpp/JsCallbackBridge.cpp +144 -0
  19. package/cpp/JsCallbackBridge.h +148 -0
  20. package/cpp/Primitives.cpp +1193 -0
  21. package/cpp/Primitives.h +39 -0
  22. package/cpp/ProtocolHost.cpp +613 -0
  23. package/cpp/ProtocolHost.h +88 -0
  24. package/cpp/RuntimeInvoker.h +29 -0
  25. package/cpp/StoreCallbacks.cpp +1293 -0
  26. package/cpp/StoreCallbacks.h +111 -0
  27. package/cpp/WorkerThread.cpp +74 -0
  28. package/cpp/WorkerThread.h +54 -0
  29. package/cpp/derec_ffi.h +2145 -0
  30. package/ios/DeRec.h +23 -0
  31. package/ios/DeRec.mm +39 -0
  32. package/ios/DeRecFFI.xcframework/Info.plist +48 -0
  33. package/ios/DeRecFFI.xcframework/ios-arm64/Headers/derec_ffi.h +2145 -0
  34. package/ios/DeRecFFI.xcframework/ios-arm64/libderec_library.a +0 -0
  35. package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/Headers/derec_ffi.h +2145 -0
  36. package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/libderec_library.a +0 -0
  37. package/lib/binary.d.ts +19 -0
  38. package/lib/binary.js +164 -0
  39. package/lib/codec.d.ts +16 -0
  40. package/lib/codec.js +80 -0
  41. package/lib/errors.d.ts +23 -0
  42. package/lib/errors.js +35 -0
  43. package/lib/index.d.ts +4 -0
  44. package/lib/index.js +27 -0
  45. package/lib/messages.d.ts +41 -0
  46. package/lib/messages.js +192 -0
  47. package/lib/native.d.ts +11 -0
  48. package/lib/native.js +40 -0
  49. package/lib/primitives.d.ts +137 -0
  50. package/lib/primitives.js +329 -0
  51. package/lib/protocol.d.ts +188 -0
  52. package/lib/protocol.js +514 -0
  53. package/lib/types.d.ts +1014 -0
  54. package/lib/types.js +63 -0
  55. package/package.json +55 -0
  56. package/react-native.config.js +19 -0
  57. package/src/binary.ts +182 -0
  58. package/src/codec.ts +79 -0
  59. package/src/errors.ts +45 -0
  60. package/src/index.ts +7 -0
  61. package/src/messages.ts +192 -0
  62. package/src/native.ts +52 -0
  63. package/src/primitives.ts +770 -0
  64. package/src/protocol.ts +621 -0
  65. package/src/types.ts +1040 -0
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ #include <fbjni/fbjni.h>
5
+ #include <jni.h>
6
+ #include <jsi/jsi.h>
7
+ #include <ReactCommon/CallInvokerHolder.h>
8
+
9
+ #include <memory>
10
+
11
+ #include "DeRecInstaller.h"
12
+ #include "RuntimeInvoker.h"
13
+
14
+ extern "C" JNIEXPORT void JNICALL
15
+ Java_org_derec_DeRecModule_nativeInstall(JNIEnv* env,
16
+ jobject /*thiz*/,
17
+ jlong runtimePtr,
18
+ jobject callInvokerHolder) {
19
+ auto* runtime = reinterpret_cast<facebook::jsi::Runtime*>(runtimePtr);
20
+ if (runtime == nullptr) {
21
+ return;
22
+ }
23
+ auto holder = facebook::jni::alias_ref<
24
+ facebook::react::CallInvokerHolder::javaobject>{
25
+ static_cast<facebook::react::CallInvokerHolder::javaobject>(
26
+ callInvokerHolder)};
27
+ auto invoker = std::make_shared<derec::RuntimeInvoker>(
28
+ holder->cthis()->getCallInvoker());
29
+ derec::install(*runtime, invoker);
30
+ }
31
+
32
+ extern "C" JNIEXPORT void JNICALL
33
+ Java_org_derec_DeRecModule_nativeInvalidate(JNIEnv*, jobject) {
34
+ derec::invalidateAll();
35
+ }
36
+
37
+ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
38
+ return facebook::jni::initialize(vm, [] {});
39
+ }
@@ -0,0 +1,50 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ #include "Convert.h"
5
+
6
+ #include <cstdlib>
7
+ #include <cstring>
8
+
9
+ namespace derec {
10
+
11
+ OwnedBytes allocBytes(size_t len) {
12
+ if (len == 0) {
13
+ return OwnedBytes{nullptr, 0};
14
+ }
15
+ auto* ptr = static_cast<uint8_t*>(std::malloc(len));
16
+ return OwnedBytes{ptr, ptr == nullptr ? 0 : len};
17
+ }
18
+
19
+ void freeBytes(uint8_t* ptr, size_t /*len*/) {
20
+ if (ptr != nullptr) {
21
+ std::free(ptr);
22
+ }
23
+ }
24
+
25
+ std::vector<uint8_t> takeBuffer(DeRecBuffer& buffer) {
26
+ std::vector<uint8_t> out;
27
+ if (buffer.ptr != nullptr && buffer.len > 0) {
28
+ out.assign(buffer.ptr, buffer.ptr + buffer.len);
29
+ }
30
+ derec_free_buffer(buffer.ptr, buffer.len);
31
+ buffer.ptr = nullptr;
32
+ buffer.len = 0;
33
+ return out;
34
+ }
35
+
36
+ std::string takeString(char* owned) {
37
+ if (owned == nullptr) {
38
+ return {};
39
+ }
40
+ std::string out(owned);
41
+ derec_free_string(owned);
42
+ return out;
43
+ }
44
+
45
+ std::pair<std::string, std::string> errorName(int32_t category, int32_t code) {
46
+ return {std::string(derec_error_category_name(category)),
47
+ std::string(derec_error_code_name(code))};
48
+ }
49
+
50
+ } // namespace derec
package/cpp/Convert.h ADDED
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ #pragma once
5
+
6
+ #include <cstddef>
7
+ #include <cstdint>
8
+ #include <string>
9
+ #include <utility>
10
+ #include <vector>
11
+
12
+ extern "C" {
13
+ #include "derec_ffi.h"
14
+ }
15
+
16
+ namespace derec {
17
+
18
+ /// A byte buffer allocated by this binding. Ownership passes to Rust across a
19
+ /// store callback out-parameter; Rust copies the contents and hands the
20
+ /// pointer back through the callback struct's `free_buffer`.
21
+ struct OwnedBytes {
22
+ uint8_t* ptr;
23
+ size_t len;
24
+ };
25
+
26
+ OwnedBytes allocBytes(size_t len);
27
+ void freeBytes(uint8_t* ptr, size_t len);
28
+
29
+ /// Copy `buffer` into a vector and release the Rust allocation.
30
+ std::vector<uint8_t> takeBuffer(DeRecBuffer& buffer);
31
+
32
+ /// Copy a Rust-owned C string and release it via `derec_free_string`.
33
+ std::string takeString(char* owned);
34
+
35
+ /// Resolve the `(category, code)` name pair from the crate's accessors.
36
+ std::pair<std::string, std::string> errorName(int32_t category, int32_t code);
37
+
38
+ } // namespace derec
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ #include "DeRecInstaller.h"
5
+
6
+ #include <memory>
7
+ #include <mutex>
8
+ #include <string>
9
+ #include <utility>
10
+ #include <vector>
11
+
12
+ #include "Convert.h"
13
+ #include "JsCallbackBridge.h"
14
+ #include "Primitives.h"
15
+ #include "ProtocolHost.h"
16
+ #include "StoreCallbacks.h"
17
+
18
+ using namespace facebook;
19
+
20
+ namespace derec {
21
+
22
+ namespace {
23
+
24
+ std::mutex& registryMutex() {
25
+ static std::mutex m;
26
+ return m;
27
+ }
28
+
29
+ std::vector<std::weak_ptr<InstanceState>>& registry() {
30
+ static std::vector<std::weak_ptr<InstanceState>> instances;
31
+ return instances;
32
+ }
33
+
34
+ void requireArgs(jsi::Runtime& rt, const char* name, size_t count, size_t required) {
35
+ if (count < required) {
36
+ throw jsi::JSError(rt, std::string(name) + " expects " +
37
+ std::to_string(required) + " arguments");
38
+ }
39
+ }
40
+
41
+ /// `secret_id` travels through `configJson` as a quoted decimal string (the
42
+ /// same field `DeRecProtocolBuilder.build()` always writes first).
43
+ /// `derec_protocol_new`'s result carries only the opaque handle, not the id,
44
+ /// so `ProtocolHost::secretId()` — which has no FFI accessor of its own —
45
+ /// needs this recovered independently. `JSON.stringify` emits no internal
46
+ /// whitespace, so a direct scan for the field is sufficient; this mirrors
47
+ /// the bare-JSON-integer scanning `StoreCallbacks.cpp` already relies on for
48
+ /// the wire's other compact JSON payloads.
49
+ uint64_t parseSecretIdFromConfigJson(const std::string& json) {
50
+ static const std::string needle = "\"secret_id\":\"";
51
+ size_t pos = json.find(needle);
52
+ if (pos == std::string::npos) {
53
+ return 0;
54
+ }
55
+ pos += needle.size();
56
+ uint64_t value = 0;
57
+ while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') {
58
+ value = value * 10 + static_cast<uint64_t>(json[pos] - '0');
59
+ ++pos;
60
+ }
61
+ return value;
62
+ }
63
+
64
+ /// `protocol_new(configJson: string, communicationInfo: ArrayBuffer, stores:
65
+ /// object) -> HostObject`. The one place that constructs a `ProtocolHost`:
66
+ /// builds the per-instance `InstanceState` and `JsCallbackBridge`, resolves
67
+ /// the six store/transport callback structs via `StoreBindings::create`, and
68
+ /// hands them to `derec_protocol_new` in the FFI's own declared parameter
69
+ /// order — channel, secret, share, user_secret, state, transport, which is
70
+ /// neither alphabetical nor the order the six `with*Store` builder methods
71
+ /// appear in (see `library/src/interop/ffi/protocol/handle/mod.rs`'s
72
+ /// `derec_protocol_new` declaration).
73
+ jsi::Value protocolNew(jsi::Runtime& rt,
74
+ const std::shared_ptr<Invoker>& invoker,
75
+ const jsi::Value&,
76
+ const jsi::Value* args,
77
+ size_t count) {
78
+ requireArgs(rt, "protocol_new", count, 3);
79
+ std::string configJson = args[0].asString(rt).utf8(rt);
80
+ ByteView info = asBytes(rt, args[1]);
81
+ jsi::Object storesObj = args[2].asObject(rt);
82
+
83
+ auto state = std::make_shared<InstanceState>();
84
+ auto bridge = std::make_shared<JsCallbackBridge>(*invoker, state);
85
+ std::shared_ptr<StoreBindings> stores = StoreBindings::create(rt, storesObj, *bridge, state);
86
+
87
+ DeRecProtocolNewResult result = derec_protocol_new(
88
+ reinterpret_cast<const uint8_t*>(configJson.data()), configJson.size(), info.ptr, info.len,
89
+ stores->channel(), stores->secret(), stores->share(), stores->userSecret(), stores->state(),
90
+ stores->transport());
91
+ if (result.error.code != DEREC_CODE_OK) {
92
+ throwDeRecError(rt, result.error);
93
+ }
94
+
95
+ uint64_t secretId = parseSecretIdFromConfigJson(configJson);
96
+
97
+ // Ownership: `stores` (a `StoreBindings`) holds `bridge`'s referent as a
98
+ // plain `JsCallbackBridge&` (see `StoreBindings.h`), not a `shared_ptr` —
99
+ // and a store-callback trampoline can keep `StoreBindings` alive via
100
+ // `shared_from_this()` on the JavaScript `CallInvoker`'s queue after this
101
+ // `ProtocolHost` itself has been fully destroyed. `bridge` therefore must
102
+ // not be scoped to die *with* `ProtocolHost` in the ordinary sense of "a
103
+ // member that is destroyed inside its owner's destructor" — it must
104
+ // outlive `ProtocolHost`'s own destruction, not merely coincide with it.
105
+ //
106
+ // The custom deleter below achieves that without adding a member to
107
+ // `ProtocolHost` (its constructor is fixed): it ties `bridge`'s release to
108
+ // the *same* `shared_ptr` control block that owns the `ProtocolHost`
109
+ // object, but strictly *after* that control block's normal deletion step.
110
+ // A `shared_ptr`'s custom deleter call (`delete p`, which runs
111
+ // `~ProtocolHost` to completion — invalidate, drain the worker, free the
112
+ // handle) always finishes before the control block releases whatever the
113
+ // deleter closure itself captured. So `bridge` is released only once
114
+ // `ProtocolHost`'s entire teardown has already run, which is later than
115
+ // any point at which a worker-thread trampoline could still call
116
+ // `StoreBindings::bridge()` — that call only ever happens synchronously,
117
+ // from inside a `derec_protocol_*` call on the worker thread, and
118
+ // `drainAndJoin` (part of that same teardown) does not return until every
119
+ // such call has finished.
120
+ std::shared_ptr<ProtocolHost> host(
121
+ new ProtocolHost(result.handle, secretId, invoker, state, std::move(stores)),
122
+ [bridge](ProtocolHost* p) mutable {
123
+ delete p;
124
+ // `bridge` (captured by value) is released only after the `delete`
125
+ // above returns, i.e. only after `~ProtocolHost` has fully run.
126
+ });
127
+
128
+ return jsi::Object::createFromHostObject(rt, std::move(host));
129
+ }
130
+
131
+ } // namespace
132
+
133
+ void registerInstance(const std::shared_ptr<InstanceState>& state) {
134
+ std::lock_guard<std::mutex> lock(registryMutex());
135
+ registry().push_back(state);
136
+ }
137
+
138
+ void invalidateAll() {
139
+ std::lock_guard<std::mutex> lock(registryMutex());
140
+ for (auto& weak : registry()) {
141
+ if (auto state = weak.lock()) {
142
+ // The runtime itself is going away, not just one protocol instance:
143
+ // work already queued onto the JavaScript thread must be abandoned
144
+ // rather than run, and the JavaScript values it retains must be leaked
145
+ // rather than released.
146
+ state->invalidateRuntime();
147
+ }
148
+ }
149
+ registry().clear();
150
+ }
151
+
152
+ void install(jsi::Runtime& rt, std::shared_ptr<Invoker> invoker) {
153
+ // `install` runs on the JavaScript thread by construction, so this is the
154
+ // one place that can record which thread that is. `JsCallbackBridge::
155
+ // callSync` asserts against it in debug builds.
156
+ noteJsThread();
157
+
158
+ auto host = jsi::Object(rt);
159
+
160
+ host.setProperty(
161
+ rt, "version",
162
+ jsi::Function::createFromHostFunction(
163
+ rt, jsi::PropNameID::forAscii(rt, "version"), 0,
164
+ [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t)
165
+ -> jsi::Value {
166
+ DeRecProtocolVersion version = derec_protocol_version();
167
+ std::string text = std::to_string(version.major) + "." +
168
+ std::to_string(version.minor);
169
+ return jsi::String::createFromUtf8(rt, text);
170
+ }));
171
+
172
+ installPrimitives(rt, host);
173
+
174
+ host.setProperty(
175
+ rt, "protocol_new",
176
+ jsi::Function::createFromHostFunction(
177
+ rt, jsi::PropNameID::forAscii(rt, "protocol_new"), 3,
178
+ [invoker](jsi::Runtime& rt2, const jsi::Value& thisVal, const jsi::Value* args,
179
+ size_t count) -> jsi::Value {
180
+ return protocolNew(rt2, invoker, thisVal, args, count);
181
+ }));
182
+
183
+ rt.global().setProperty(rt, "__DeRec", std::move(host));
184
+ }
185
+
186
+ } // namespace derec
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ #pragma once
5
+
6
+ #include <jsi/jsi.h>
7
+
8
+ #include <memory>
9
+
10
+ #include "Invoker.h"
11
+ #include "JsCallbackBridge.h"
12
+
13
+ namespace derec {
14
+
15
+ /// Install the `__DeRec` host object into `rt`.
16
+ ///
17
+ /// `invoker` must schedule work on the same JavaScript thread that owns `rt`.
18
+ void install(facebook::jsi::Runtime& rt, std::shared_ptr<Invoker> invoker);
19
+
20
+ /// Signal that the JavaScript runtime is going away. Wakes every protocol
21
+ /// instance blocked in a store callback so their workers can unwind instead
22
+ /// of hanging.
23
+ void invalidateAll();
24
+
25
+ /// Track a protocol instance so `invalidateAll` can reach it at teardown.
26
+ void registerInstance(const std::shared_ptr<InstanceState>& state);
27
+
28
+ } // namespace derec
package/cpp/Invoker.h ADDED
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ #pragma once
5
+
6
+ #include <functional>
7
+
8
+ namespace derec {
9
+
10
+ /// Abstraction over React Native's `CallInvoker`. Declared as an interface so
11
+ /// the callback bridge can be exercised on the host with no JSI runtime.
12
+ class Invoker {
13
+ public:
14
+ virtual ~Invoker() = default;
15
+
16
+ /// Schedule `work` to run on the JavaScript thread. Must not block.
17
+ virtual void invokeAsync(std::function<void()> work) = 0;
18
+ };
19
+
20
+ } // namespace derec
@@ -0,0 +1,144 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ #include "JsCallbackBridge.h"
5
+
6
+ #include <cassert>
7
+ #include <mutex>
8
+ #include <utility>
9
+
10
+ namespace derec {
11
+
12
+ namespace {
13
+
14
+ /// One in-flight callback. Heap-allocated and shared so a settlement arriving
15
+ /// after a timeout writes into live memory rather than a dead stack frame.
16
+ ///
17
+ /// Both fields are guarded by the owning `InstanceState`'s mutex; the slot
18
+ /// carries no lock of its own so that a waiter and `InstanceState::invalidate`
19
+ /// synchronise on the same condition variable.
20
+ struct CallSlot {
21
+ bool done = false;
22
+ CallResult result{kBackendFailure, {}};
23
+ };
24
+
25
+ /// Written once, from `noteJsThread`, and published by the release store to
26
+ /// `jsThreadKnown` below; readers acquire it. Not a `thread::id` atomic
27
+ /// because `std::thread::id` is not guaranteed lock-free.
28
+ std::thread::id& jsThreadId() {
29
+ static std::thread::id id;
30
+ return id;
31
+ }
32
+
33
+ std::atomic<bool>& jsThreadKnown() {
34
+ static std::atomic<bool> known{false};
35
+ return known;
36
+ }
37
+
38
+ int& storeCallbackDepth() {
39
+ static thread_local int depth = 0;
40
+ return depth;
41
+ }
42
+
43
+ } // namespace
44
+
45
+ void InstanceState::invalidate() {
46
+ {
47
+ std::lock_guard<std::mutex> lock(m);
48
+ invalidated.store(true);
49
+ }
50
+ cv.notify_all();
51
+ }
52
+
53
+ void InstanceState::invalidateRuntime() {
54
+ {
55
+ std::lock_guard<std::mutex> lock(m);
56
+ runtimeInvalidated.store(true);
57
+ invalidated.store(true);
58
+ }
59
+ cv.notify_all();
60
+ }
61
+
62
+ void noteJsThread() {
63
+ jsThreadId() = std::this_thread::get_id();
64
+ jsThreadKnown().store(true, std::memory_order_release);
65
+ }
66
+
67
+ bool isJsThread() {
68
+ if (!jsThreadKnown().load(std::memory_order_acquire)) {
69
+ return false;
70
+ }
71
+ return std::this_thread::get_id() == jsThreadId();
72
+ }
73
+
74
+ bool isInsideStoreCallback() { return storeCallbackDepth() > 0; }
75
+
76
+ StoreCallbackScope::StoreCallbackScope() { ++storeCallbackDepth(); }
77
+
78
+ StoreCallbackScope::~StoreCallbackScope() { --storeCallbackDepth(); }
79
+
80
+ JsCallbackBridge::JsCallbackBridge(Invoker& invoker,
81
+ std::shared_ptr<InstanceState> state,
82
+ std::chrono::milliseconds timeout)
83
+ : invoker_(invoker), state_(std::move(state)), timeout_(timeout) {}
84
+
85
+ CallResult JsCallbackBridge::callSync(
86
+ std::function<void(std::function<void(CallResult)>)> body) {
87
+ // Blocking here on the JavaScript thread would wait for a settlement that
88
+ // only the JavaScript thread can produce. Debug-only: a release build must
89
+ // not pay for the check, and there is no correct recovery anyway.
90
+ assert(!isJsThread() &&
91
+ "JsCallbackBridge::callSync must not run on the JavaScript thread");
92
+
93
+ if (state_->invalidated.load()) {
94
+ return CallResult{kBackendFailure, {}};
95
+ }
96
+
97
+ auto slot = std::make_shared<CallSlot>();
98
+ // The scheduled work owns a reference to the instance state: it must be able
99
+ // to take the mutex even if the bridge and its owner are already gone.
100
+ auto state = state_;
101
+
102
+ invoker_.invokeAsync([slot, state, body = std::move(body)]() mutable {
103
+ auto settle = [slot, state](CallResult result) {
104
+ {
105
+ std::lock_guard<std::mutex> lock(state->m);
106
+ if (slot->done) {
107
+ return; // a timeout or an invalidation already resolved this slot
108
+ }
109
+ slot->result = std::move(result);
110
+ slot->done = true;
111
+ }
112
+ state->cv.notify_all();
113
+ };
114
+
115
+ // A JavaScript store that throws synchronously must not escape into the
116
+ // runtime's dispatch loop; report it as a backend failure instead.
117
+ try {
118
+ // Marks exactly the window in which a protocol call made from this
119
+ // thread would be genuinely re-entrant: the store body's own execution.
120
+ // It ends when `body` returns, even though the worker stays parked
121
+ // until the Promise the store handed back settles.
122
+ StoreCallbackScope scope;
123
+ body(settle);
124
+ } catch (...) {
125
+ settle(CallResult{kBackendFailure, {}});
126
+ }
127
+ });
128
+
129
+ std::unique_lock<std::mutex> lock(state_->m);
130
+ state_->cv.wait_for(lock, timeout_, [this, &slot] {
131
+ return slot->done || state_->invalidated.load();
132
+ });
133
+
134
+ if (!slot->done) {
135
+ // Timed out or invalidated. Mark the slot resolved under the same lock so
136
+ // a late settlement becomes a no-op, and report a backend failure — never
137
+ // `1`, which would tell the protocol a stored share is absent.
138
+ slot->done = true;
139
+ return CallResult{kBackendFailure, {}};
140
+ }
141
+ return std::move(slot->result);
142
+ }
143
+
144
+ } // namespace derec
@@ -0,0 +1,148 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
+
4
+ #pragma once
5
+
6
+ #include <atomic>
7
+ #include <chrono>
8
+ #include <condition_variable>
9
+ #include <cstdint>
10
+ #include <functional>
11
+ #include <memory>
12
+ #include <mutex>
13
+ #include <thread>
14
+ #include <vector>
15
+
16
+ #include "Invoker.h"
17
+
18
+ namespace derec {
19
+
20
+ /// Store-callback return code for a backend failure. `0` means success and
21
+ /// `1` means "not found" (load-family only); every other value is a backend
22
+ /// failure, so any value >= 2 is equivalent here.
23
+ constexpr int32_t kBackendFailure = 2;
24
+
25
+ /// Liveness backstop for a JavaScript store that never settles. Generous
26
+ /// enough for disk and keychain access. Not application-configurable: it is a
27
+ /// binding-level guard, not a protocol parameter.
28
+ constexpr std::chrono::seconds kStoreTimeout{30};
29
+
30
+ struct CallResult {
31
+ int32_t code;
32
+ std::vector<uint8_t> bytes;
33
+ };
34
+
35
+ /// Shared per-protocol-instance liveness flag. Set when the JavaScript
36
+ /// runtime goes away so blocked workers can unwind instead of hanging.
37
+ ///
38
+ /// The instance also owns the synchronisation primitives that every in-flight
39
+ /// callback for that instance waits on. Keeping the mutex and condition
40
+ /// variable here — rather than on the individual call — is what lets
41
+ /// `invalidate` wake a worker that is blocked inside `JsCallbackBridge::
42
+ /// callSync`: a waiter and the invalidator must share one condition variable
43
+ /// for the notification to reach the waiter at all.
44
+ ///
45
+ /// Held by `shared_ptr` because a settlement scheduled onto the JavaScript
46
+ /// thread may arrive after the bridge that started it is gone, and that
47
+ /// settlement still needs the mutex.
48
+ struct InstanceState {
49
+ std::atomic<bool> invalidated{false};
50
+
51
+ /// Set only when the JavaScript runtime itself is going away (the React
52
+ /// Native reload / bridge-invalidate path), never by an ordinary
53
+ /// `free()`/destructor teardown of a single protocol instance.
54
+ ///
55
+ /// The distinction matters because the two cases demand opposite
56
+ /// behaviour from work already scheduled onto the JavaScript thread. After
57
+ /// a plain instance teardown the runtime is still alive, so a settlement
58
+ /// that is already queued must still run — otherwise the Promise a caller
59
+ /// is awaiting never settles. Once the runtime is gone, that same
60
+ /// settlement must not run at all, and the JavaScript values it retains
61
+ /// must not be released either, because both would call back into a
62
+ /// runtime that is being torn down.
63
+ std::atomic<bool> runtimeInvalidated{false};
64
+
65
+ std::mutex m;
66
+ std::condition_variable cv;
67
+
68
+ /// Mark the instance dead and wake every blocked waiter. The flag is set
69
+ /// under `m` so a waiter cannot evaluate its predicate between the store and
70
+ /// the notification and then sleep through the wake-up.
71
+ void invalidate();
72
+
73
+ /// `invalidate`, plus the stronger statement that the JavaScript runtime is
74
+ /// going away. Both flags are set under `m` in one critical section so a
75
+ /// waiter that wakes on `invalidated` can never observe it without also
76
+ /// observing `runtimeInvalidated`.
77
+ void invalidateRuntime();
78
+ };
79
+
80
+ /// Record the calling thread as the JavaScript thread. Called once from
81
+ /// `install`, which by construction runs on it.
82
+ void noteJsThread();
83
+
84
+ /// True when the calling thread is the one `noteJsThread` recorded. False
85
+ /// until `install` has run, so a caller must treat it as advisory.
86
+ bool isJsThread();
87
+
88
+ /// True while this thread is executing the synchronous body of a store
89
+ /// callback — that is, the JavaScript call into an application store, up to
90
+ /// the point that call returns (typically a pending Promise).
91
+ ///
92
+ /// This is deliberately narrower than "the worker is parked in a callback".
93
+ /// A protocol call issued from ordinary JavaScript-thread work that merely
94
+ /// happens to overlap a parked worker is safe: the serial worker runs it
95
+ /// after the current call finishes. Only a call issued from inside the store
96
+ /// body itself can deadlock, because that body's own completion is what the
97
+ /// worker is waiting for.
98
+ bool isInsideStoreCallback();
99
+
100
+ /// Raises `isInsideStoreCallback` for the calling thread over its lifetime.
101
+ /// Nests, so a store body that legitimately drives another one is counted
102
+ /// correctly.
103
+ class StoreCallbackScope {
104
+ public:
105
+ StoreCallbackScope();
106
+ ~StoreCallbackScope();
107
+
108
+ StoreCallbackScope(const StoreCallbackScope&) = delete;
109
+ StoreCallbackScope& operator=(const StoreCallbackScope&) = delete;
110
+ };
111
+
112
+ /// Adapts Rust's synchronous store/transport callbacks to the SDK's
113
+ /// asynchronous JavaScript store interfaces.
114
+ ///
115
+ /// Called from the protocol worker thread, never from the JavaScript thread —
116
+ /// blocking the JavaScript thread here would deadlock, since the settlement
117
+ /// that unblocks the wait is itself scheduled onto that thread. `callSync`
118
+ /// asserts this in debug builds against the thread `noteJsThread` recorded.
119
+ class JsCallbackBridge {
120
+ public:
121
+ JsCallbackBridge(Invoker& invoker,
122
+ std::shared_ptr<InstanceState> state,
123
+ std::chrono::milliseconds timeout =
124
+ std::chrono::duration_cast<std::chrono::milliseconds>(kStoreTimeout));
125
+
126
+ /// Run `body` on the JavaScript thread and block until it settles.
127
+ ///
128
+ /// `body` receives a settle function it must invoke exactly once, from any
129
+ /// thread. Returns a backend failure if the instance is invalidated or the
130
+ /// timeout elapses — never `1`, because a fabricated "not found" would make
131
+ /// a stored share look absent and corrupt a recovery outcome.
132
+ ///
133
+ /// `body` must capture everything it touches by value or by `shared_ptr`,
134
+ /// never by reference to the caller's stack. Giving up on a call does not
135
+ /// cancel it: on timeout or invalidation `callSync` returns while `body` is
136
+ /// still queued on the JavaScript thread, so `body` and its captures must
137
+ /// stay valid after `callSync` has returned and the caller's frame is gone.
138
+ /// The same applies to the settle function, which may be invoked after the
139
+ /// return; a late settlement is discarded, but it still runs.
140
+ CallResult callSync(std::function<void(std::function<void(CallResult)>)> body);
141
+
142
+ private:
143
+ Invoker& invoker_;
144
+ std::shared_ptr<InstanceState> state_;
145
+ std::chrono::milliseconds timeout_;
146
+ };
147
+
148
+ } // namespace derec