@cnhongwei/react-native-mqtt 0.0.0-stage → 0.1.0
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/LICENSE +20 -0
- package/README.md +169 -2
- package/README.zh-CN.md +100 -0
- package/ReactNativeMqtt.podspec +28 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/android/build.gradle +70 -0
- package/android/consumer-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttOptionsParser.kt +176 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttSession.kt +1006 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoLogger.java +44 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoMqttTransport.kt +399 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttModule.kt +619 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttPackage.kt +31 -0
- package/ios/ReactNativeMqtt.h +15 -0
- package/ios/ReactNativeMqtt.mm +1184 -0
- package/ios/ReactNativeMqttCocoaTransport.swift +782 -0
- package/ios/ReactNativeMqttConnAckProbe.swift +87 -0
- package/ios/ReactNativeMqttSession.swift +420 -0
- package/lib/module/NativeReactNativeMqtt.js +5 -0
- package/lib/module/NativeReactNativeMqtt.js.map +1 -0
- package/lib/module/client.js +555 -0
- package/lib/module/client.js.map +1 -0
- package/lib/module/errors.js +20 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/helpers.js +116 -0
- package/lib/module/helpers.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/validation.js +306 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeMqtt.d.ts +154 -0
- package/lib/typescript/src/NativeReactNativeMqtt.d.ts.map +1 -0
- package/lib/typescript/src/client.d.ts +3 -0
- package/lib/typescript/src/client.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +18 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/helpers.d.ts +5 -0
- package/lib/typescript/src/helpers.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +161 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +13 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +188 -4
- package/react-native.config.js +1 -0
- package/src/NativeReactNativeMqtt.ts +173 -0
- package/src/client.ts +859 -0
- package/src/errors.ts +51 -0
- package/src/helpers.ts +151 -0
- package/src/index.ts +30 -0
- package/src/types.ts +184 -0
- package/src/validation.ts +735 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cnhongwei
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,170 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cnhongwei/react-native-mqtt
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native MQTT 3.1.1 and MQTT 5 client for React Native on Android and iOS.
|
|
4
|
+
The package uses the New Architecture TurboModule and does not support Web.
|
|
5
|
+
|
|
6
|
+
MQTT 3.1 (`protocolVersion: 3`) is **not supported** on either platform.
|
|
7
|
+
Protocol levels `4` and `5` mean MQTT 3.1.1 and MQTT 5.0, respectively.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @cnhongwei/react-native-mqtt
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install native dependencies with the normal React Native workflow, then
|
|
16
|
+
rebuild the application. Native changes are not delivered by Metro or OTA.
|
|
17
|
+
|
|
18
|
+
## Basic usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createMqttClient, decodeUtf8 } from '@cnhongwei/react-native-mqtt';
|
|
22
|
+
|
|
23
|
+
const client = createMqttClient({
|
|
24
|
+
host: 'test.mosquitto.org',
|
|
25
|
+
port: 1883,
|
|
26
|
+
protocolVersion: 4,
|
|
27
|
+
clientId: 'example-client',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const listener = client.onMessage((message) => {
|
|
31
|
+
console.log(message.topic, decodeUtf8(message.bytes));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const ack = await client.connect();
|
|
36
|
+
console.log(ack.sessionPresent);
|
|
37
|
+
const subscription = await client.subscribe('demo/status');
|
|
38
|
+
await client.publishText('demo/status', 'hello');
|
|
39
|
+
await client.ready();
|
|
40
|
+
await subscription.unsubscribe();
|
|
41
|
+
} finally {
|
|
42
|
+
listener.remove();
|
|
43
|
+
await client.dispose();
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`createMqttClient` is synchronous and performs no Native I/O. `connect()` is
|
|
48
|
+
lazy and deduplicated for same-turn callers. `ready()` waits for the captured
|
|
49
|
+
subscription intent and does not connect implicitly. Always call `dispose()` in
|
|
50
|
+
a `finally` block; `Symbol.asyncDispose` is installed when the runtime provides
|
|
51
|
+
it.
|
|
52
|
+
|
|
53
|
+
## Options
|
|
54
|
+
|
|
55
|
+
`protocolVersion` is `4` (MQTT 3.1.1, the default) or `5`.
|
|
56
|
+
Passing `3` (MQTT 3.1, not MQTT 3.0) throws `ERR_INVALID_OPTIONS` before native I/O.
|
|
57
|
+
Paho Java can support MQTT 3.1, but this wrapper does not expose it; the locked
|
|
58
|
+
CocoaMQTT 2.4.0 backend implements MQTT 3.1.1/5, not MQTT 3.1.
|
|
59
|
+
|
|
60
|
+
### Testing protocols in the example
|
|
61
|
+
|
|
62
|
+
Both RN and Expo examples offer MQTT 3.1.1 (4) / MQTT 5.0 (5) selection
|
|
63
|
+
(example default: 5; SDK default: 4). Disconnect before switching versions.
|
|
64
|
+
For each connection profile and version, connect, run **Test QoS 0/1/2**, then
|
|
65
|
+
disconnect. The test checks actual payload/QoS round trips and unsubscribes its
|
|
66
|
+
unique test topic. In version 5, **Test v5 properties** also checks Content Type
|
|
67
|
+
and ordered, duplicate-key User Properties received from the broker.
|
|
68
|
+
The UI reports PASS/FAIL, including timeouts, and disables the v5 test in version 4.
|
|
69
|
+
These checks are interactive smoke tests, not full protocol conformance tests;
|
|
70
|
+
see [example testing instructions](example/README.md) for the remaining matrix.
|
|
71
|
+
|
|
72
|
+
| Option | Meaning and default |
|
|
73
|
+
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `host`, `port` | Required host and integer port `1..65535`. |
|
|
75
|
+
| `transport`, `path` | `tcp`, `tls`, `ws`, or `wss`; `path` applies only to WS/WSS and defaults to `/mqtt`. |
|
|
76
|
+
| `clientId` | Optional MQTT UTF-8 string; an ID is generated when omitted. |
|
|
77
|
+
| `username`, `password` | Optional UTF-8 credentials; password requires username. |
|
|
78
|
+
| `keepAliveSeconds` | `0..65535`, default `60`; `0` disables protocol heartbeat. |
|
|
79
|
+
| `connectTimeoutMs` | Positive safe integer, default `30000`. |
|
|
80
|
+
| `operationTimeoutMs` | Positive safe integer, default `30000`. |
|
|
81
|
+
| `disconnectTimeoutMs` | Positive safe integer, default `5000`. |
|
|
82
|
+
| `reconnect` | `enabled: true`, `initialDelayMs: 1000`, `maxDelayMs: 30000`, `jitterRatio: 0.2`. |
|
|
83
|
+
| `tls` | Optional DER CA / PKCS#12 Base64 and explicit `insecure`; only TLS/WSS. Default verifies system trust and hostname. |
|
|
84
|
+
| `will` | MQTT will `{ topic, payloadBase64, qos?, retained? }`. |
|
|
85
|
+
| `cleanSession` | MQTT 3.1.1 only. |
|
|
86
|
+
| `cleanStart`, `sessionExpiryIntervalSeconds`, `receiveMaximum`, `userProperties` | MQTT 5 only; user properties preserve order and duplicate keys. |
|
|
87
|
+
| `delivery` | Queue defaults `1024` messages / `8 MiB` / `1 MiB` per message; batches default `64` messages / `256 KiB`, ACK watchdog `15000 ms`, overflow `disconnect`. |
|
|
88
|
+
|
|
89
|
+
Subscribe filters support `+` and a terminal `#`, preserve empty levels, and
|
|
90
|
+
do not support `$share/`. Publish topics are topic names and reject wildcards.
|
|
91
|
+
Use `publishBytes` for arbitrary bytes and `publishBase64` for canonical RFC
|
|
92
|
+
4648 Base64; neither converts binary payloads to text.
|
|
93
|
+
|
|
94
|
+
## Public surface
|
|
95
|
+
|
|
96
|
+
- `connect`, `disconnect`, `dispose`, `ready`, and `getSnapshot` manage one
|
|
97
|
+
in-memory session.
|
|
98
|
+
- `subscribe(filter, options)` returns a reference-counted handle. Equal
|
|
99
|
+
options share one broker subscription; conflicting options are rejected.
|
|
100
|
+
- `publishText`, `publishBytes`, and `publishBase64` publish QoS `0..2`.
|
|
101
|
+
- `onStateChange`, `onMessage`, and `onDiagnostic` return removable listeners.
|
|
102
|
+
- `encodeUtf8`/`decodeUtf8` and `encodeBase64`/`decodeBase64` are strict helpers.
|
|
103
|
+
|
|
104
|
+
`Message.bytes` and `ConnAck.userProperties` are the lossless wire-facing
|
|
105
|
+
forms. `onMessage` is delivery acknowledgement to the bridge, not MQTT or
|
|
106
|
+
business acknowledgement.
|
|
107
|
+
|
|
108
|
+
### Android MQTT backend and QoS 2
|
|
109
|
+
|
|
110
|
+
Android uses the direct Eclipse Paho MQTT Java 1.2.5 clients for MQTT 3.1.1
|
|
111
|
+
(`org.eclipse.paho.client.mqttv3`) and MQTT 5
|
|
112
|
+
(`org.eclipse.paho:org.eclipse.paho.mqttv5.client`); Paho Android Service is
|
|
113
|
+
not used. MQTT 5 QoS 2 publish completion must reach `PUBCOMP`. The dedicated
|
|
114
|
+
Android `PUBREC` → `PUBCOMP` success, timeout, disconnect, duplicate, and
|
|
115
|
+
negative-path behavior has been verified with the repository-controlled Paho
|
|
116
|
+
tests. This does not imply that the incomplete public RN/Expo matrix is
|
|
117
|
+
complete.
|
|
118
|
+
|
|
119
|
+
## Errors and platform limits
|
|
120
|
+
|
|
121
|
+
Operations reject with `MqttError` containing `code` and `message`. The fixed
|
|
122
|
+
codes are `ERR_INVALID_OPTIONS`, `ERR_INVALID_PAYLOAD`,
|
|
123
|
+
`ERR_NATIVE_UNAVAILABLE`, `ERR_CONTRACT_MISMATCH`, `ERR_NOT_CONNECTED`,
|
|
124
|
+
`ERR_DISPOSED`, `ERR_OPERATION_IN_PROGRESS`, `ERR_OPERATION_SUPERSEDED`,
|
|
125
|
+
`ERR_CONNECT_TIMEOUT`, `ERR_OPERATION_TIMEOUT`, `ERR_CONNECTION_REFUSED`,
|
|
126
|
+
`ERR_CONNECTION_LOST`, `ERR_SUBSCRIPTION_CONFLICT`, `ERR_SUBSCRIBE_FAILED`,
|
|
127
|
+
`ERR_UNSUBSCRIBE_FAILED`, `ERR_PUBLISH_FAILED`, `ERR_TLS`,
|
|
128
|
+
`ERR_CLEANUP_FAILED`, and `ERR_UNKNOWN`. `outcome` distinguishes
|
|
129
|
+
`not-sent`, `rejected`, and `unknown` where known.
|
|
130
|
+
|
|
131
|
+
This release is foreground, in-memory operation. It does not promise a
|
|
132
|
+
persistent offline outbox, guaranteed background execution, Doze/process-death
|
|
133
|
+
keep-alive, or Expo Go support. Expo requires a development or release build;
|
|
134
|
+
native changes require a new build.
|
|
135
|
+
|
|
136
|
+
For Android `ws://` connections, the consuming application must explicitly
|
|
137
|
+
allow cleartext traffic for the build variant that uses them. The example
|
|
138
|
+
application does this for Release because its public smoke matrix includes
|
|
139
|
+
plaintext WebSocket listeners; `wss://` does not require this opt-in.
|
|
140
|
+
|
|
141
|
+
## Development verification
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
yarn typecheck
|
|
145
|
+
yarn lint
|
|
146
|
+
yarn test
|
|
147
|
+
yarn codegen
|
|
148
|
+
yarn build
|
|
149
|
+
yarn test:package
|
|
150
|
+
yarn verify
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Android and iOS native commands require their respective SDK/toolchain; see
|
|
154
|
+
`example/android` and `example/ios`. The public Mosquitto matrix is configured
|
|
155
|
+
under `test/mosquitto`; public service availability is not a substitute for
|
|
156
|
+
fault-injection or performance evidence.
|
|
157
|
+
|
|
158
|
+
## Contributing
|
|
159
|
+
|
|
160
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
161
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
162
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @cnhongwei/react-native-mqtt
|
|
2
|
+
|
|
3
|
+
面向 React Native Android/iOS 的 MQTT 3.1.1 与 MQTT 5 原生客户端,使用新架构 TurboModule,不支持 Web。
|
|
4
|
+
|
|
5
|
+
Android/iOS 均**不支持 MQTT 3.1(`protocolVersion: 3`)**。
|
|
6
|
+
协议级别 `4` 对应 MQTT 3.1.1,`5` 对应 MQTT 5.0;`3` 不是 MQTT 3.0。
|
|
7
|
+
|
|
8
|
+
## 安装
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @cnhongwei/react-native-mqtt
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
按普通 React Native 流程安装原生依赖并重新构建应用;Metro 或 OTA 不会交付原生变更。
|
|
15
|
+
|
|
16
|
+
## 基本用法
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createMqttClient, decodeUtf8 } from '@cnhongwei/react-native-mqtt';
|
|
20
|
+
|
|
21
|
+
const client = createMqttClient({
|
|
22
|
+
host: 'test.mosquitto.org',
|
|
23
|
+
port: 1883,
|
|
24
|
+
protocolVersion: 4,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const listener = client.onMessage((message) => {
|
|
28
|
+
console.log(message.topic, decodeUtf8(message.bytes));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await client.connect();
|
|
33
|
+
const subscription = await client.subscribe('demo/status');
|
|
34
|
+
await client.publishText('demo/status', 'hello');
|
|
35
|
+
await client.ready();
|
|
36
|
+
await subscription.unsubscribe();
|
|
37
|
+
} finally {
|
|
38
|
+
listener.remove();
|
|
39
|
+
await client.dispose();
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`createMqttClient` 同步返回且不执行 Native I/O;`connect()` 惰性执行,同一轮调用会合并。`ready()` 不会隐式连接,只等待调用时捕获的订阅意图。请在 `finally` 中调用 `dispose()`。
|
|
44
|
+
|
|
45
|
+
## 选项
|
|
46
|
+
|
|
47
|
+
`protocolVersion` 为 `4`(MQTT 3.1.1,默认)或 `5`。
|
|
48
|
+
传入 `3` 会在 Native I/O 前抛出 `ERR_INVALID_OPTIONS`。Paho Java 本身支持
|
|
49
|
+
MQTT 3.1,但本库未开放;锁定的 CocoaMQTT 2.4.0 实现仅支持 MQTT 3.1.1/5。
|
|
50
|
+
|
|
51
|
+
RN/Expo 示例支持选择 MQTT 3.1.1(4)或 MQTT 5.0(5),示例默认 5,SDK 默认 4。
|
|
52
|
+
断开后可切换版本;连接后运行 `Test QoS 0/1/2` 检查实际消息回环,选择 5 时还可运行
|
|
53
|
+
`Test v5 properties` 检查 Content Type 和重复 key 的有序 User Properties。
|
|
54
|
+
结果显示 PASS/FAIL(含超时),测试会取消其独立 topic 的订阅。这是交互冒烟测试,
|
|
55
|
+
不代表完整协议一致性验收;完整操作与剩余测试范围见 [示例说明](example/README.md)。
|
|
56
|
+
|
|
57
|
+
- `host`、`port`:必填,端口为 `1..65535` 整数。
|
|
58
|
+
- `transport`:`tcp`、`tls`、`ws`、`wss`;WS/WSS 的 `path` 默认 `/mqtt`。
|
|
59
|
+
- `clientId`、`username`、`password`:可选 MQTT UTF-8 字符串;password 必须配合 username。
|
|
60
|
+
- `keepAliveSeconds`:`0..65535`,默认 `60`。
|
|
61
|
+
- `connectTimeoutMs` / `operationTimeoutMs` / `disconnectTimeoutMs`:默认分别为 `30000`、`30000`、`5000`。
|
|
62
|
+
- `reconnect`:默认启用,初始延迟 `1000`、最大延迟 `30000`、抖动比例 `0.2`。
|
|
63
|
+
- `tls`:可选 CA、PKCS#12 Base64 与显式 `insecure`;仅 TLS/WSS 可用,默认校验证书链和主机名。
|
|
64
|
+
- MQTT 3.1.1 使用 `cleanSession`;MQTT 5 使用 `cleanStart`、会话过期、`receiveMaximum` 和有序(允许重复 key)`userProperties`。
|
|
65
|
+
- `delivery` 默认队列为 `1024` 条/`8 MiB`、单条 `1 MiB`;batch 默认 `64` 条/`256 KiB`,ACK watchdog `15000 ms`,溢出策略 `disconnect`。
|
|
66
|
+
|
|
67
|
+
订阅 filter 支持 `+` 和末尾独占层级 `#`,保留空层级,不支持 `$share/`。发布 topic name 不允许通配符。二进制请使用 `publishBytes` 或严格 RFC 4648 的 `publishBase64`。
|
|
68
|
+
|
|
69
|
+
## API、错误与限制
|
|
70
|
+
|
|
71
|
+
客户端提供 `connect`、`disconnect`、`dispose`、`ready`、`getSnapshot`、`subscribe`、`publishText`、`publishBytes`、`publishBase64`,以及 `onStateChange`、`onMessage`、`onDiagnostic`。订阅 handle 是引用计数的;相同选项共享 Native 订阅,冲突会拒绝。
|
|
72
|
+
|
|
73
|
+
### Android MQTT 后端与 QoS 2
|
|
74
|
+
|
|
75
|
+
Android 使用直连的 Eclipse Paho MQTT Java 1.2.5:MQTT 3.1.1 使用
|
|
76
|
+
`org.eclipse.paho.client.mqttv3`,MQTT 5 使用
|
|
77
|
+
`org.eclipse.paho:org.eclipse.paho.mqttv5.client`,不使用 Paho Android
|
|
78
|
+
Service。MQTT 5 QoS 2 发布必须完成到 `PUBCOMP`;Android 对
|
|
79
|
+
`PUBREC` → `PUBCOMP` 的成功、超时、断开、重复包和负路径已通过仓库受控
|
|
80
|
+
Paho 测试验证。但这不代表 RN/Expo 公共矩阵已经全部完成。
|
|
81
|
+
|
|
82
|
+
错误通过 `MqttError` 返回,固定 code 包括:`ERR_INVALID_OPTIONS`、`ERR_INVALID_PAYLOAD`、`ERR_NATIVE_UNAVAILABLE`、`ERR_CONTRACT_MISMATCH`、`ERR_NOT_CONNECTED`、`ERR_DISPOSED`、`ERR_OPERATION_IN_PROGRESS`、`ERR_OPERATION_SUPERSEDED`、`ERR_CONNECT_TIMEOUT`、`ERR_OPERATION_TIMEOUT`、`ERR_CONNECTION_REFUSED`、`ERR_CONNECTION_LOST`、`ERR_SUBSCRIPTION_CONFLICT`、`ERR_SUBSCRIBE_FAILED`、`ERR_UNSUBSCRIBE_FAILED`、`ERR_PUBLISH_FAILED`、`ERR_TLS`、`ERR_CLEANUP_FAILED`、`ERR_UNKNOWN`。
|
|
83
|
+
|
|
84
|
+
当前为前台、进程内存操作,不保证磁盘离线 outbox、后台持续运行、Doze/进程终止后的保活,也不支持 Expo Go。Expo 需要 development/release build。
|
|
85
|
+
|
|
86
|
+
Android 使用 `ws://` 时,宿主应用必须为实际使用该传输的构建变体显式允许明文流量。示例应用因公共 smoke 矩阵包含明文 WebSocket 监听器,已为 Release 打开该配置;`wss://` 不需要此明文许可。
|
|
87
|
+
|
|
88
|
+
## 开发验证
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
yarn typecheck
|
|
92
|
+
yarn lint
|
|
93
|
+
yarn test
|
|
94
|
+
yarn codegen
|
|
95
|
+
yarn build
|
|
96
|
+
yarn test:package
|
|
97
|
+
yarn verify
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
不编写旧 API 迁移指南;本项目没有旧 API 兼容目标。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "ReactNativeMqtt"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.swift_version = "5.7"
|
|
15
|
+
s.source = { :git => "https://github.com/cnhongwei/react-native-mqtt.git", :tag => "#{s.version}" }
|
|
16
|
+
|
|
17
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
18
|
+
s.exclude_files = "ios/Tests/**/*"
|
|
19
|
+
s.private_header_files = "ios/**/*.h"
|
|
20
|
+
|
|
21
|
+
install_modules_dependencies(s)
|
|
22
|
+
s.dependency "CocoaMQTT/WebSockets", "2.4.0"
|
|
23
|
+
|
|
24
|
+
s.test_spec "NativeSessionScenarios" do |test_spec|
|
|
25
|
+
test_spec.source_files = "ios/Tests/**/*.swift"
|
|
26
|
+
test_spec.requires_app_host = false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
This project owns only the source code in this repository, licensed under MIT.
|
|
4
|
+
|
|
5
|
+
The native MQTT implementation dependencies are kept as external dependencies:
|
|
6
|
+
|
|
7
|
+
- CocoaMQTT 2.4.0 — MIT.
|
|
8
|
+
- MqttCocoaAsyncSocket 1.0.8 — dual public-domain/MIT-style terms in its
|
|
9
|
+
`LICENSE.txt`.
|
|
10
|
+
- Starscream 4.0.8 — Apache-2.0.
|
|
11
|
+
- Eclipse Paho MQTT Java 1.2.5 (`org.eclipse.paho.client.mqttv3` and
|
|
12
|
+
`org.eclipse.paho.mqttv5.client`) — EPL-2.0 / EDL-1.0; each artifact's
|
|
13
|
+
upstream license terms apply.
|
|
14
|
+
- Bouncy Castle `bcprov-jdk18on` 1.77 — MIT.
|
|
15
|
+
|
|
16
|
+
This file records dependency attribution only; it does not claim that either
|
|
17
|
+
transport has passed the capability gate.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.ReactNativeMqtt = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
ext.getExtOrDefault = { prop ->
|
|
9
|
+
if (rootProject.ext.has(prop)) {
|
|
10
|
+
return rootProject.ext.get(prop)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return ReactNativeMqtt[prop]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
repositories {
|
|
17
|
+
google()
|
|
18
|
+
mavenCentral()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
dependencies {
|
|
22
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
23
|
+
// noinspection DifferentKotlinGradleVersion
|
|
24
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
apply plugin: "com.android.library"
|
|
30
|
+
|
|
31
|
+
if (project.extensions.findByName("kotlin") == null) {
|
|
32
|
+
apply plugin: "kotlin-android"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
apply plugin: "com.facebook.react"
|
|
37
|
+
|
|
38
|
+
android {
|
|
39
|
+
namespace "com.cnhongwei.reactnativemqtt"
|
|
40
|
+
|
|
41
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
42
|
+
|
|
43
|
+
packagingOptions {
|
|
44
|
+
resources.excludes += [
|
|
45
|
+
"META-INF/INDEX.LIST",
|
|
46
|
+
"bundle.properties",
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
defaultConfig {
|
|
51
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
52
|
+
consumerProguardFiles "consumer-rules.pro"
|
|
53
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
compileOptions {
|
|
57
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
58
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
dependencies {
|
|
63
|
+
implementation "com.facebook.react:react-android"
|
|
64
|
+
implementation "org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5"
|
|
65
|
+
implementation "org.eclipse.paho:org.eclipse.paho.mqttv5.client:1.2.5"
|
|
66
|
+
implementation "org.bouncycastle:bcprov-jdk18on:1.77"
|
|
67
|
+
testImplementation "junit:junit:4.13.2"
|
|
68
|
+
androidTestImplementation "androidx.test:runner:1.6.2"
|
|
69
|
+
androidTestImplementation "androidx.test.ext:junit:1.2.1"
|
|
70
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Paho selects its logger implementation by class name.
|
|
2
|
+
-keep class com.cnhongwei.reactnativemqtt.PahoLogger { public <init>(); *; }
|
|
3
|
+
-keep class org.eclipse.paho.client.mqttv3.logging.** { *; }
|
|
4
|
+
-keep class org.eclipse.paho.mqttv5.client.logging.** { *; }
|
|
5
|
+
|
|
6
|
+
# TLS providers and the explicitly selected insecure TrustManagerFactory are
|
|
7
|
+
# instantiated by name/provider registration.
|
|
8
|
+
-keep class com.cnhongwei.reactnativemqtt.InsecureTrustManagerFactorySpi { public <init>(); }
|
|
9
|
+
-keep class org.bouncycastle.jce.provider.BouncyCastleProvider { *; }
|
|
10
|
+
-keep class org.bouncycastle.jcajce.provider.** { *; }
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
package com.cnhongwei.reactnativemqtt
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap
|
|
4
|
+
import com.facebook.react.bridge.ReadableArray
|
|
5
|
+
|
|
6
|
+
internal fun isValidMqttUtf8(value: String): Boolean {
|
|
7
|
+
var index = 0
|
|
8
|
+
while (index < value.length) {
|
|
9
|
+
val character = value[index]
|
|
10
|
+
if (character == '\u0000') return false
|
|
11
|
+
if (character.isHighSurrogate()) {
|
|
12
|
+
if (index + 1 >= value.length || !value[index + 1].isLowSurrogate()) return false
|
|
13
|
+
index += 2
|
|
14
|
+
} else {
|
|
15
|
+
if (character.isLowSurrogate()) return false
|
|
16
|
+
index += 1
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
internal data class ParsedMqttOptions(
|
|
23
|
+
val host: String,
|
|
24
|
+
val port: Int,
|
|
25
|
+
val protocolVersion: Int,
|
|
26
|
+
val transport: String,
|
|
27
|
+
val clientId: String = "rn-${java.util.UUID.randomUUID().toString().replace("-", "").take(21)}",
|
|
28
|
+
val username: String? = null,
|
|
29
|
+
val password: String? = null,
|
|
30
|
+
val willTopic: String? = null,
|
|
31
|
+
val willPayloadBase64: String? = null,
|
|
32
|
+
val willQos: Int = 0,
|
|
33
|
+
val willRetained: Boolean = false,
|
|
34
|
+
val keepAliveSeconds: Int = 60,
|
|
35
|
+
val cleanSession: Boolean = true,
|
|
36
|
+
val cleanStart: Boolean = true,
|
|
37
|
+
val sessionExpiryIntervalSeconds: Long = 0,
|
|
38
|
+
val userProperties: List<Pair<String, String>> = emptyList(),
|
|
39
|
+
val path: String = if (transport == "ws" || transport == "wss") "/mqtt" else "",
|
|
40
|
+
val connectTimeoutMs: Long = 30_000,
|
|
41
|
+
val operationTimeoutMs: Long = 30_000,
|
|
42
|
+
val disconnectTimeoutMs: Long = 5_000,
|
|
43
|
+
val reconnectEnabled: Boolean = true,
|
|
44
|
+
val reconnectInitialDelayMs: Long = 1_000,
|
|
45
|
+
val reconnectMaxDelayMs: Long = 30_000,
|
|
46
|
+
val reconnectJitterRatio: Double = 0.2,
|
|
47
|
+
val tlsCaBase64: String? = null,
|
|
48
|
+
val tlsP12Base64: String? = null,
|
|
49
|
+
val tlsP12Password: String? = null,
|
|
50
|
+
val tlsInsecure: Boolean = false,
|
|
51
|
+
val tlsAllowInvalidHostnameForTesting: Boolean = false,
|
|
52
|
+
val receiveMaximum: Int = 65535,
|
|
53
|
+
val maxQueuedMessages: Int = 1024,
|
|
54
|
+
val maxQueuedBytes: Long = 8L * 1024 * 1024,
|
|
55
|
+
val maxMessageBytes: Int = 1024 * 1024,
|
|
56
|
+
val batchMaxMessages: Int = 64,
|
|
57
|
+
val batchMaxBytes: Int = 256 * 1024,
|
|
58
|
+
val batchAckTimeoutMs: Long = 15_000,
|
|
59
|
+
val overflowPolicy: String = "disconnect",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
internal object MqttOptionsParser {
|
|
63
|
+
fun parse(options: ReadableMap): Result<ParsedMqttOptions> = runCatching {
|
|
64
|
+
val allowed = setOf(
|
|
65
|
+
"host", "port", "protocolVersion", "transport", "path", "clientId", "username", "password",
|
|
66
|
+
"keepAliveSeconds", "connectTimeoutMs", "operationTimeoutMs", "disconnectTimeoutMs", "reconnectEnabled",
|
|
67
|
+
"reconnectInitialDelayMs", "reconnectMaxDelayMs", "reconnectJitterRatio", "tlsCaBase64", "tlsP12Base64",
|
|
68
|
+
"tlsP12Password", "tlsInsecure", "tlsAllowInvalidHostnameForTesting", "willTopic", "willPayloadBase64", "willQos", "willRetained",
|
|
69
|
+
"cleanSession", "cleanStart", "sessionExpiryIntervalSeconds", "receiveMaximum", "userProperties",
|
|
70
|
+
"maxQueuedMessages", "maxQueuedBytes", "maxMessageBytes", "batchMaxMessages", "batchMaxBytes",
|
|
71
|
+
"batchAckTimeoutMs", "overflowPolicy"
|
|
72
|
+
)
|
|
73
|
+
val iterator = options.keySetIterator()
|
|
74
|
+
while (iterator.hasNextKey()) {
|
|
75
|
+
val key = iterator.nextKey()
|
|
76
|
+
require(key in allowed) { "unsupported option: $key" }
|
|
77
|
+
}
|
|
78
|
+
val host = options.getString("host")
|
|
79
|
+
?: throw IllegalArgumentException("host must be a non-empty string")
|
|
80
|
+
require(host.isNotEmpty()) { "host must be a non-empty string" }
|
|
81
|
+
val port = options.getDouble("port").toInt().also {
|
|
82
|
+
require(options.getDouble("port") == it.toDouble() && it in 1..65535) {
|
|
83
|
+
"port must be an integer in 1..65535"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
val protocolVersion = if (options.hasKey("protocolVersion")) {
|
|
87
|
+
options.getDouble("protocolVersion").toInt().also {
|
|
88
|
+
require(options.getDouble("protocolVersion") == it.toDouble() && it in 4..5) {
|
|
89
|
+
"protocolVersion must be 4 or 5"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else 4
|
|
93
|
+
val transport = if (options.hasKey("transport")) {
|
|
94
|
+
options.getString("transport") ?: throw IllegalArgumentException("transport must be a string")
|
|
95
|
+
} else "tcp"
|
|
96
|
+
require(transport in setOf("tcp", "tls", "ws", "wss")) { "transport is invalid" }
|
|
97
|
+
val hasPath = options.hasKey("path") && !options.isNull("path")
|
|
98
|
+
require(!hasPath || transport == "ws" || transport == "wss") { "path is only valid for ws/wss" }
|
|
99
|
+
val path = if (hasPath) options.getString("path") else if (transport == "ws" || transport == "wss") "/mqtt" else ""
|
|
100
|
+
require(path != null && (transport == "tcp" || transport == "tls" || (path.isNotEmpty() && path.startsWith("/")))) { "path must start with /" }
|
|
101
|
+
val clientId = if (options.hasKey("clientId") && !options.isNull("clientId")) {
|
|
102
|
+
options.getString("clientId") ?: throw IllegalArgumentException("clientId must be a string")
|
|
103
|
+
} else "rn-${java.util.UUID.randomUUID().toString().replace("-", "").take(21)}"
|
|
104
|
+
require(isValidMqttUtf8(clientId) && (clientId.isNotEmpty() || (protocolVersion == 5 && readBoolean(options, "cleanStart", true))) && clientId.toByteArray(Charsets.UTF_8).size <= 65535) { "clientId is invalid" }
|
|
105
|
+
val username = if (options.hasKey("username") && !options.isNull("username")) options.getString("username") else null
|
|
106
|
+
val password = if (options.hasKey("password") && !options.isNull("password")) options.getString("password") else null
|
|
107
|
+
require(username == null || (isValidMqttUtf8(username) && username.toByteArray(Charsets.UTF_8).size <= 65535)) { "username is invalid" }
|
|
108
|
+
require(password == null || (isValidMqttUtf8(password) && password.toByteArray(Charsets.UTF_8).size <= 65535)) { "password is invalid" }
|
|
109
|
+
require(password == null || username != null) { "password requires username" }
|
|
110
|
+
val willTopic = readString(options, "willTopic")
|
|
111
|
+
val willPayloadBase64 = readString(options, "willPayloadBase64")
|
|
112
|
+
require((willTopic == null) == (willPayloadBase64 == null)) { "willTopic and willPayloadBase64 must be provided together" }
|
|
113
|
+
require(willTopic == null || (isValidMqttUtf8(willTopic) && willTopic.isNotEmpty() && !willTopic.contains('+') && !willTopic.contains('#') && willTopic.toByteArray(Charsets.UTF_8).size <= 65535)) { "willTopic is invalid" }
|
|
114
|
+
willPayloadBase64?.let { require(isStrictBase64Shape(it)) { "willPayloadBase64 is invalid" } }
|
|
115
|
+
val willQos = readInt(options, "willQos", 0, 0, 2)
|
|
116
|
+
val willRetained = readBoolean(options, "willRetained", false)
|
|
117
|
+
val keepAlive = readInt(options, "keepAliveSeconds", 60, 0, 65535)
|
|
118
|
+
val cleanSession = readBoolean(options, "cleanSession", true)
|
|
119
|
+
val cleanStart = readBoolean(options, "cleanStart", true)
|
|
120
|
+
require(protocolVersion != 4 || !hasNonNull(options, "cleanStart", "sessionExpiryIntervalSeconds", "receiveMaximum", "userProperties")) {
|
|
121
|
+
"MQTT5 options are not valid for protocolVersion 4"
|
|
122
|
+
}
|
|
123
|
+
require(protocolVersion != 5 || !hasNonNull(options, "cleanSession")) {
|
|
124
|
+
"cleanSession is not valid for protocolVersion 5"
|
|
125
|
+
}
|
|
126
|
+
val expiry = readLong(options, "sessionExpiryIntervalSeconds", 0, 0, 4294967295L)
|
|
127
|
+
val receiveMaximum = readInt(options, "receiveMaximum", 65535, 1, 65535)
|
|
128
|
+
val userProperties = if (options.hasKey("userProperties") && !options.isNull("userProperties")) {
|
|
129
|
+
val array: ReadableArray = options.getArray("userProperties") ?: throw IllegalArgumentException("userProperties must be an array")
|
|
130
|
+
(0 until array.size()).map { index ->
|
|
131
|
+
val pair = array.getMap(index) ?: throw IllegalArgumentException("userProperties[$index] must be an object")
|
|
132
|
+
val key = pair.getString("key") ?: throw IllegalArgumentException("userProperties[$index].key must be a string")
|
|
133
|
+
val value = pair.getString("value") ?: throw IllegalArgumentException("userProperties[$index].value must be a string")
|
|
134
|
+
require(isValidMqttUtf8(key) && key.toByteArray(Charsets.UTF_8).size <= 65535) { "userProperties[$index].key is invalid" }
|
|
135
|
+
require(isValidMqttUtf8(value) && value.toByteArray(Charsets.UTF_8).size <= 65535) { "userProperties[$index].value is invalid" }
|
|
136
|
+
key to value
|
|
137
|
+
}
|
|
138
|
+
} else emptyList()
|
|
139
|
+
val connectTimeout = readLong(options, "connectTimeoutMs", 30_000, 1, Long.MAX_VALUE)
|
|
140
|
+
val operationTimeout = readLong(options, "operationTimeoutMs", 30_000, 1, Long.MAX_VALUE)
|
|
141
|
+
val disconnectTimeout = readLong(options, "disconnectTimeoutMs", 5_000, 1, Long.MAX_VALUE)
|
|
142
|
+
val reconnectInitial = readLong(options, "reconnectInitialDelayMs", 1_000, 1, Long.MAX_VALUE)
|
|
143
|
+
val reconnectMax = readLong(options, "reconnectMaxDelayMs", 30_000, 1, Long.MAX_VALUE)
|
|
144
|
+
require(reconnectMax >= reconnectInitial) { "reconnectMaxDelayMs must be >= reconnectInitialDelayMs" }
|
|
145
|
+
val jitter = readDouble(options, "reconnectJitterRatio", 0.2)
|
|
146
|
+
require(jitter in 0.0..1.0) { "reconnectJitterRatio must be in 0..1" }
|
|
147
|
+
val tlsFields = listOf("tlsCaBase64", "tlsP12Base64", "tlsP12Password", "tlsInsecure", "tlsAllowInvalidHostnameForTesting")
|
|
148
|
+
require(transport == "tls" || transport == "wss" || !hasNonNull(options, *tlsFields.toTypedArray())) { "tls options are only valid for tls/wss" }
|
|
149
|
+
val p12 = readString(options, "tlsP12Base64")
|
|
150
|
+
val p12Password = readString(options, "tlsP12Password")
|
|
151
|
+
require(p12Password == null || p12 != null) { "tlsP12Password requires tlsP12Base64" }
|
|
152
|
+
val maxQueuedMessages = readInt(options, "maxQueuedMessages", 1024, 1, 1_000_000)
|
|
153
|
+
val maxQueuedBytes = readLong(options, "maxQueuedBytes", 8L * 1024 * 1024, 1, Long.MAX_VALUE)
|
|
154
|
+
val maxMessageBytes = readInt(options, "maxMessageBytes", 1024 * 1024, 1, Int.MAX_VALUE)
|
|
155
|
+
val batchMaxMessages = readInt(options, "batchMaxMessages", 64, 1, 64)
|
|
156
|
+
val batchMaxBytes = readInt(options, "batchMaxBytes", 256 * 1024, 1, 256 * 1024)
|
|
157
|
+
val batchAckTimeout = readLong(options, "batchAckTimeoutMs", 15_000, 1, Long.MAX_VALUE)
|
|
158
|
+
val overflowPolicy = readString(options, "overflowPolicy") ?: "disconnect"
|
|
159
|
+
require(overflowPolicy == "disconnect" || overflowPolicy == "drop-newest") { "overflowPolicy is invalid" }
|
|
160
|
+
ParsedMqttOptions(host, port, protocolVersion, transport, clientId, username, password, willTopic, willPayloadBase64, willQos, willRetained, keepAlive, cleanSession, cleanStart, expiry, userProperties,
|
|
161
|
+
path ?: "", connectTimeout, operationTimeout, disconnectTimeout, readBoolean(options, "reconnectEnabled", true), reconnectInitial,
|
|
162
|
+
reconnectMax, jitter, readString(options, "tlsCaBase64"), p12, p12Password, readBoolean(options, "tlsInsecure", false), readBoolean(options, "tlsAllowInvalidHostnameForTesting", false), receiveMaximum,
|
|
163
|
+
maxQueuedMessages, maxQueuedBytes, maxMessageBytes, batchMaxMessages, batchMaxBytes, batchAckTimeout, overflowPolicy)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private fun hasNonNull(options: ReadableMap, vararg keys: String): Boolean = keys.any { options.hasKey(it) && !options.isNull(it) }
|
|
167
|
+
private fun readString(options: ReadableMap, key: String): String? = if (options.hasKey(key) && !options.isNull(key)) options.getString(key) else null
|
|
168
|
+
private fun readBoolean(options: ReadableMap, key: String, default: Boolean): Boolean = if (options.hasKey(key) && !options.isNull(key)) options.getBoolean(key) else default
|
|
169
|
+
private fun readDouble(options: ReadableMap, key: String, default: Double): Double = if (options.hasKey(key) && !options.isNull(key)) options.getDouble(key) else default
|
|
170
|
+
private fun readLong(options: ReadableMap, key: String, default: Long, min: Long, max: Long): Long {
|
|
171
|
+
val value = readDouble(options, key, default.toDouble())
|
|
172
|
+
require(value.isFinite() && value == value.toLong().toDouble() && value >= min && value <= max) { "$key must be an integer in $min..$max" }
|
|
173
|
+
return value.toLong()
|
|
174
|
+
}
|
|
175
|
+
private fun readInt(options: ReadableMap, key: String, default: Int, min: Int, max: Int): Int = readLong(options, key, default.toLong(), min.toLong(), max.toLong()).toInt()
|
|
176
|
+
}
|