@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.
- package/README.md +155 -0
- package/dist/datachannel-early-message-polyfill.d.ts +23 -0
- package/dist/datachannel-early-message-polyfill.d.ts.map +1 -0
- package/dist/datachannel-early-message-polyfill.js +131 -0
- package/dist/datachannel-early-message-polyfill.js.map +1 -0
- package/dist/event-target-polyfill.d.ts +40 -0
- package/dist/event-target-polyfill.d.ts.map +1 -0
- package/dist/event-target-polyfill.js +192 -0
- package/dist/event-target-polyfill.js.map +1 -0
- package/dist/factories.d.ts +68 -0
- package/dist/factories.d.ts.map +1 -0
- package/dist/factories.js +73 -0
- package/dist/factories.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/polyfills.d.ts +41 -0
- package/dist/polyfills.d.ts.map +1 -0
- package/dist/polyfills.js +67 -0
- package/dist/polyfills.js.map +1 -0
- package/dist/quick-crypto-noise.d.ts +35 -0
- package/dist/quick-crypto-noise.d.ts.map +1 -0
- package/dist/quick-crypto-noise.js +105 -0
- package/dist/quick-crypto-noise.js.map +1 -0
- package/dist/text-codec-polyfill.d.ts +25 -0
- package/dist/text-codec-polyfill.d.ts.map +1 -0
- package/dist/text-codec-polyfill.js +79 -0
- package/dist/text-codec-polyfill.js.map +1 -0
- package/dist/webrtc-certificate-polyfill.d.ts +37 -0
- package/dist/webrtc-certificate-polyfill.d.ts.map +1 -0
- package/dist/webrtc-certificate-polyfill.js +53 -0
- package/dist/webrtc-certificate-polyfill.js.map +1 -0
- package/dist/websocket-buffered-amount-polyfill.d.ts +21 -0
- package/dist/websocket-buffered-amount-polyfill.d.ts.map +1 -0
- package/dist/websocket-buffered-amount-polyfill.js +41 -0
- package/dist/websocket-buffered-amount-polyfill.js.map +1 -0
- package/package.json +78 -0
- package/src/datachannel-early-message-polyfill.ts +176 -0
- package/src/event-target-polyfill.ts +268 -0
- package/src/factories.ts +116 -0
- package/src/index.ts +10 -0
- package/src/polyfills.ts +72 -0
- package/src/quick-crypto-noise.ts +130 -0
- package/src/rn-modules.d.ts +100 -0
- package/src/text-codec-polyfill.ts +118 -0
- package/src/webrtc-certificate-polyfill.ts +60 -0
- 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
|
+
}
|
package/src/factories.ts
ADDED
|
@@ -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