@peerkit/transport-libp2p-react-native 0.1.0-alpha.13

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 (48) hide show
  1. package/README.md +155 -0
  2. package/dist/datachannel-early-message-polyfill.d.ts +23 -0
  3. package/dist/datachannel-early-message-polyfill.d.ts.map +1 -0
  4. package/dist/datachannel-early-message-polyfill.js +131 -0
  5. package/dist/datachannel-early-message-polyfill.js.map +1 -0
  6. package/dist/event-target-polyfill.d.ts +40 -0
  7. package/dist/event-target-polyfill.d.ts.map +1 -0
  8. package/dist/event-target-polyfill.js +192 -0
  9. package/dist/event-target-polyfill.js.map +1 -0
  10. package/dist/factories.d.ts +68 -0
  11. package/dist/factories.d.ts.map +1 -0
  12. package/dist/factories.js +73 -0
  13. package/dist/factories.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +3 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/polyfills.d.ts +41 -0
  19. package/dist/polyfills.d.ts.map +1 -0
  20. package/dist/polyfills.js +67 -0
  21. package/dist/polyfills.js.map +1 -0
  22. package/dist/quick-crypto-noise.d.ts +35 -0
  23. package/dist/quick-crypto-noise.d.ts.map +1 -0
  24. package/dist/quick-crypto-noise.js +105 -0
  25. package/dist/quick-crypto-noise.js.map +1 -0
  26. package/dist/text-codec-polyfill.d.ts +25 -0
  27. package/dist/text-codec-polyfill.d.ts.map +1 -0
  28. package/dist/text-codec-polyfill.js +79 -0
  29. package/dist/text-codec-polyfill.js.map +1 -0
  30. package/dist/webrtc-certificate-polyfill.d.ts +37 -0
  31. package/dist/webrtc-certificate-polyfill.d.ts.map +1 -0
  32. package/dist/webrtc-certificate-polyfill.js +53 -0
  33. package/dist/webrtc-certificate-polyfill.js.map +1 -0
  34. package/dist/websocket-buffered-amount-polyfill.d.ts +21 -0
  35. package/dist/websocket-buffered-amount-polyfill.d.ts.map +1 -0
  36. package/dist/websocket-buffered-amount-polyfill.js +41 -0
  37. package/dist/websocket-buffered-amount-polyfill.js.map +1 -0
  38. package/package.json +78 -0
  39. package/src/datachannel-early-message-polyfill.ts +176 -0
  40. package/src/event-target-polyfill.ts +268 -0
  41. package/src/factories.ts +116 -0
  42. package/src/index.ts +10 -0
  43. package/src/polyfills.ts +72 -0
  44. package/src/quick-crypto-noise.ts +130 -0
  45. package/src/rn-modules.d.ts +100 -0
  46. package/src/text-codec-polyfill.ts +118 -0
  47. package/src/webrtc-certificate-polyfill.ts +60 -0
  48. package/src/websocket-buffered-amount-polyfill.ts +45 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Make React Native's `WebSocket` report a numeric `bufferedAmount`.
3
+ *
4
+ * React Native declares `bufferedAmount` on its `WebSocket` but never assigns
5
+ * it, so it reads as `undefined`. `@libp2p/websockets` drives write backpressure
6
+ * off that value, and the `undefined` comparisons leave every write parked
7
+ * waiting for a `drain` that never fires, hanging the connection.
8
+ *
9
+ * React Native hands every frame straight to the native socket with no
10
+ * JS-visible send buffer to measure, so `0` ("fully flushed") is the correct,
11
+ * backpressure-free answer. A prototype accessor returns `0` for reads and
12
+ * swallows React Native's `undefined` field initialization with a no-op setter,
13
+ * so no shadowing own property is left on the instance.
14
+ */
15
+ /**
16
+ * Install the `bufferedAmount` accessor on the global `WebSocket` prototype.
17
+ * Idempotent, and a no-op when the runtime already exposes a real getter (e.g.
18
+ * a browser or Node polyfill) or provides no `WebSocket` at all (e.g. tests).
19
+ */
20
+ export function installWebSocketBufferedAmountPolyfill() {
21
+ const scope = globalThis;
22
+ const prototype = scope.WebSocket?.prototype;
23
+ if (prototype == null) {
24
+ return;
25
+ }
26
+ const existing = Object.getOwnPropertyDescriptor(prototype, "bufferedAmount");
27
+ if (existing?.get != null) {
28
+ return;
29
+ }
30
+ Object.defineProperty(prototype, "bufferedAmount", {
31
+ configurable: true,
32
+ get() {
33
+ return 0;
34
+ },
35
+ set() {
36
+ // Swallow React Native's `undefined` field initialization so it cannot
37
+ // create an own property that shadows this accessor.
38
+ },
39
+ });
40
+ }
41
+ //# sourceMappingURL=websocket-buffered-amount-polyfill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-buffered-amount-polyfill.js","sourceRoot":"","sources":["../src/websocket-buffered-amount-polyfill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH;;;;GAIG;AACH,MAAM,UAAU,sCAAsC;IACpD,MAAM,KAAK,GAAG,UAA2C,CAAC;IAC1D,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC;IAC7C,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;QACtB,OAAO;IACT,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,wBAAwB,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;IAC9E,IAAI,QAAQ,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC;QAC1B,OAAO;IACT,CAAC;IACD,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,gBAAgB,EAAE;QACjD,YAAY,EAAE,IAAI;QAClB,GAAG;YACD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,GAAG;YACD,uEAAuE;YACvE,qDAAqD;QACvD,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@peerkit/transport-libp2p-react-native",
3
+ "version": "0.1.0-alpha.13",
4
+ "description": "React Native libp2p transport for peerkit",
5
+ "keywords": [
6
+ "peerkit",
7
+ "transport",
8
+ "libp2p",
9
+ "react-native",
10
+ "p2p"
11
+ ],
12
+ "author": "Holochain Dev Team <devcore@holochain.org>",
13
+ "license": "CAL-1.0",
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "bugs": {
19
+ "url": "https://github.com/holochain/peerkit/issues"
20
+ },
21
+ "homepage": "https://github.com/holochain/peerkit#readme",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/holochain/peerkit.git"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": [
28
+ "./dist/polyfills.js",
29
+ "./src/polyfills.ts"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ },
36
+ "./polyfills": {
37
+ "types": "./dist/polyfills.d.ts",
38
+ "import": "./dist/polyfills.js"
39
+ },
40
+ "./quick-crypto-noise": {
41
+ "types": "./dist/quick-crypto-noise.d.ts",
42
+ "import": "./dist/quick-crypto-noise.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "test": "vitest --run"
47
+ },
48
+ "dependencies": {
49
+ "@chainsafe/libp2p-noise": "^17.0.0",
50
+ "@chainsafe/libp2p-yamux": "^8.0.1",
51
+ "@libp2p/circuit-relay-v2": "^4.2.3",
52
+ "@libp2p/identify": "^4.1.0",
53
+ "@libp2p/webrtc": "^6.0.23",
54
+ "@peerkit/api": "^0.1.0-alpha.13",
55
+ "@peerkit/transport-libp2p-core": "^0.1.0-alpha.13",
56
+ "libp2p": "^3.2.0"
57
+ },
58
+ "peerDependencies": {
59
+ "react-native": ">=0.81.0",
60
+ "react-native-get-random-values": "^2.0.0",
61
+ "react-native-quick-crypto": "^1.1.3",
62
+ "react-native-webrtc": "^124.0.7"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "react-native": {
66
+ "optional": true
67
+ },
68
+ "react-native-get-random-values": {
69
+ "optional": true
70
+ },
71
+ "react-native-quick-crypto": {
72
+ "optional": true
73
+ },
74
+ "react-native-webrtc": {
75
+ "optional": true
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Buffers the earliest message(s) arriving on a freshly opened inbound
3
+ * (in-band) datachannel and replays them once the JS `RTCDataChannel` object
4
+ * and its listener exist.
5
+ *
6
+ * On `react-native-webrtc`, the channel-open announcement and inbound message
7
+ * events can reach JS out of order during connection setup: a message for a
8
+ * newly opened inbound channel may arrive before the corresponding
9
+ * `RTCDataChannel` object is created, leaving no listener to receive it. This
10
+ * polyfill captures such early messages by `reactTag` and delivers them when
11
+ * the listener is attached.
12
+ *
13
+ * Pre-negotiated channels (`createDataChannel` with `negotiated: true, id: 0`
14
+ * on both ends) are unaffected: their JS object and listener exist before any
15
+ * data arrives.
16
+ */
17
+
18
+ import { Buffer } from "buffer";
19
+ import { NativeEventEmitter, NativeModules } from "react-native";
20
+
21
+ interface NativeReceiveMessageEvent {
22
+ reactTag: string;
23
+ type?: string;
24
+ data: string;
25
+ }
26
+
27
+ interface NativeStateChangeEvent {
28
+ reactTag: string;
29
+ state?: string;
30
+ }
31
+
32
+ // The subset of a `react-native-webrtc` RTCDataChannel instance this relies on.
33
+ interface ReactNativeDataChannel {
34
+ _reactTag: string;
35
+ }
36
+
37
+ interface MessageEventLike {
38
+ data: ArrayBuffer;
39
+ }
40
+
41
+ type MessageHandler = (event: MessageEventLike) => unknown;
42
+
43
+ interface DataChannelProbeHost {
44
+ createDataChannel(label: string): object;
45
+ close(): void;
46
+ }
47
+
48
+ type RTCPeerConnectionConstructor = new () => DataChannelProbeHost;
49
+
50
+ const PROBE_CHANNEL_LABEL = "peerkit-early-message-probe";
51
+ const INSTALLED_FLAG = "__peerkitDataChannelEarlyMessagePatched";
52
+
53
+ /**
54
+ * Decode a base64 datachannel payload to an exactly sized `ArrayBuffer`,
55
+ * matching how `react-native-webrtc` surfaces binary messages
56
+ * (`event.data.byteLength` must equal the message length).
57
+ */
58
+ function base64ToArrayBuffer(base64: string): ArrayBuffer {
59
+ const view = Buffer.from(base64, "base64") as unknown as {
60
+ buffer: ArrayBuffer;
61
+ byteOffset: number;
62
+ byteLength: number;
63
+ };
64
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
65
+ }
66
+
67
+ /**
68
+ * Capture the shared `RTCDataChannel` prototype. The class is not exported, so a
69
+ * throwaway peer connection mints a probe channel and its prototype is read off
70
+ * the instance. Returns `null` if a peer connection cannot be created.
71
+ */
72
+ function captureDataChannelPrototype(
73
+ RTCPeerConnectionCtor: RTCPeerConnectionConstructor,
74
+ ): object | null {
75
+ let probeConnection: DataChannelProbeHost | undefined;
76
+ try {
77
+ probeConnection = new RTCPeerConnectionCtor();
78
+ const channel = probeConnection.createDataChannel(PROBE_CHANNEL_LABEL);
79
+ return Object.getPrototypeOf(channel) as object;
80
+ } catch {
81
+ return null;
82
+ } finally {
83
+ probeConnection?.close();
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Install the early-message recovery. Idempotent, and a no-op when the WebRTC
89
+ * native module or the global `RTCPeerConnection` is absent (`registerGlobals()`
90
+ * has not run, or under tests).
91
+ */
92
+ export function installDataChannelEarlyMessagePolyfill(): void {
93
+ const webRtcModule = NativeModules.WebRTCModule;
94
+ const scope = globalThis as {
95
+ RTCPeerConnection?: RTCPeerConnectionConstructor;
96
+ };
97
+ const RTCPeerConnectionCtor = scope.RTCPeerConnection;
98
+ if (webRtcModule == null || RTCPeerConnectionCtor == null) {
99
+ return;
100
+ }
101
+
102
+ const prototype = captureDataChannelPrototype(RTCPeerConnectionCtor) as
103
+ | (Record<string, unknown> & object)
104
+ | null;
105
+ if (prototype == null || prototype[INSTALLED_FLAG] === true) {
106
+ return;
107
+ }
108
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, "onmessage");
109
+ if (descriptor?.get == null || descriptor.set == null) {
110
+ return;
111
+ }
112
+
113
+ // Early messages buffered per datachannel reactTag, awaiting a consumer.
114
+ const buffered = new Map<string, ArrayBuffer[]>();
115
+ // reactTags whose consumer has attached. Their live messages are delivered by
116
+ // react-native-webrtc's own path, so the duplicate seen here is ignored.
117
+ const consumed = new Set<string>();
118
+
119
+ const emitter = new NativeEventEmitter(webRtcModule);
120
+ emitter.addListener("dataChannelReceiveMessage", (raw) => {
121
+ const event = raw as NativeReceiveMessageEvent;
122
+ // libp2p only sends binary frames; ignore everything else.
123
+ if (event.type !== "binary" || consumed.has(event.reactTag)) {
124
+ return;
125
+ }
126
+ const payload = base64ToArrayBuffer(event.data);
127
+ const existing = buffered.get(event.reactTag);
128
+ if (existing) {
129
+ existing.push(payload);
130
+ } else {
131
+ buffered.set(event.reactTag, [payload]);
132
+ }
133
+ });
134
+ emitter.addListener("dataChannelStateChanged", (raw) => {
135
+ const event = raw as NativeStateChangeEvent;
136
+ if (event.state === "closed") {
137
+ buffered.delete(event.reactTag);
138
+ consumed.delete(event.reactTag);
139
+ }
140
+ });
141
+
142
+ const originalGet = descriptor.get;
143
+ const originalSet = descriptor.set;
144
+ Object.defineProperty(prototype, "onmessage", {
145
+ configurable: true,
146
+ enumerable: descriptor.enumerable ?? false,
147
+ get(this: ReactNativeDataChannel): unknown {
148
+ return originalGet.call(this);
149
+ },
150
+ set(this: ReactNativeDataChannel, handler: MessageHandler | null): void {
151
+ originalSet.call(this, handler);
152
+ if (handler == null) {
153
+ return;
154
+ }
155
+ const reactTag = this._reactTag;
156
+ // From now on react-native-webrtc delivers live messages to this handler;
157
+ // mark the channel so the duplicate copies seen here are dropped.
158
+ consumed.add(reactTag);
159
+ const pending = buffered.get(reactTag);
160
+ if (pending == null) {
161
+ return;
162
+ }
163
+ buffered.delete(reactTag);
164
+ for (const data of pending) {
165
+ handler({ data });
166
+ }
167
+ },
168
+ });
169
+
170
+ Object.defineProperty(prototype, INSTALLED_FLAG, {
171
+ value: true,
172
+ enumerable: false,
173
+ writable: false,
174
+ configurable: true,
175
+ });
176
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * A self-contained WHATWG `Event` / `EventTarget` / `CustomEvent` polyfill for
3
+ * the React Native Hermes engine.
4
+ *
5
+ * React Native's `setUpDOM` polyfills a handful of DOM globals (`DOMRect`,
6
+ * `Node`, `Element`, ...) but **not** the event classes, and Hermes ships none
7
+ * of them. libp2p depends on all three at module-evaluation time:
8
+ *
9
+ * - `@libp2p/interface` declares `class StreamMessageEvent extends Event`.
10
+ * - `main-event`'s `TypedEventEmitter extends EventTarget` and dispatches
11
+ * `new CustomEvent(type, { detail })`.
12
+ *
13
+ * Without these globals the bundle red-screens with
14
+ * `Property 'Event' doesn't exist` before any application code runs.
15
+ *
16
+ * This is a hand-rolled implementation rather than a dependency on
17
+ * `event-target-shim` on purpose: the shim's type definitions expose `Event`
18
+ * only as an interface (not a constructor value), and its dispatcher wraps
19
+ * foreign event objects in a copy — which breaks the `instanceof` checks and
20
+ * subclass fields (e.g. `StreamMessageEvent.data`) that libp2p relies on.
21
+ * Owning both `Event` and `EventTarget` here guarantees the exact event
22
+ * instance is delivered to every listener.
23
+ */
24
+
25
+ interface EventInitLike {
26
+ readonly bubbles?: boolean;
27
+ readonly cancelable?: boolean;
28
+ readonly composed?: boolean;
29
+ }
30
+
31
+ interface CustomEventInitLike<T> extends EventInitLike {
32
+ readonly detail?: T;
33
+ }
34
+
35
+ interface EventListenerOptions {
36
+ readonly capture?: boolean;
37
+ }
38
+
39
+ interface AddEventListenerOptions extends EventListenerOptions {
40
+ readonly once?: boolean;
41
+ readonly passive?: boolean;
42
+ readonly signal?: AbortSignalLike;
43
+ }
44
+
45
+ /**
46
+ * The minimal `AbortSignal` surface honoured by `addEventListener`'s `signal`
47
+ * option. libp2p frequently scopes listeners to an abort signal, so a listener
48
+ * registered with an already-aborted signal must never fire, and one tied to a
49
+ * live signal must be removed when it aborts.
50
+ */
51
+ interface AbortSignalLike {
52
+ readonly aborted: boolean;
53
+ addEventListener(type: "abort", listener: () => void): void;
54
+ }
55
+
56
+ type EventListenerCallback =
57
+ | ((event: PolyfillEvent) => void)
58
+ | { handleEvent(event: PolyfillEvent): void };
59
+
60
+ interface RegisteredListener {
61
+ readonly callback: EventListenerCallback;
62
+ readonly once: boolean;
63
+ }
64
+
65
+ function normalizeAddOptions(
66
+ options?: boolean | AddEventListenerOptions,
67
+ ): AddEventListenerOptions {
68
+ if (typeof options === "boolean") {
69
+ return { capture: options };
70
+ }
71
+ return options ?? {};
72
+ }
73
+
74
+ /** WHATWG `Event`. */
75
+ class PolyfillEvent {
76
+ static readonly NONE = 0;
77
+ static readonly CAPTURING_PHASE = 1;
78
+ static readonly AT_TARGET = 2;
79
+ static readonly BUBBLING_PHASE = 3;
80
+
81
+ readonly type: string;
82
+ readonly bubbles: boolean;
83
+ readonly cancelable: boolean;
84
+ readonly composed: boolean;
85
+ readonly timeStamp = 0;
86
+ readonly isTrusted = false;
87
+ readonly eventPhase = PolyfillEvent.NONE;
88
+
89
+ private targetInternal: PolyfillEventTarget | null = null;
90
+ private currentTargetInternal: PolyfillEventTarget | null = null;
91
+ private defaultPreventedInternal = false;
92
+ private propagationStopped = false;
93
+ private immediatePropagationStopped = false;
94
+
95
+ constructor(type: string, eventInitDict?: EventInitLike) {
96
+ this.type = type;
97
+ this.bubbles = eventInitDict?.bubbles ?? false;
98
+ this.cancelable = eventInitDict?.cancelable ?? false;
99
+ this.composed = eventInitDict?.composed ?? false;
100
+ }
101
+
102
+ get target(): PolyfillEventTarget | null {
103
+ return this.targetInternal;
104
+ }
105
+
106
+ get srcElement(): PolyfillEventTarget | null {
107
+ return this.targetInternal;
108
+ }
109
+
110
+ get currentTarget(): PolyfillEventTarget | null {
111
+ return this.currentTargetInternal;
112
+ }
113
+
114
+ get defaultPrevented(): boolean {
115
+ return this.defaultPreventedInternal;
116
+ }
117
+
118
+ preventDefault(): void {
119
+ if (this.cancelable) {
120
+ this.defaultPreventedInternal = true;
121
+ }
122
+ }
123
+
124
+ stopPropagation(): void {
125
+ this.propagationStopped = true;
126
+ }
127
+
128
+ stopImmediatePropagation(): void {
129
+ this.propagationStopped = true;
130
+ this.immediatePropagationStopped = true;
131
+ }
132
+
133
+ composedPath(): PolyfillEventTarget[] {
134
+ return this.currentTargetInternal == null
135
+ ? []
136
+ : [this.currentTargetInternal];
137
+ }
138
+
139
+ // The following methods are polyfill-internal: `PolyfillEventTarget` uses them
140
+ // to drive dispatch state across the class boundary (TypeScript `private`
141
+ // members are not reachable from another class).
142
+
143
+ /** @internal */
144
+ beginDispatch(target: PolyfillEventTarget): void {
145
+ this.targetInternal = target;
146
+ this.currentTargetInternal = target;
147
+ }
148
+
149
+ /** @internal */
150
+ endDispatch(): void {
151
+ this.currentTargetInternal = null;
152
+ }
153
+
154
+ /** @internal */
155
+ get immediatePropagationWasStopped(): boolean {
156
+ return this.immediatePropagationStopped;
157
+ }
158
+ }
159
+
160
+ /** WHATWG `CustomEvent`. */
161
+ class PolyfillCustomEvent<T = unknown> extends PolyfillEvent {
162
+ readonly detail: T | null;
163
+
164
+ constructor(type: string, eventInitDict?: CustomEventInitLike<T>) {
165
+ super(type, eventInitDict);
166
+ this.detail = eventInitDict?.detail ?? null;
167
+ }
168
+ }
169
+
170
+ /** WHATWG `EventTarget`. */
171
+ class PolyfillEventTarget {
172
+ private readonly listenerMap = new Map<string, RegisteredListener[]>();
173
+
174
+ addEventListener(
175
+ type: string,
176
+ callback: EventListenerCallback | null,
177
+ options?: boolean | AddEventListenerOptions,
178
+ ): void {
179
+ if (callback == null) {
180
+ return;
181
+ }
182
+ const opts = normalizeAddOptions(options);
183
+ const signal = opts.signal;
184
+ if (signal?.aborted === true) {
185
+ return;
186
+ }
187
+ const list = this.listenerMap.get(type) ?? [];
188
+ // Per spec a (callback, capture) pair is registered at most once.
189
+ if (list.some((entry) => entry.callback === callback)) {
190
+ return;
191
+ }
192
+ list.push({ callback, once: opts.once ?? false });
193
+ this.listenerMap.set(type, list);
194
+ if (signal != null) {
195
+ signal.addEventListener("abort", () => {
196
+ this.removeEventListener(type, callback);
197
+ });
198
+ }
199
+ }
200
+
201
+ removeEventListener(
202
+ type: string,
203
+ callback: EventListenerCallback | null,
204
+ _options?: boolean | EventListenerOptions,
205
+ ): void {
206
+ if (callback == null) {
207
+ return;
208
+ }
209
+ const list = this.listenerMap.get(type);
210
+ if (list == null) {
211
+ return;
212
+ }
213
+ const index = list.findIndex((entry) => entry.callback === callback);
214
+ if (index >= 0) {
215
+ list.splice(index, 1);
216
+ }
217
+ }
218
+
219
+ dispatchEvent(event: PolyfillEvent): boolean {
220
+ event.beginDispatch(this);
221
+ // Snapshot the list: a listener may add or remove listeners mid-dispatch.
222
+ const list = this.listenerMap.get(event.type);
223
+ if (list != null) {
224
+ for (const entry of [...list]) {
225
+ if (entry.once) {
226
+ this.removeEventListener(event.type, entry.callback);
227
+ }
228
+ const { callback } = entry;
229
+ if (typeof callback === "function") {
230
+ callback(event);
231
+ } else {
232
+ callback.handleEvent(event);
233
+ }
234
+ if (event.immediatePropagationWasStopped) {
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ event.endDispatch();
240
+ return !event.defaultPrevented;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Install `Event`, `EventTarget`, and `CustomEvent` on the global scope,
246
+ * overwriting any implementation the runtime already provides.
247
+ *
248
+ * The overwrite is deliberate, not defensive. React Native's own global `Event`
249
+ * exposes read-only/non-configurable members (`type`, the phase constants) that
250
+ * libp2p's event subclasses redeclare as class fields, which throws at
251
+ * construction on Hermes. {@link PolyfillEvent} declares them as writable,
252
+ * configurable fields, so the three globals are replaced outright (not `??=`d)
253
+ * to guarantee a single, subclass-safe hierarchy for libp2p's `instanceof`
254
+ * checks and `EventTarget` dispatch.
255
+ *
256
+ * Runs once from the polyfills entry, before any libp2p module evaluates. Safe
257
+ * to call more than once.
258
+ */
259
+ export function installEventTargetPolyfill(): void {
260
+ const eventScope = globalThis as {
261
+ Event?: unknown;
262
+ EventTarget?: unknown;
263
+ CustomEvent?: unknown;
264
+ };
265
+ eventScope.Event = PolyfillEvent;
266
+ eventScope.EventTarget = PolyfillEventTarget;
267
+ eventScope.CustomEvent = PolyfillCustomEvent;
268
+ }
@@ -0,0 +1,116 @@
1
+ import { noise } from "@chainsafe/libp2p-noise";
2
+ import { yamux } from "@chainsafe/libp2p-yamux";
3
+ import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
4
+ import { identify } from "@libp2p/identify";
5
+ import { webRTC, webRTCDirect } from "@libp2p/webrtc";
6
+ import type { ConnectionGater } from "@libp2p/interface";
7
+ import type { NodeAddress, RelayDialAddress } from "@peerkit/api";
8
+ import {
9
+ TransportLibp2p,
10
+ type NodeOptions,
11
+ } from "@peerkit/transport-libp2p-core";
12
+ import { createLibp2p } from "libp2p";
13
+ import { quickCryptoNoise } from "./quick-crypto-noise.js";
14
+
15
+ /**
16
+ * Default libp2p listen addresses for a React Native peerkit node.
17
+ *
18
+ * Mobile peers cannot accept inbound direct TCP/UDP (CGNAT, no listen socket
19
+ * survives backgrounding on iOS / Android), so reachability is always
20
+ * relay-mediated: `/p2p-circuit` advertises a reservation on a connected
21
+ * relay, `/webrtc` advertises that the peer can be upgraded to a direct
22
+ * WebRTC connection through the relay-coordinated SDP exchange.
23
+ */
24
+ export const defaultNodeListenAddrs: NodeAddress[] = [
25
+ "/p2p-circuit",
26
+ "/webrtc",
27
+ ];
28
+
29
+ /**
30
+ * React Native-specific options accepted by {@link createNode}, on top of the
31
+ * platform-agnostic {@link NodeOptions}.
32
+ */
33
+ export interface CreateNodeOptions extends NodeOptions {
34
+ /**
35
+ * Listening addresses.
36
+ *
37
+ * Defaults to {@link defaultNodeListenAddrs}.
38
+ */
39
+ addrs?: string[];
40
+ /**
41
+ * Relay multiaddrs to dial at startup. Required on mobile — without an
42
+ * active relay reservation the peer is unreachable. The transport invokes
43
+ * {@link NodeOptions.connectedToRelayCallback} once the dialable circuit
44
+ * address has been received.
45
+ */
46
+ bootstrapRelays?: RelayDialAddress[];
47
+ /**
48
+ * List of ICE server URLs needed for establishing direct connections.
49
+ */
50
+ iceServerUrls?: string[];
51
+ /**
52
+ * Overrides libp2p's connection gater.
53
+ *
54
+ * On React Native libp2p applies its browser gater, which refuses to dial
55
+ * private/LAN addresses. A production deployment behind a public relay never
56
+ * hits this, but development and demo setups that target a LAN relay or LAN
57
+ * peers must supply a gater that permits those dials
58
+ * (e.g. `{ denyDialMultiaddr: async () => false }`).
59
+ *
60
+ * Left undefined, libp2p keeps its secure-by-default browser gater.
61
+ */
62
+ connectionGater?: ConnectionGater;
63
+ }
64
+
65
+ /**
66
+ * Build a React Native peerkit transport. Configures libp2p with WebRTC +
67
+ * WebRTC Direct + circuit-relay-v2 client + noise + yamux + identify.
68
+ *
69
+ * Noise is wired with `quickCryptoNoise`, the JSI-backed `ICryptoInterface`
70
+ * from `./quick-crypto-noise`, so SHA-256, HKDF, and ChaCha20-Poly1305 are
71
+ * offloaded to `react-native-quick-crypto`. X25519 stays on pure JS because
72
+ * `ICryptoInterface` is synchronous and quick-crypto's X25519 surface is not.
73
+ *
74
+ * Crypto primitives, RNG and the WebRTC globals come from runtime polyfills
75
+ * — the consumer must import `@peerkit/transport-libp2p-react-native/polyfills`
76
+ * once from the app entry before this factory runs.
77
+ *
78
+ * Handles all three peerkit protocols (access, agents, messages). DCUtR is
79
+ * deliberately not registered: mobile cannot listen on TCP/QUIC, so there is
80
+ * no relayed connection that can be upgraded by hole-punching; WebRTC ICE
81
+ * provides the only viable direct-connection path on mobile.
82
+ */
83
+ export async function createNode(options: CreateNodeOptions) {
84
+ // Build array of ICE servers if provided or leave undefined to use
85
+ // libp2p default.
86
+ const iceServers =
87
+ options.iceServerUrls &&
88
+ options.iceServerUrls.map((url) => ({
89
+ urls: url,
90
+ }));
91
+ const libp2pNode = await createLibp2p({
92
+ // Defer listening so TransportLibp2p can register protocol handlers
93
+ // (including /peerkit/access/v1) before any inbound connection arrives.
94
+ start: false,
95
+ transports: [
96
+ webRTC({ rtcConfiguration: { iceServers } }),
97
+ webRTCDirect(),
98
+ circuitRelayTransport(),
99
+ ],
100
+ connectionEncrypters: [noise({ crypto: quickCryptoNoise })],
101
+ streamMuxers: [yamux()],
102
+ services: { identify: identify() },
103
+ connectionGater: options.connectionGater,
104
+ addresses: {
105
+ listen: options.addrs ?? defaultNodeListenAddrs,
106
+ },
107
+ });
108
+ const transport = new TransportLibp2p(libp2pNode, options);
109
+ await libp2pNode.start();
110
+ // Connect to all provided relays. Fire-and-forget: the transport calls
111
+ // connectedToRelayCallback on successful connect to a relay.
112
+ if (options.bootstrapRelays?.length) {
113
+ transport.connectToRelays(options.bootstrapRelays);
114
+ }
115
+ return transport;
116
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./factories.js";
2
+ export {
3
+ TransportLibp2p,
4
+ CURRENT_ACCESS_PROTOCOL,
5
+ CURRENT_AGENTS_PROTOCOL,
6
+ CURRENT_MESSAGE_PROTOCOL,
7
+ type NodeOptions,
8
+ type RelayOptions,
9
+ type TransportOptionsBase,
10
+ } from "@peerkit/transport-libp2p-core";