@vdoninja/ninja-p2p 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -42
- package/dist/vdo-bridge.d.ts +13 -1
- package/dist/vdo-bridge.js +59 -1
- package/package.json +15 -3
- package/skills/ninja-p2p/SKILL.md +19 -23
package/README.md
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
# ninja-p2p
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`ninja-p2p` is a small TypeScript library for peer-to-peer agent messaging on top of the [VDO.Ninja](https://vdo.ninja) SDK.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It gives you a shared room, peer discovery, direct messages, group chat, topics, in-memory history, and a simple browser dashboard. The transport is WebRTC data channels. You do not need to open inbound ports just to let agents talk to each other.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- A Codex skill at `skills/ninja-p2p`
|
|
10
|
-
- An npm package published as `@vdoninja/ninja-p2p`
|
|
7
|
+
Package: [`@vdoninja/ninja-p2p`](https://www.npmjs.com/package/@vdoninja/ninja-p2p)
|
|
8
|
+
Support: https://discord.vdo.ninja
|
|
11
9
|
|
|
12
|
-
##
|
|
10
|
+
## What It Does
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
- peers join the same room and discover each other
|
|
13
|
+
- direct messages between named peers
|
|
14
|
+
- room-wide chat
|
|
15
|
+
- topic-based pub/sub
|
|
16
|
+
- peer status, skills, and presence tracking
|
|
17
|
+
- in-memory history replay
|
|
18
|
+
- in-memory offline queue for peers that drop and reconnect
|
|
19
|
+
- a standalone `dashboard.html` room monitor/chat client
|
|
20
|
+
|
|
21
|
+
## What It Does Not Do
|
|
22
|
+
|
|
23
|
+
- it is not a VPN
|
|
24
|
+
- it is not a generic TCP tunnel
|
|
25
|
+
- it is not a generic HTTP tunnel
|
|
26
|
+
- it does not provide durable storage
|
|
27
|
+
- it does not guarantee message delivery
|
|
28
|
+
- it does not turn the dashboard into a remote shell
|
|
29
|
+
|
|
30
|
+
This package is for agent coordination. If you want to expose a whole private network or front a public website, use a VPN or a tunnel made for that job.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
15
33
|
|
|
16
34
|
```bash
|
|
17
35
|
npm install @vdoninja/ninja-p2p @roamhq/wrtc
|
|
@@ -19,10 +37,9 @@ npm install @vdoninja/ninja-p2p @roamhq/wrtc
|
|
|
19
37
|
|
|
20
38
|
Notes:
|
|
21
39
|
|
|
22
|
-
- `@vdoninja/sdk` is
|
|
23
|
-
- `ws` comes from `@vdoninja/sdk` in Node
|
|
24
|
-
- `@roamhq/wrtc` is recommended for Node
|
|
25
|
-
- This package is configured for public scoped publishing via `publishConfig.access = "public"`.
|
|
40
|
+
- `@vdoninja/sdk` is installed automatically.
|
|
41
|
+
- `ws` comes from `@vdoninja/sdk` in Node.
|
|
42
|
+
- `@roamhq/wrtc` is recommended for Node bots that need WebRTC support.
|
|
26
43
|
|
|
27
44
|
## Add The Codex Skill
|
|
28
45
|
|
|
@@ -60,49 +77,137 @@ const bridge = new VDOBridge({
|
|
|
60
77
|
|
|
61
78
|
await bridge.connect();
|
|
62
79
|
|
|
63
|
-
// Send to everyone in the room
|
|
64
80
|
bridge.chat("Planner online");
|
|
65
|
-
|
|
66
|
-
// Send a private message to a specific peer
|
|
67
81
|
bridge.chat("sync now", "worker_bot");
|
|
68
|
-
|
|
69
|
-
// Publish a topic event
|
|
70
82
|
bridge.publishEvent("events", "status_change", { status: "busy" });
|
|
71
83
|
|
|
72
|
-
// Listen for incoming chat
|
|
73
84
|
bridge.bus.on("message:chat", (envelope) => {
|
|
74
85
|
console.log(`${envelope.from.name}: ${envelope.payload.text}`);
|
|
75
86
|
});
|
|
76
87
|
```
|
|
77
88
|
|
|
78
|
-
##
|
|
89
|
+
## Human Operator Example
|
|
79
90
|
|
|
80
|
-
-
|
|
81
|
-
- **Pub/Sub Messaging**: Broadcast, direct send, and topic fanout
|
|
82
|
-
- **Offline Queue**: Queue messages for peers that are temporarily disconnected
|
|
83
|
-
- **Message History**: Replay recent messages to late joiners
|
|
84
|
-
- **Keyword Triggers**: Wake async bots from chat patterns
|
|
85
|
-
- **Identity Protocol**: Peers announce skills and status changes
|
|
86
|
-
- **Heartbeat**: Detect stale peers
|
|
87
|
-
- **Browser Dashboard**: Monitor peers and chat from a single HTML file
|
|
91
|
+
One simple pattern is to put a human-operated process in the same room as the bots.
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
Agent:
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- Targeted command: `bridge.command("target_stream_id", "do_work", { jobId: 123 })`
|
|
94
|
-
- Topic event: `bridge.publishEvent("events", "status_change", { status: "busy" })`
|
|
95
|
+
```ts
|
|
96
|
+
import { VDOBridge } from "@vdoninja/ninja-p2p";
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
const worker = new VDOBridge({
|
|
99
|
+
room: "agents_room",
|
|
100
|
+
streamId: "worker_bot",
|
|
101
|
+
identity: {
|
|
102
|
+
streamId: "worker_bot",
|
|
103
|
+
role: "agent",
|
|
104
|
+
name: "Worker",
|
|
105
|
+
},
|
|
106
|
+
password: false,
|
|
107
|
+
skills: ["status", "say"],
|
|
108
|
+
});
|
|
97
109
|
|
|
98
|
-
|
|
110
|
+
await worker.connect();
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
worker.bus.on("message:command", (envelope) => {
|
|
113
|
+
const payload = envelope.payload as { command?: string; args?: { text?: string } };
|
|
114
|
+
|
|
115
|
+
if (payload.command === "status") {
|
|
116
|
+
worker.commandResponse(envelope, {
|
|
117
|
+
status: "idle",
|
|
118
|
+
peers: worker.peers.toJSON(),
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (payload.command === "say") {
|
|
124
|
+
console.log(payload.args?.text ?? "");
|
|
125
|
+
worker.commandResponse(envelope, { ok: true });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
worker.commandResponse(envelope, undefined, `unknown command: ${payload.command ?? "?"}`);
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Operator:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { VDOBridge } from "@vdoninja/ninja-p2p";
|
|
137
|
+
|
|
138
|
+
const operator = new VDOBridge({
|
|
139
|
+
room: "agents_room",
|
|
140
|
+
streamId: "steve_operator",
|
|
141
|
+
identity: {
|
|
142
|
+
streamId: "steve_operator",
|
|
143
|
+
role: "operator",
|
|
144
|
+
name: "Steve",
|
|
145
|
+
},
|
|
146
|
+
password: false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await operator.connect();
|
|
150
|
+
|
|
151
|
+
operator.command("worker_bot", "status");
|
|
152
|
+
operator.command("worker_bot", "say", { text: "hello from the operator" });
|
|
153
|
+
|
|
154
|
+
operator.bus.on("message:command_response", (envelope) => {
|
|
155
|
+
console.log(envelope.payload);
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The browser dashboard can also join the same room:
|
|
101
160
|
|
|
102
161
|
```text
|
|
103
162
|
dashboard.html?room=agents_room&password=false&name=Steve&autoconnect=true
|
|
104
163
|
```
|
|
105
164
|
|
|
165
|
+
The dashboard can chat, DM a peer, and send simple slash-command messages like `/status`, `/health`, `/history`, and `/peers`.
|
|
166
|
+
|
|
167
|
+
## Coordination Helpers
|
|
168
|
+
|
|
169
|
+
- `bridge.chat(text, to?)`
|
|
170
|
+
- `bridge.chatTopic(topic, text)`
|
|
171
|
+
- `bridge.command(targetStreamId, command, args?)`
|
|
172
|
+
- `bridge.commandResponse(message, result?, error?)`
|
|
173
|
+
- `bridge.publishEvent(topic, kind, data?)`
|
|
174
|
+
- `bridge.reply(message, type, payload)`
|
|
175
|
+
- `bridge.ack(message, payload?)`
|
|
176
|
+
- `bridge.requestHistory(targetStreamId, count?)`
|
|
177
|
+
|
|
178
|
+
These are lightweight coordination messages. They are useful, but they are not hard delivery guarantees.
|
|
179
|
+
|
|
180
|
+
## Raw Data, Media, and Advanced SDK Access
|
|
181
|
+
|
|
182
|
+
This package focuses on data-channel messaging.
|
|
183
|
+
|
|
184
|
+
The underlying VDO.Ninja SDK goes further than that. It can also:
|
|
185
|
+
|
|
186
|
+
- publish and view audio/video tracks
|
|
187
|
+
- emit `track` events
|
|
188
|
+
- send binary payloads over the data channel
|
|
189
|
+
|
|
190
|
+
This wrapper exposes two escape hatches for that:
|
|
191
|
+
|
|
192
|
+
- `bridge.sendRaw(data, targetStreamId?)` sends arbitrary data without wrapping it in the message envelope
|
|
193
|
+
- `bridge.getSDK()` returns the underlying VDO.Ninja SDK instance after `connect()`
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const sdk = bridge.getSDK();
|
|
199
|
+
|
|
200
|
+
sdk?.addEventListener("track", (event) => {
|
|
201
|
+
const track = event.detail?.track;
|
|
202
|
+
console.log("track", track?.kind);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const chunk = new Uint8Array([1, 2, 3]).buffer;
|
|
206
|
+
bridge.sendRaw(chunk, "worker_bot");
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
If you want to turn video into frames for ingestion, or build a file-transfer layer, do it on top of the SDK or on top of `sendRaw`. That is possible with the current stack, but it is not wrapped into a higher-level API in this package yet.
|
|
210
|
+
|
|
106
211
|
## Public API
|
|
107
212
|
|
|
108
213
|
Main entrypoint:
|
|
@@ -118,21 +223,28 @@ import { VDOBridge } from "@vdoninja/ninja-p2p/vdo-bridge";
|
|
|
118
223
|
import { createEnvelope } from "@vdoninja/ninja-p2p/protocol";
|
|
119
224
|
```
|
|
120
225
|
|
|
121
|
-
##
|
|
226
|
+
## Files
|
|
122
227
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
228
|
+
- `src/vdo-bridge.ts`: connection lifecycle and SDK integration
|
|
229
|
+
- `src/message-bus.ts`: chat, direct messages, topics, history, offline queue
|
|
230
|
+
- `src/peer-registry.ts`: peer state and presence
|
|
231
|
+
- `src/protocol.ts`: message envelope format
|
|
232
|
+
- `dashboard.html`: browser monitor/chat client
|
|
233
|
+
- `skills/ninja-p2p`: Codex skill
|
|
129
234
|
|
|
130
235
|
## Tests
|
|
131
236
|
|
|
132
237
|
```bash
|
|
133
238
|
npm test
|
|
239
|
+
npm run build
|
|
134
240
|
```
|
|
135
241
|
|
|
242
|
+
## Support
|
|
243
|
+
|
|
244
|
+
- Discord: https://discord.vdo.ninja
|
|
245
|
+
- VDO.Ninja: https://vdo.ninja
|
|
246
|
+
- Social Stream Ninja: https://socialstream.ninja
|
|
247
|
+
|
|
136
248
|
## License
|
|
137
249
|
|
|
138
250
|
MIT
|
package/dist/vdo-bridge.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { EventEmitter } from "node:events";
|
|
11
11
|
import { MessageBus, type MessageBusOptions } from "./message-bus.js";
|
|
12
12
|
import { PeerRegistry } from "./peer-registry.js";
|
|
13
|
-
import { type AnnouncePayload, type MessageEnvelope, type PeerIdentity } from "./protocol.js";
|
|
13
|
+
import { type AnnouncePayload, type MessageEnvelope, type MessageType, type PeerIdentity } from "./protocol.js";
|
|
14
14
|
export type VDOBridgeOptions = {
|
|
15
15
|
room: string;
|
|
16
16
|
streamId: string;
|
|
@@ -58,6 +58,18 @@ export declare class VDOBridge extends EventEmitter {
|
|
|
58
58
|
command(targetStreamId: string, command: string, args?: unknown): MessageEnvelope;
|
|
59
59
|
/** Publish an event to a topic. */
|
|
60
60
|
publishEvent(topic: string, kind: string, data?: unknown): MessageEnvelope;
|
|
61
|
+
/** Send raw data through the underlying SDK without envelope wrapping. */
|
|
62
|
+
sendRaw(data: unknown, targetStreamId?: string): boolean;
|
|
63
|
+
/** Reply to a received message using its sender as the target. */
|
|
64
|
+
reply(message: MessageEnvelope, type: MessageType, payload: unknown): MessageEnvelope;
|
|
65
|
+
/** Acknowledge receipt of a received message. */
|
|
66
|
+
ack(message: MessageEnvelope, payload?: unknown): MessageEnvelope;
|
|
67
|
+
/** Respond to a command or request-style message. */
|
|
68
|
+
commandResponse(message: MessageEnvelope, result?: unknown, error?: string): MessageEnvelope;
|
|
69
|
+
/** Ask a peer to replay recent message history to this bridge. */
|
|
70
|
+
requestHistory(targetStreamId: string, count?: number): MessageEnvelope;
|
|
71
|
+
/** Access the underlying VDO.Ninja SDK instance for advanced media workflows. */
|
|
72
|
+
getSDK(): InstanceType<typeof import("@vdoninja/sdk")> | null;
|
|
61
73
|
private wireSDKEvents;
|
|
62
74
|
private startHeartbeat;
|
|
63
75
|
private stopHeartbeat;
|
package/dist/vdo-bridge.js
CHANGED
|
@@ -23,7 +23,7 @@ export class VDOBridge extends EventEmitter {
|
|
|
23
23
|
skills;
|
|
24
24
|
status = "idle";
|
|
25
25
|
statusDetail = "";
|
|
26
|
-
version = "0.1.
|
|
26
|
+
version = "0.1.1";
|
|
27
27
|
constructor(options) {
|
|
28
28
|
super();
|
|
29
29
|
this.options = options;
|
|
@@ -149,6 +149,64 @@ export class VDOBridge extends EventEmitter {
|
|
|
149
149
|
publishEvent(topic, kind, data) {
|
|
150
150
|
return this.bus.publish(topic, "event", { kind, ...((data && typeof data === "object") ? data : { data }) });
|
|
151
151
|
}
|
|
152
|
+
/** Send raw data through the underlying SDK without envelope wrapping. */
|
|
153
|
+
sendRaw(data, targetStreamId) {
|
|
154
|
+
if (!this.sdk)
|
|
155
|
+
return false;
|
|
156
|
+
try {
|
|
157
|
+
if (!targetStreamId) {
|
|
158
|
+
this.sdk.sendData(data);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
const peer = this.peers.getPeer(targetStreamId);
|
|
162
|
+
if (peer?.uuid) {
|
|
163
|
+
this.sdk.sendData(data, { UUID: peer.uuid });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
this.sdk.sendData(data, { streamID: targetStreamId });
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
this.emit("error", err);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Reply to a received message using its sender as the target. */
|
|
176
|
+
reply(message, type, payload) {
|
|
177
|
+
return this.bus.send(message.from.streamId, type, payload);
|
|
178
|
+
}
|
|
179
|
+
/** Acknowledge receipt of a received message. */
|
|
180
|
+
ack(message, payload) {
|
|
181
|
+
const ackPayload = { messageId: message.id };
|
|
182
|
+
if (payload !== undefined) {
|
|
183
|
+
ackPayload.data = payload;
|
|
184
|
+
}
|
|
185
|
+
return this.bus.send(message.from.streamId, "ack", ackPayload);
|
|
186
|
+
}
|
|
187
|
+
/** Respond to a command or request-style message. */
|
|
188
|
+
commandResponse(message, result, error) {
|
|
189
|
+
if (error) {
|
|
190
|
+
return this.bus.send(message.from.streamId, "command_response", {
|
|
191
|
+
requestId: message.id,
|
|
192
|
+
ok: false,
|
|
193
|
+
error,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return this.bus.send(message.from.streamId, "command_response", {
|
|
197
|
+
requestId: message.id,
|
|
198
|
+
ok: true,
|
|
199
|
+
result: result ?? null,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Ask a peer to replay recent message history to this bridge. */
|
|
203
|
+
requestHistory(targetStreamId, count = 50) {
|
|
204
|
+
return this.bus.send(targetStreamId, "history_request", { count });
|
|
205
|
+
}
|
|
206
|
+
/** Access the underlying VDO.Ninja SDK instance for advanced media workflows. */
|
|
207
|
+
getSDK() {
|
|
208
|
+
return this.sdk;
|
|
209
|
+
}
|
|
152
210
|
// ── SDK Event Wiring ─────────────────────────────────────────────────────
|
|
153
211
|
wireSDKEvents() {
|
|
154
212
|
if (!this.sdk)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vdoninja/ninja-p2p",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Open source P2P agent messaging and coordination over VDO.Ninja WebRTC data channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -42,7 +42,19 @@
|
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|
|
43
43
|
"prepare": "npm run build"
|
|
44
44
|
},
|
|
45
|
-
"keywords": [
|
|
45
|
+
"keywords": [
|
|
46
|
+
"p2p",
|
|
47
|
+
"webrtc",
|
|
48
|
+
"vdo.ninja",
|
|
49
|
+
"agent",
|
|
50
|
+
"agent-mesh",
|
|
51
|
+
"pub-sub",
|
|
52
|
+
"data-channel",
|
|
53
|
+
"bot",
|
|
54
|
+
"nat-traversal",
|
|
55
|
+
"hole-punching",
|
|
56
|
+
"remote-control"
|
|
57
|
+
],
|
|
46
58
|
"author": "Steve Seguin",
|
|
47
59
|
"license": "MIT",
|
|
48
60
|
"publishConfig": {
|
|
@@ -1,41 +1,36 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ninja-p2p
|
|
3
|
-
description:
|
|
3
|
+
description: Use VDO.Ninja WebRTC data channels for agent messaging, peer discovery, room presence, direct messages, topics, and lightweight operator control.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ninja-p2p
|
|
7
7
|
|
|
8
|
-
Use this
|
|
8
|
+
Use this when a project needs agents to meet in a room and talk to each other over WebRTC.
|
|
9
9
|
|
|
10
|
-
## What
|
|
10
|
+
## What It Gives You
|
|
11
11
|
|
|
12
|
-
- `VDOBridge` for connection lifecycle
|
|
13
|
-
- `PeerRegistry` for presence, skills, topics, and status
|
|
14
|
-
- `MessageBus` for
|
|
15
|
-
- `dashboard.html` for a browser
|
|
12
|
+
- `VDOBridge` for connection lifecycle and SDK wiring
|
|
13
|
+
- `PeerRegistry` for presence, skills, topics, and status
|
|
14
|
+
- `MessageBus` for room chat, direct messages, topic fanout, history replay, and the in-memory offline queue
|
|
15
|
+
- `dashboard.html` for a browser room monitor/chat client
|
|
16
16
|
|
|
17
|
-
## Install
|
|
18
|
-
|
|
19
|
-
Install it from npm:
|
|
17
|
+
## Install
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
20
|
npm install @vdoninja/ninja-p2p @roamhq/wrtc
|
|
23
21
|
```
|
|
24
22
|
|
|
25
|
-
`@vdoninja/sdk` is installed
|
|
26
|
-
|
|
27
|
-
## Add This Skill To Codex
|
|
28
|
-
|
|
29
|
-
Use Codex's built-in `skill-installer` helper and install the `skills/ninja-p2p` path from this repo, then restart Codex.
|
|
23
|
+
`@vdoninja/sdk` is installed automatically. `ws` comes from the SDK in Node.
|
|
30
24
|
|
|
31
|
-
## Default
|
|
25
|
+
## Default Pattern
|
|
32
26
|
|
|
33
|
-
1. Pick a shared `room
|
|
34
|
-
2. Give each
|
|
27
|
+
1. Pick a shared `room`.
|
|
28
|
+
2. Give each peer a unique `streamId`.
|
|
35
29
|
3. Connect with `VDOBridge`.
|
|
36
|
-
4. Use `bridge.chat(
|
|
37
|
-
5. Use `bridge.chat(
|
|
38
|
-
6. Use `bridge.publishEvent(
|
|
30
|
+
4. Use `bridge.chat()` for room messages.
|
|
31
|
+
5. Use `bridge.chat(..., targetStreamId)` or `bridge.command()` for direct messages.
|
|
32
|
+
6. Use `bridge.publishEvent()` for topic messages.
|
|
33
|
+
7. Use `bridge.commandResponse()`, `bridge.ack()`, or `bridge.reply()` when a peer should answer back.
|
|
39
34
|
|
|
40
35
|
## Example
|
|
41
36
|
|
|
@@ -68,7 +63,8 @@ bridge.bus.on("message:chat", (envelope) => {
|
|
|
68
63
|
|
|
69
64
|
## Notes
|
|
70
65
|
|
|
71
|
-
- `room` and `streamId` should be stable and human-readable
|
|
66
|
+
- `room` and `streamId` should be stable and human-readable.
|
|
72
67
|
- Private messages target peer `streamId`s.
|
|
73
68
|
- Topic messages are broadcast and filtered on the receiver side.
|
|
74
|
-
- The
|
|
69
|
+
- The offline queue is in memory, not durable storage.
|
|
70
|
+
- `sendRaw()` and `getSDK()` are there if you need lower-level SDK access for binary data or media work.
|