@derec-alliance/react-native 0.0.1 → 0.0.3

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.
package/README.md CHANGED
@@ -121,7 +121,7 @@ declared in `src/types.ts`:
121
121
  - **`UserSecretStore`** — the most recent user-facing secret snapshot per
122
122
  `secretId`, used to auto-publish to newly paired peers.
123
123
  - **`StateStore`** — in-flight orchestrator bookkeeping (pending
124
- verifications, recoveries, unpairs, sharing rounds, sync checks) so a
124
+ verifications, recoveries, unpairs, sharing rounds, replica discoveries) so a
125
125
  multi-round flow survives an app restart.
126
126
  - **`Transport`** — outbound delivery. `send(endpoint, message)` posts a
127
127
  wire-format envelope to a peer; it is a mailbox, not a request/response
@@ -131,6 +131,32 @@ Every method is `async` and keyed by decimal-string ids — see the next
131
131
  section for why that differs from `DeRecProtocol`'s own methods. Full field-
132
132
  level contracts are documented as TSDoc on each interface in `src/types.ts`.
133
133
 
134
+ ### Filtered listings
135
+
136
+ `listHelpers` and `listReplicas` receive a filter object — `ids`, `status`,
137
+ `role` and `exclude`, where every empty value means "do not restrict on this",
138
+ so an all-empty filter selects everything. `exclude` is applied last,
139
+ overriding `ids`. Ids are decimal strings, like every other `u64` here.
140
+
141
+ **Apply it in your query.** That is the point: a `WHERE` clause or a
142
+ key-condition expression instead of transferring rows the caller will discard.
143
+ That transfer costs bandwidth everywhere, and on a metered backing such as
144
+ DynamoDB, which bills by bytes read, it costs money.
145
+
146
+ **A store that ignores it is slow, not wrong.** The library re-applies the
147
+ filter to every listing before acting on it and drops anything the filter
148
+ excluded.
149
+
150
+ **That is a one-way guarantee, not a validation of your store.** Dropping rows
151
+ enforces an upper bound; it cannot recover a row you omitted. A store that
152
+ returns *fewer* rows than the filter selects is still wrong, in a way nothing
153
+ in the library can detect — the protocol simply fails to act. Applying the
154
+ filter faithfully is still your job.
155
+
156
+ The backstop matters here in particular: TypeScript accepts a function of fewer
157
+ parameters where more are declared, so a store written before the filter
158
+ existed still satisfies this interface and compiles without a diagnostic.
159
+
134
160
  ---
135
161
 
136
162
  ## Threading model
@@ -171,7 +197,7 @@ after whatever is in flight.
171
197
 
172
198
  ## Known differences from `@derec-alliance/nodejs`
173
199
 
174
- **`setCommunicationInfo()` and `setOwnTransport()` return a `Promise`.**
200
+ **`setCommunicationInfo()`, `setOwnTransport()` and `setOwnTransports()` return a `Promise`.**
175
201
  `@derec-alliance/nodejs` declares both `void`. Here they must not run on the
176
202
  JavaScript thread: the underlying FFI setter takes the same handle lock a
177
203
  running flow holds across its store callbacks, and those callbacks block the
@@ -184,8 +210,8 @@ awaiting them additionally lets a failure surface instead of vanishing.
184
210
 
185
211
  **The `DeRecErrorCategory` union differs from nodejs's.** This SDK's union
186
212
  omits `"wasm"`, which a C-ABI binding can never emit, and adds `"ok"`,
187
- `"ffi"` and `"state_store"`, which it can. A test pins the union to
188
- `bindings/test_fixture/enums.json` so it stays in step with the Rust enum.
213
+ `"ffi"` and `"state_store"`, which it can. A test pins the union to the crate's shared enum fixture so it stays in step
214
+ with the Rust enum.
189
215
 
190
216
  **Ids are decimal strings in stores, `bigint | number` on `DeRecProtocol`.**
191
217
  Store interfaces (`ChannelStore`, `ShareStore`, event fields such as
@@ -263,7 +263,7 @@ jsi::Value readTraceId(jsi::Runtime& rt,
263
263
  }
264
264
 
265
265
  /// `create_contact_message(channelId: bigint, contactMode: number,
266
- /// transportProtocol: ArrayBuffer, hasNonce: number, nonce: bigint)
266
+ /// transportProtocols: ArrayBuffer, hasNonce: number, nonce: bigint)
267
267
  /// -> { contact_wire_bytes, secret_key_material }`
268
268
  jsi::Value createContactMessage(jsi::Runtime& rt,
269
269
  const jsi::Value&,
@@ -272,11 +272,11 @@ jsi::Value createContactMessage(jsi::Runtime& rt,
272
272
  requireArgs(rt, "create_contact_message", count, 5);
273
273
  uint64_t channelId = asU64(rt, args[0]);
274
274
  auto contactMode = static_cast<int32_t>(args[1].asNumber());
275
- ByteView transportProtocol = asBytes(rt, args[2]);
275
+ ByteView transportProtocols = asBytes(rt, args[2]);
276
276
  auto hasNonce = static_cast<uint32_t>(args[3].asNumber());
277
277
  uint64_t nonce = asU64(rt, args[4]);
278
278
  CreateContactMessageResult result = create_contact_message(
279
- channelId, contactMode, transportProtocol.ptr, transportProtocol.len,
279
+ channelId, contactMode, transportProtocols.ptr, transportProtocols.len,
280
280
  hasNonce, nonce);
281
281
  if (result.error.code != 0) {
282
282
  throwDeRecError(rt, result.error);
@@ -379,7 +379,7 @@ jsi::Value encodeMessageJson(jsi::Runtime& rt,
379
379
  return toArrayBuffer(rt, bytes.data(), bytes.size());
380
380
  }
381
381
 
382
- /// `produce_pair_request_message(senderKind: number, transportProtocol:
382
+ /// `produce_pair_request_message(senderKind: number, transportProtocols:
383
383
  /// ArrayBuffer, contactMessage: ArrayBuffer, communicationInfo:
384
384
  /// ArrayBuffer|null, parameterRange: ArrayBuffer|null) ->
385
385
  /// { request_wire_bytes, initiator_contact_message_wire_bytes,
@@ -390,12 +390,12 @@ jsi::Value producePairRequestMessage(jsi::Runtime& rt,
390
390
  size_t count) {
391
391
  requireArgs(rt, "produce_pair_request_message", count, 5);
392
392
  auto senderKind = static_cast<int32_t>(args[0].asNumber());
393
- ByteView transportProtocol = asBytes(rt, args[1]);
393
+ ByteView transportProtocols = asBytes(rt, args[1]);
394
394
  ByteView contactMessage = asBytes(rt, args[2]);
395
395
  ByteView communicationInfo = asBytes(rt, args[3]);
396
396
  ByteView parameterRange = asBytes(rt, args[4]);
397
397
  ProducePairRequestMessageResult result = produce_pair_request_message(
398
- senderKind, transportProtocol.ptr, transportProtocol.len,
398
+ senderKind, transportProtocols.ptr, transportProtocols.len,
399
399
  contactMessage.ptr, contactMessage.len, communicationInfo.ptr,
400
400
  communicationInfo.len, parameterRange.ptr, parameterRange.len);
401
401
  if (result.error.code != 0) {
@@ -440,35 +440,41 @@ jsi::Value extractPairRequest(jsi::Runtime& rt,
440
440
 
441
441
  /// `produce_pair_response_message(channelId: bigint, requestProto:
442
442
  /// ArrayBuffer, secretKeyMaterial: ArrayBuffer, communicationInfo:
443
- /// ArrayBuffer|null, parameterRange: ArrayBuffer|null) ->
444
- /// { response_wire_bytes, peer_transport_protocol, shared_key, channel_id }`
443
+ /// ArrayBuffer|null, parameterRange: ArrayBuffer|null,
444
+ /// unsafeConnection: boolean) ->
445
+ /// { response_wire_bytes, peer_transports, shared_key, channel_id }`
446
+ ///
447
+ /// `peer_transports` is a length-delimited sequence of encoded
448
+ /// `TransportProtocol` messages — every endpoint the requester advertised,
449
+ /// in its own order, filtered to those the library will record. It crosses
450
+ /// this seam framed; the TypeScript layer decodes it.
445
451
  jsi::Value producePairResponseMessage(jsi::Runtime& rt,
446
452
  const jsi::Value&,
447
453
  const jsi::Value* args,
448
454
  size_t count) {
449
- requireArgs(rt, "produce_pair_response_message", count, 5);
455
+ requireArgs(rt, "produce_pair_response_message", count, 6);
450
456
  uint64_t channelId = asU64(rt, args[0]);
451
457
  ByteView requestProto = asBytes(rt, args[1]);
452
458
  ByteView secretKeyMaterial = asBytes(rt, args[2]);
453
459
  ByteView communicationInfo = asBytes(rt, args[3]);
454
460
  ByteView parameterRange = asBytes(rt, args[4]);
461
+ uint32_t unsafeConnection = args[5].asBool() ? 1u : 0u;
455
462
  ProducePairResponseMessageResult result = produce_pair_response_message(
456
463
  channelId, requestProto.ptr, requestProto.len, secretKeyMaterial.ptr,
457
464
  secretKeyMaterial.len, communicationInfo.ptr, communicationInfo.len,
458
- parameterRange.ptr, parameterRange.len);
465
+ parameterRange.ptr, parameterRange.len, unsafeConnection);
459
466
  if (result.error.code != 0) {
460
467
  throwDeRecError(rt, result.error);
461
468
  }
462
469
  std::vector<uint8_t> responseBytes = takeBuffer(result.response_wire_bytes);
463
- std::vector<uint8_t> peerTransportProtocol =
464
- takeBuffer(result.peer_transport_protocol);
470
+ std::vector<uint8_t> peerTransports = takeBuffer(result.peer_transports);
465
471
  std::vector<uint8_t> sharedKey = takeBuffer(result.shared_key);
466
472
  auto payload = jsi::Object(rt);
467
473
  payload.setProperty(rt, "response_wire_bytes",
468
474
  toArrayBuffer(rt, responseBytes.data(), responseBytes.size()));
469
475
  payload.setProperty(
470
- rt, "peer_transport_protocol",
471
- toArrayBuffer(rt, peerTransportProtocol.data(), peerTransportProtocol.size()));
476
+ rt, "peer_transports",
477
+ toArrayBuffer(rt, peerTransports.data(), peerTransports.size()));
472
478
  payload.setProperty(rt, "shared_key",
473
479
  toArrayBuffer(rt, sharedKey.data(), sharedKey.size()));
474
480
  payload.setProperty(rt, "channel_id",
@@ -525,17 +531,20 @@ jsi::Value processPairResponseMessage(jsi::Runtime& rt,
525
531
  return jsi::Value(rt, payload);
526
532
  }
527
533
 
528
- /// `produce_pre_pair_request_message(transportProtocol: ArrayBuffer,
534
+ /// `produce_pre_pair_request_message(transportProtocols: ArrayBuffer,
529
535
  /// contactMessage: ArrayBuffer) -> ArrayBuffer`
536
+ ///
537
+ /// `transportProtocols` is a length-delimited sequence of encoded
538
+ /// `TransportProtocol` messages in the scanner's preference order.
530
539
  jsi::Value producePrePairRequestMessage(jsi::Runtime& rt,
531
540
  const jsi::Value&,
532
541
  const jsi::Value* args,
533
542
  size_t count) {
534
543
  requireArgs(rt, "produce_pre_pair_request_message", count, 2);
535
- ByteView transportProtocol = asBytes(rt, args[0]);
544
+ ByteView transportProtocols = asBytes(rt, args[0]);
536
545
  ByteView contactMessage = asBytes(rt, args[1]);
537
546
  ProducePrePairRequestMessageResult result = produce_pre_pair_request_message(
538
- transportProtocol.ptr, transportProtocol.len, contactMessage.ptr,
547
+ transportProtocols.ptr, transportProtocols.len, contactMessage.ptr,
539
548
  contactMessage.len);
540
549
  if (result.error.code != 0) {
541
550
  throwDeRecError(rt, result.error);
@@ -1145,7 +1154,7 @@ void installPrimitives(jsi::Runtime& rt, jsi::Object& host) {
1145
1154
  bind(rt, host, "encode_message_json", 2, encodeMessageJson);
1146
1155
  bind(rt, host, "produce_pair_request_message", 5, producePairRequestMessage);
1147
1156
  bind(rt, host, "extract_pair_request", 2, extractPairRequest);
1148
- bind(rt, host, "produce_pair_response_message", 5, producePairResponseMessage);
1157
+ bind(rt, host, "produce_pair_response_message", 7, producePairResponseMessage);
1149
1158
  bind(rt, host, "extract_pair_response", 2, extractPairResponse);
1150
1159
  bind(rt, host, "process_pair_response_message", 3, processPairResponseMessage);
1151
1160
  bind(rt, host, "produce_pre_pair_request_message", 2,
@@ -320,9 +320,45 @@ jsi::Value ProtocolHost::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
320
320
  DeRecProtocolHandle* handle = handle_;
321
321
 
322
322
  auto body = [handle, uri, protocol]() -> std::vector<uint8_t> {
323
+ // This binding *is* the deprecated single-endpoint path, so it calls the
324
+ // deprecated export on purpose — the same reason the Rust side carries
325
+ // `#[allow(deprecated)]` on its compatibility paths. Both go at 0.0.5.
326
+ #pragma GCC diagnostic push
327
+ #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
323
328
  DeRecError error = derec_protocol_set_own_transport(
324
329
  handle, reinterpret_cast<const uint8_t*>(uri.data()), uri.size(),
325
330
  protocol);
331
+ #pragma GCC diagnostic pop
332
+ if (error.code != DEREC_CODE_OK) {
333
+ throw FfiFailure(error);
334
+ }
335
+ return {};
336
+ };
337
+ auto convert = [](jsi::Runtime&, std::vector<uint8_t>&) {
338
+ return jsi::Value::undefined();
339
+ };
340
+ return runAsync(rt2, std::move(body), std::move(convert));
341
+ });
342
+ }
343
+
344
+ // Takes the endpoint list already JSON-encoded, the same way
345
+ // `setCommunicationInfo` takes its map: the FFI parses JSON on the other
346
+ // side, so encoding in JavaScript keeps this bridge free of a shape it
347
+ // would otherwise have to marshal field by field.
348
+ if (prop == "setOwnTransports") {
349
+ return jsi::Function::createFromHostFunction(
350
+ rt, name, 1,
351
+ [this](jsi::Runtime& rt2, const jsi::Value&, const jsi::Value* args,
352
+ size_t count) -> jsi::Value {
353
+ requireArgs(rt2, "setOwnTransports", count, 1);
354
+ ByteView view = asBytes(rt2, args[0]);
355
+ std::vector<uint8_t> transports(view.ptr, view.ptr + view.len);
356
+ DeRecProtocolHandle* handle = handle_;
357
+
358
+ auto body = [handle,
359
+ transports = std::move(transports)]() -> std::vector<uint8_t> {
360
+ DeRecError error = derec_protocol_set_own_transports(
361
+ handle, transports.data(), transports.size());
326
362
  if (error.code != DEREC_CODE_OK) {
327
363
  throw FfiFailure(error);
328
364
  }
@@ -597,6 +633,7 @@ jsi::Value ProtocolHost::get(jsi::Runtime& rt, const jsi::PropNameID& name) {
597
633
  std::vector<jsi::PropNameID> ProtocolHost::getPropertyNames(jsi::Runtime& rt) {
598
634
  const char* names[] = {
599
635
  "secretId", "setCommunicationInfo", "setOwnTransport",
636
+ "setOwnTransports",
600
637
  "createContact", "start", "process",
601
638
  "tick", "accept", "reject",
602
639
  "getFingerprint", "verifyFingerprint", "removeExpiredChannels",
@@ -4,6 +4,7 @@
4
4
  #include "StoreCallbacks.h"
5
5
 
6
6
  #include <cstring>
7
+ #include <optional>
7
8
  #include <string>
8
9
  #include <vector>
9
10
 
@@ -577,19 +578,31 @@ extern "C" int32_t channelStoreRemove(void* userData, uint64_t secretId, uint64_
577
578
  return result.code;
578
579
  }
579
580
 
580
- /// `ChannelStore.listHelpers(secretId) -> Uint8Array | null`
581
- extern "C" int32_t channelStoreListHelpers(void* userData, uint64_t secretId, uint8_t** outPtr,
582
- size_t* outLen) {
581
+ /// `ChannelStore.listHelpers(secretId, filter) -> Uint8Array | null`
582
+ ///
583
+ /// `filter` arrives as JSON and is handed to JavaScript as a plain object —
584
+ /// see `ChannelFilter` in the SDK's types. The buffer is owned by the core and
585
+ /// valid only for this call, so it is parsed before the promise is awaited.
586
+ extern "C" int32_t channelStoreListHelpers(void* userData, uint64_t secretId, const uint8_t* filter,
587
+ size_t filterLen, uint8_t** outPtr, size_t* outLen) {
583
588
  auto* self = static_cast<StoreBindings*>(userData);
584
589
  // Kept alive past this call so the lambda handed to `callSync` — which the
585
590
  // JavaScript CallInvoker's queue may still be holding after this object's
586
591
  // owner has released its own reference — has somewhere safe to run.
587
592
  auto keepAlive = self->shared_from_this();
588
- CallResult result = keepAlive->bridge().callSync([keepAlive, secretId](std::function<void(CallResult)> settle) {
593
+ std::vector<uint8_t> filterJson(filter, filter + filterLen);
594
+ CallResult result = keepAlive->bridge().callSync([keepAlive, secretId, filterJson](std::function<void(CallResult)> settle) {
589
595
  jsi::Runtime& rt = keepAlive->runtime();
590
596
  auto store = keepAlive->channelStore();
591
597
  auto method = store->getPropertyAsFunction(rt, "listHelpers");
592
- jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId));
598
+ // Ids arrive as decimal strings — the core encodes them that way because
599
+ // `JSON.parse` cannot hold a u64; see `encode_filter` in
600
+ // `interop/ffi/protocol/stores.rs`.
601
+ jsi::Value filterVal = filterJson.empty()
602
+ ? jsi::Value(jsi::Object(rt))
603
+ : jsonParseUtf8(rt, filterJson.data(), filterJson.size());
604
+ jsi::Value promise =
605
+ method.callWithThis(rt, *store, idVal(rt, secretId), std::move(filterVal));
593
606
  keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toCallResult);
594
607
  });
595
608
  if (result.code == 0) {
@@ -603,19 +616,31 @@ extern "C" int32_t channelStoreListHelpers(void* userData, uint64_t secretId, ui
603
616
  return result.code;
604
617
  }
605
618
 
606
- /// `ChannelStore.listReplicas(secretId) -> Uint8Array | null`
607
- extern "C" int32_t channelStoreListReplicas(void* userData, uint64_t secretId, uint8_t** outPtr,
608
- size_t* outLen) {
619
+ /// `ChannelStore.listReplicas(secretId, filter) -> Uint8Array | null`
620
+ ///
621
+ /// `filter` arrives as JSON and is handed to JavaScript as a plain object —
622
+ /// see `ChannelFilter` in the SDK's types. The buffer is owned by the core and
623
+ /// valid only for this call, so it is parsed before the promise is awaited.
624
+ extern "C" int32_t channelStoreListReplicas(void* userData, uint64_t secretId, const uint8_t* filter,
625
+ size_t filterLen, uint8_t** outPtr, size_t* outLen) {
609
626
  auto* self = static_cast<StoreBindings*>(userData);
610
627
  // Kept alive past this call so the lambda handed to `callSync` — which the
611
628
  // JavaScript CallInvoker's queue may still be holding after this object's
612
629
  // owner has released its own reference — has somewhere safe to run.
613
630
  auto keepAlive = self->shared_from_this();
614
- CallResult result = keepAlive->bridge().callSync([keepAlive, secretId](std::function<void(CallResult)> settle) {
631
+ std::vector<uint8_t> filterJson(filter, filter + filterLen);
632
+ CallResult result = keepAlive->bridge().callSync([keepAlive, secretId, filterJson](std::function<void(CallResult)> settle) {
615
633
  jsi::Runtime& rt = keepAlive->runtime();
616
634
  auto store = keepAlive->channelStore();
617
635
  auto method = store->getPropertyAsFunction(rt, "listReplicas");
618
- jsi::Value promise = method.callWithThis(rt, *store, idVal(rt, secretId));
636
+ // Ids arrive as decimal strings — the core encodes them that way because
637
+ // `JSON.parse` cannot hold a u64; see `encode_filter` in
638
+ // `interop/ffi/protocol/stores.rs`.
639
+ jsi::Value filterVal = filterJson.empty()
640
+ ? jsi::Value(jsi::Object(rt))
641
+ : jsonParseUtf8(rt, filterJson.data(), filterJson.size());
642
+ jsi::Value promise =
643
+ method.callWithThis(rt, *store, idVal(rt, secretId), std::move(filterVal));
619
644
  keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toCallResult);
620
645
  });
621
646
  if (result.code == 0) {
@@ -1073,27 +1098,110 @@ extern "C" int32_t stateStoreLoadAll(void* userData, uint64_t secretId, uint32_t
1073
1098
  /// is reported rather than guessed at.
1074
1099
  std::string protocolToString(int32_t protocol) { return protocol == 0 ? "https" : "unknown"; }
1075
1100
 
1076
- /// `Transport.send(endpoint, message) -> void`
1077
- extern "C" int32_t transportSend(void* userData, const uint8_t* uriPtr, size_t uriLen, int32_t protocol,
1101
+ /// Minimal reader for an encoded `TransportProtocol`: field 1 is the URI
1102
+ /// (length-delimited string), field 2 the protocol discriminant (varint).
1103
+ /// Hand-rolled rather than linking a protobuf runtime into the JSI layer for
1104
+ /// two fields; unknown fields are skipped so a newer library stays readable.
1105
+ static std::optional<std::pair<std::string, int32_t>> decodeTransportProtocol(
1106
+ const uint8_t* data, size_t len) {
1107
+ std::string uri;
1108
+ int32_t protocol = 0;
1109
+ size_t i = 0;
1110
+
1111
+ auto readVarint = [&](uint64_t& out) -> bool {
1112
+ out = 0;
1113
+ int shift = 0;
1114
+ while (i < len) {
1115
+ uint8_t b = data[i++];
1116
+ out |= static_cast<uint64_t>(b & 0x7F) << shift;
1117
+ if ((b & 0x80) == 0) return true;
1118
+ shift += 7;
1119
+ if (shift > 63) return false;
1120
+ }
1121
+ return false;
1122
+ };
1123
+
1124
+ while (i < len) {
1125
+ uint64_t tag = 0;
1126
+ if (!readVarint(tag)) return std::nullopt;
1127
+ uint32_t field = static_cast<uint32_t>(tag >> 3);
1128
+ uint32_t wire = static_cast<uint32_t>(tag & 0x7);
1129
+
1130
+ if (field == 1 && wire == 2) {
1131
+ uint64_t size = 0;
1132
+ if (!readVarint(size) || i + size > len) return std::nullopt;
1133
+ uri.assign(reinterpret_cast<const char*>(data + i), size);
1134
+ i += size;
1135
+ } else if (field == 2 && wire == 0) {
1136
+ uint64_t value = 0;
1137
+ if (!readVarint(value)) return std::nullopt;
1138
+ protocol = static_cast<int32_t>(value);
1139
+ } else if (wire == 0) {
1140
+ uint64_t skip = 0;
1141
+ if (!readVarint(skip)) return std::nullopt;
1142
+ } else if (wire == 2) {
1143
+ uint64_t size = 0;
1144
+ if (!readVarint(size) || i + size > len) return std::nullopt;
1145
+ i += size;
1146
+ } else {
1147
+ return std::nullopt;
1148
+ }
1149
+ }
1150
+ return std::make_pair(uri, protocol);
1151
+ }
1152
+
1153
+ /// `Transport.send(endpoints, message) -> void`
1154
+ ///
1155
+ /// `endpoints` is every address the peer advertised, in the peer's order,
1156
+ /// already filtered to those the library will record. The library does not
1157
+ /// rank them: which to dial, and whether to fall back, is the JavaScript
1158
+ /// implementation's choice. Delivery to any one of them is success.
1159
+ extern "C" int32_t transportSend(void* userData, const uint8_t* endpointsPtr, size_t endpointsLen,
1078
1160
  const uint8_t* bytes, size_t len) {
1079
1161
  auto* self = static_cast<StoreBindings*>(userData);
1080
1162
  // Kept alive past this call so the lambda handed to `callSync` — which the
1081
1163
  // JavaScript CallInvoker's queue may still be holding after this object's
1082
1164
  // owner has released its own reference — has somewhere safe to run.
1083
1165
  auto keepAlive = self->shared_from_this();
1084
- std::string uri(reinterpret_cast<const char*>(uriPtr), uriLen);
1166
+
1167
+ // Length-delimited TransportProtocol sequence: each entry preceded by its
1168
+ // protobuf varint byte length.
1169
+ std::vector<std::pair<std::string, int32_t>> endpoints;
1170
+ size_t offset = 0;
1171
+ while (offset < endpointsLen) {
1172
+ uint64_t size = 0;
1173
+ int shift = 0;
1174
+ while (offset < endpointsLen) {
1175
+ uint8_t b = endpointsPtr[offset++];
1176
+ size |= static_cast<uint64_t>(b & 0x7F) << shift;
1177
+ if ((b & 0x80) == 0) break;
1178
+ shift += 7;
1179
+ }
1180
+ if (offset + size > endpointsLen) return -1;
1181
+ auto parsed = decodeTransportProtocol(endpointsPtr + offset, size);
1182
+ if (!parsed) return -1;
1183
+ endpoints.push_back(*parsed);
1184
+ offset += size;
1185
+ }
1186
+
1085
1187
  std::vector<uint8_t> message(bytes, bytes + len);
1086
- CallResult result = keepAlive->bridge().callSync([keepAlive, uri, protocol,
1188
+ CallResult result = keepAlive->bridge().callSync([keepAlive, endpoints,
1087
1189
  message](std::function<void(CallResult)> settle) {
1088
1190
  jsi::Runtime& rt = keepAlive->runtime();
1089
1191
  auto store = keepAlive->transportStore();
1090
1192
  auto method = store->getPropertyAsFunction(rt, "send");
1091
- jsi::Object endpoint(rt);
1092
- endpoint.setProperty(rt, "protocol",
1093
- jsi::Value(rt, jsi::String::createFromUtf8(rt, protocolToString(protocol))));
1094
- endpoint.setProperty(rt, "uri", jsi::Value(rt, jsi::String::createFromUtf8(rt, uri)));
1193
+ jsi::Array endpointsJs(rt, endpoints.size());
1194
+ for (size_t i = 0; i < endpoints.size(); ++i) {
1195
+ jsi::Object endpoint(rt);
1196
+ endpoint.setProperty(rt, "protocol",
1197
+ jsi::Value(rt, jsi::String::createFromUtf8(
1198
+ rt, protocolToString(endpoints[i].second))));
1199
+ endpoint.setProperty(rt, "uri",
1200
+ jsi::Value(rt, jsi::String::createFromUtf8(rt, endpoints[i].first)));
1201
+ endpointsJs.setValueAtIndex(rt, i, endpoint);
1202
+ }
1095
1203
  jsi::Value promise =
1096
- method.callWithThis(rt, *store, jsi::Value(rt, endpoint), toUint8ArrayVal(rt, message));
1204
+ method.callWithThis(rt, *store, jsi::Value(rt, endpointsJs), toUint8ArrayVal(rt, message));
1097
1205
  keepAlive->settleFromPromise(rt, std::move(promise), std::move(settle), toVoidResult);
1098
1206
  });
1099
1207
  return result.code;