@relyvolabs/web-sdk 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/README.md +107 -2
- package/dist/index.cjs +303 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +232 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -4
package/README.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @relyvolabs/web-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Browser client SDK for Relyvo: realtime chat and video calls. The browser holds
|
|
4
|
+
only short-lived credentials minted by your backend — never an API key.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { RelyvoChat, RelyvoCall } from "@relyvolabs/web-sdk";
|
|
8
|
+
|
|
9
|
+
const chat = new RelyvoChat(credentials);
|
|
10
|
+
await chat.connect();
|
|
11
|
+
chat.on("message", ({ message, senderName }) => render(message, senderName));
|
|
12
|
+
await chat.send({ content: "hello", clientMessageId: crypto.randomUUID() });
|
|
13
|
+
|
|
14
|
+
const call = await RelyvoCall.connect({ serverUrl, token });
|
|
15
|
+
await call.enableCamera();
|
|
16
|
+
await call.enableMicrophone();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Your backend creates both credential sets with
|
|
20
|
+
[`relyvo-server-sdk`](https://github.com/relyvolabs/relyvo-server-sdk):
|
|
21
|
+
|
|
22
|
+
| Feature | Server-side call | What the browser receives |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Video | `video.createToken(...)` | `serverUrl` + participant `token` |
|
|
25
|
+
| Chat | `chat.upsertUser`, `chat.createUserToken`, realtime tokens | `wsUrl`, connection `token`, `channel`, `subscriptionToken` (+ typing channel) |
|
|
26
|
+
|
|
27
|
+
## Chat runs without REST
|
|
28
|
+
|
|
29
|
+
Chat operations are RPC frames on the Centrifugo WebSocket the client already
|
|
30
|
+
holds:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
browser ──WS──► Centrifugo ──proxy──► chat-api ──► PostgreSQL
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`send`, `history`, `markRead` and `channels` all travel this path, so the browser
|
|
37
|
+
never calls Relyvo REST and no CORS allowance is required. Identity and project
|
|
38
|
+
scope come from the connection token; the SDK never sends a project or user id.
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `new RelyvoChat(credentials, options?)`
|
|
43
|
+
|
|
44
|
+
| Credential | Meaning |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `wsUrl` | Centrifugo WebSocket URL |
|
|
47
|
+
| `token` | Centrifugo connection token |
|
|
48
|
+
| `channel` | Conversation channel (`project:<id>:channel:<id>`) |
|
|
49
|
+
| `subscriptionToken` | Subscription token for that channel |
|
|
50
|
+
| `typingChannel` / `typingSubscriptionToken` | Optional typing sibling |
|
|
51
|
+
|
|
52
|
+
Methods: `connect()`, `on(event, handler)`, `send()`, `history()`, `markRead()`,
|
|
53
|
+
`channels()`, `disconnect()`.
|
|
54
|
+
|
|
55
|
+
### Token refresh
|
|
56
|
+
|
|
57
|
+
Connection and subscription tokens are short-lived. Give the SDK callbacks that
|
|
58
|
+
ask your backend for replacement tokens; the transport reconnects on its own.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const chat = new RelyvoChat(credentials, {
|
|
62
|
+
getConnectionToken: async () => {
|
|
63
|
+
const response = await fetch("/api/chat/token");
|
|
64
|
+
return (await response.json()).token;
|
|
65
|
+
},
|
|
66
|
+
getSubscriptionToken: async (channel) => {
|
|
67
|
+
const response = await fetch(`/api/chat/subscription?channel=${encodeURIComponent(channel)}`);
|
|
68
|
+
return (await response.json()).token;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The callbacks run when a token expires, never before, and they must reach a
|
|
74
|
+
server: a browser cannot mint these tokens.
|
|
75
|
+
|
|
76
|
+
Events: `connected`, `disconnected`, `message`, `typing`, `error`.
|
|
77
|
+
|
|
78
|
+
### `RelyvoCall.connect({ serverUrl, token }, options?)`
|
|
79
|
+
|
|
80
|
+
Methods: `enableCamera()`, `enableMicrophone()`, `shareScreen()`, `on(event, handler)`,
|
|
81
|
+
`disconnect()`. `call.room` exposes the underlying LiveKit room when you need
|
|
82
|
+
something this wrapper does not cover.
|
|
83
|
+
|
|
84
|
+
Events: `connected`, `disconnected`, `participantConnected`,
|
|
85
|
+
`participantDisconnected`, `trackSubscribed`, `trackUnsubscribed`,
|
|
86
|
+
`connectionQualityChanged`, `mediaDevicesError`.
|
|
87
|
+
|
|
88
|
+
## Errors
|
|
89
|
+
|
|
90
|
+
Every failure is a `RelyvoError` with a stable `code`: transport failures use
|
|
91
|
+
`transport_error` / `connect_failed`, and a rejected RPC carries the chat
|
|
92
|
+
service's own code (`forbidden`, `not_found`, `rate_limited`, …).
|
|
93
|
+
|
|
94
|
+
## Requirements
|
|
95
|
+
|
|
96
|
+
- `centrifuge` and `livekit-client` are peer dependencies; install the versions
|
|
97
|
+
your app already uses so there is one copy of each.
|
|
98
|
+
- Video and screen sharing need a secure context (`https://` or `localhost`).
|
|
99
|
+
|
|
100
|
+
## Status
|
|
101
|
+
|
|
102
|
+
`0.1.0`: chat (send, history, read, channels, typing) and call connect with
|
|
103
|
+
camera/microphone/screen control. Attachments, edits and deletes are not exposed
|
|
104
|
+
yet.
|
|
105
|
+
|
|
106
|
+
## Licence
|
|
107
|
+
|
|
108
|
+
Proprietary. Not for redistribution.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
var index_exports = {};
|
|
24
|
+
__export(index_exports, {
|
|
25
|
+
RelyvoCall: () => RelyvoCall,
|
|
26
|
+
RelyvoChat: () => RelyvoChat,
|
|
27
|
+
RelyvoError: () => RelyvoError,
|
|
28
|
+
VERSION: () => VERSION,
|
|
29
|
+
isRelyvoError: () => isRelyvoError
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/chat.ts
|
|
34
|
+
var import_centrifuge = require("centrifuge");
|
|
35
|
+
|
|
36
|
+
// src/errors.ts
|
|
37
|
+
var RelyvoError = class extends Error {
|
|
38
|
+
constructor(code, message, cause) {
|
|
39
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
40
|
+
/** Stable machine code. RPC failures carry the chat service's code. */
|
|
41
|
+
__publicField(this, "code");
|
|
42
|
+
/** Transport-level cause, when there is one. */
|
|
43
|
+
__publicField(this, "cause");
|
|
44
|
+
this.name = "RelyvoError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.cause = cause;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function isRelyvoError(error) {
|
|
50
|
+
return error instanceof RelyvoError;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/chat.ts
|
|
54
|
+
var defaultFactory = (wsUrl, options) => new import_centrifuge.Centrifuge(wsUrl, {
|
|
55
|
+
token: options.token,
|
|
56
|
+
...options.getToken === void 0 ? {} : { getToken: options.getToken }
|
|
57
|
+
});
|
|
58
|
+
var RelyvoChat = class {
|
|
59
|
+
constructor(credentials, options = {}) {
|
|
60
|
+
__publicField(this, "credentials");
|
|
61
|
+
__publicField(this, "client");
|
|
62
|
+
__publicField(this, "connectTimeoutMs");
|
|
63
|
+
__publicField(this, "getSubscriptionToken");
|
|
64
|
+
__publicField(this, "handlers", {
|
|
65
|
+
connected: /* @__PURE__ */ new Set(),
|
|
66
|
+
disconnected: /* @__PURE__ */ new Set(),
|
|
67
|
+
message: /* @__PURE__ */ new Set(),
|
|
68
|
+
typing: /* @__PURE__ */ new Set(),
|
|
69
|
+
error: /* @__PURE__ */ new Set()
|
|
70
|
+
});
|
|
71
|
+
__publicField(this, "typingSubscription");
|
|
72
|
+
__publicField(this, "conversationSubscription");
|
|
73
|
+
this.credentials = credentials;
|
|
74
|
+
this.connectTimeoutMs = options.connectTimeoutMs ?? 1e4;
|
|
75
|
+
this.getSubscriptionToken = options.getSubscriptionToken;
|
|
76
|
+
this.client = (options.createCentrifuge ?? defaultFactory)(credentials.wsUrl, {
|
|
77
|
+
token: credentials.token,
|
|
78
|
+
...options.getConnectionToken === void 0 ? {} : { getToken: options.getConnectionToken }
|
|
79
|
+
});
|
|
80
|
+
this.client.on("connected", () => this.emit("connected"));
|
|
81
|
+
this.client.on("disconnected", (ctx) => this.emit("disconnected", ctx?.reason));
|
|
82
|
+
this.client.on(
|
|
83
|
+
"error",
|
|
84
|
+
(ctx) => this.emit("error", new RelyvoError("transport_error", String(ctx?.error ?? "connection error")))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
/** Opens the socket and subscribes to the conversation and typing channels. */
|
|
88
|
+
async connect() {
|
|
89
|
+
const connected = new Promise((resolve, reject) => {
|
|
90
|
+
const timer = setTimeout(
|
|
91
|
+
() => reject(new RelyvoError("connect_timeout", "the realtime connection timed out")),
|
|
92
|
+
this.connectTimeoutMs
|
|
93
|
+
);
|
|
94
|
+
this.handlers.connected.add(() => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
this.handlers.error.add((error) => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
reject(error);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
this.client.connect();
|
|
104
|
+
await connected;
|
|
105
|
+
this.conversationSubscription = this.client.newSubscription(
|
|
106
|
+
this.credentials.channel,
|
|
107
|
+
this.subscriptionTokenOptions(this.credentials.channel, this.credentials.subscriptionToken)
|
|
108
|
+
);
|
|
109
|
+
this.conversationSubscription.on("publication", (ctx) => this.handlePublication(ctx?.data));
|
|
110
|
+
await this.conversationSubscription.subscribe();
|
|
111
|
+
if (this.credentials.typingChannel && this.credentials.typingSubscriptionToken) {
|
|
112
|
+
this.typingSubscription = this.client.newSubscription(
|
|
113
|
+
this.credentials.typingChannel,
|
|
114
|
+
this.subscriptionTokenOptions(
|
|
115
|
+
this.credentials.typingChannel,
|
|
116
|
+
this.credentials.typingSubscriptionToken
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
this.typingSubscription.on("publication", (ctx) => this.handleTyping(ctx?.data));
|
|
120
|
+
await this.typingSubscription.subscribe();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
on(event, handler) {
|
|
124
|
+
this.handlers[event].add(handler);
|
|
125
|
+
return () => this.handlers[event].delete(handler);
|
|
126
|
+
}
|
|
127
|
+
/** Sends a message. The same clientMessageId returns the original message. */
|
|
128
|
+
async send(input) {
|
|
129
|
+
return this.rpc("chat.send", {
|
|
130
|
+
channel_id: this.channelId,
|
|
131
|
+
client_message_id: input.clientMessageId,
|
|
132
|
+
content: input.content,
|
|
133
|
+
type: input.type ?? "text",
|
|
134
|
+
...input.metadata === void 0 ? {} : { metadata: input.metadata }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Reads history backwards from a cursor, or forwards with `direction: "after"`. */
|
|
138
|
+
history(input = {}) {
|
|
139
|
+
return this.rpc("chat.history", {
|
|
140
|
+
channel_id: this.channelId,
|
|
141
|
+
...input.before === void 0 ? {} : { cursor: input.before },
|
|
142
|
+
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
143
|
+
...input.direction === void 0 ? {} : { direction: input.direction }
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Marks the channel read up to a message. */
|
|
147
|
+
async markRead(messageId) {
|
|
148
|
+
await this.rpc("chat.read", { channel_id: this.channelId, message_id: messageId });
|
|
149
|
+
}
|
|
150
|
+
/** Lists the channels this user belongs to. */
|
|
151
|
+
channels(input = {}) {
|
|
152
|
+
return this.rpc("chat.channels", input).then((page) => page.items ?? []);
|
|
153
|
+
}
|
|
154
|
+
disconnect() {
|
|
155
|
+
this.conversationSubscription?.unsubscribe();
|
|
156
|
+
this.typingSubscription?.unsubscribe();
|
|
157
|
+
this.client.disconnect();
|
|
158
|
+
}
|
|
159
|
+
/** Channel id extracted from the subscription channel name. */
|
|
160
|
+
get channelId() {
|
|
161
|
+
const parts = this.credentials.channel.split(":");
|
|
162
|
+
const id = parts[parts.length - 1];
|
|
163
|
+
if (!id) {
|
|
164
|
+
throw new RelyvoError("invalid_credentials", "the channel name does not contain an id");
|
|
165
|
+
}
|
|
166
|
+
return id;
|
|
167
|
+
}
|
|
168
|
+
subscriptionTokenOptions(channel, token) {
|
|
169
|
+
return {
|
|
170
|
+
token,
|
|
171
|
+
...this.getSubscriptionToken === void 0 ? {} : { getToken: () => this.getSubscriptionToken(channel) }
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async rpc(method, data) {
|
|
175
|
+
let response;
|
|
176
|
+
try {
|
|
177
|
+
response = await this.client.rpc(method, data);
|
|
178
|
+
} catch (cause) {
|
|
179
|
+
throw new RelyvoError("transport_error", `rpc ${method} failed`, cause);
|
|
180
|
+
}
|
|
181
|
+
const envelope = response?.data;
|
|
182
|
+
if (!envelope || envelope.ok !== true) {
|
|
183
|
+
throw new RelyvoError(
|
|
184
|
+
envelope?.error?.code ?? "rpc_failed",
|
|
185
|
+
envelope?.error?.message ?? `rpc ${method} failed`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return envelope.data;
|
|
189
|
+
}
|
|
190
|
+
handlePublication(payload) {
|
|
191
|
+
if (!payload || payload.type !== "chat.message.created") return;
|
|
192
|
+
const data = payload.data ?? {};
|
|
193
|
+
if (data.channel_id !== this.channelId || !data.message) return;
|
|
194
|
+
const event = {
|
|
195
|
+
message: data.message,
|
|
196
|
+
channelId: data.channel_id,
|
|
197
|
+
senderId: data.sender?.id ?? data.message.sender_id,
|
|
198
|
+
senderName: data.sender?.display_name
|
|
199
|
+
};
|
|
200
|
+
this.emit("message", event);
|
|
201
|
+
}
|
|
202
|
+
handleTyping(payload) {
|
|
203
|
+
if (!payload || payload.type !== "chat.typing") return;
|
|
204
|
+
const data = payload.data ?? {};
|
|
205
|
+
if (data.channel_id !== this.channelId || !data.user_id) return;
|
|
206
|
+
const event = {
|
|
207
|
+
channel_id: data.channel_id,
|
|
208
|
+
user_id: data.user_id,
|
|
209
|
+
display_name: data.display_name
|
|
210
|
+
};
|
|
211
|
+
this.emit("typing", event);
|
|
212
|
+
}
|
|
213
|
+
emit(event, ...args) {
|
|
214
|
+
for (const handler of this.handlers[event]) {
|
|
215
|
+
handler(...args);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// src/video.ts
|
|
221
|
+
var import_livekit_client = require("livekit-client");
|
|
222
|
+
var defaultFactory2 = () => new import_livekit_client.Room({ adaptiveStream: true, dynacast: true });
|
|
223
|
+
var EVENT_NAMES = {
|
|
224
|
+
connected: import_livekit_client.RoomEvent.Connected,
|
|
225
|
+
disconnected: import_livekit_client.RoomEvent.Disconnected,
|
|
226
|
+
participantConnected: import_livekit_client.RoomEvent.ParticipantConnected,
|
|
227
|
+
participantDisconnected: import_livekit_client.RoomEvent.ParticipantDisconnected,
|
|
228
|
+
trackSubscribed: import_livekit_client.RoomEvent.TrackSubscribed,
|
|
229
|
+
trackUnsubscribed: import_livekit_client.RoomEvent.TrackUnsubscribed,
|
|
230
|
+
connectionQualityChanged: import_livekit_client.RoomEvent.ConnectionQualityChanged,
|
|
231
|
+
mediaDevicesError: import_livekit_client.RoomEvent.MediaDevicesError
|
|
232
|
+
};
|
|
233
|
+
var RelyvoCall = class _RelyvoCall {
|
|
234
|
+
constructor(room) {
|
|
235
|
+
__publicField(this, "room");
|
|
236
|
+
__publicField(this, "connected", false);
|
|
237
|
+
this.room = room;
|
|
238
|
+
}
|
|
239
|
+
/** Connects to the room the token was minted for. */
|
|
240
|
+
static async connect(credentials, options = {}) {
|
|
241
|
+
if (!credentials.serverUrl || !credentials.token) {
|
|
242
|
+
throw new RelyvoError("invalid_credentials", "serverUrl and token are required");
|
|
243
|
+
}
|
|
244
|
+
const call = new _RelyvoCall((options.createRoom ?? defaultFactory2)());
|
|
245
|
+
try {
|
|
246
|
+
await call.room.connect(credentials.serverUrl, credentials.token);
|
|
247
|
+
} catch (cause) {
|
|
248
|
+
throw new RelyvoError("connect_failed", "the media connection could not be established", cause);
|
|
249
|
+
}
|
|
250
|
+
call.connected = true;
|
|
251
|
+
return call;
|
|
252
|
+
}
|
|
253
|
+
/** Publishes or stops the camera. */
|
|
254
|
+
async enableCamera(enabled = true) {
|
|
255
|
+
this.requireConnected();
|
|
256
|
+
await this.room.localParticipant.setCameraEnabled(enabled);
|
|
257
|
+
}
|
|
258
|
+
/** Publishes or stops the microphone. */
|
|
259
|
+
async enableMicrophone(enabled = true) {
|
|
260
|
+
this.requireConnected();
|
|
261
|
+
await this.room.localParticipant.setMicrophoneEnabled(enabled);
|
|
262
|
+
}
|
|
263
|
+
/** Starts or stops screen sharing where the browser supports it. */
|
|
264
|
+
async shareScreen(enabled = true) {
|
|
265
|
+
this.requireConnected();
|
|
266
|
+
if (enabled && typeof navigator !== "undefined" && !navigator.mediaDevices?.getDisplayMedia) {
|
|
267
|
+
throw new RelyvoError("unsupported", "this browser does not support screen sharing");
|
|
268
|
+
}
|
|
269
|
+
await this.room.localParticipant.setScreenShareEnabled(enabled);
|
|
270
|
+
}
|
|
271
|
+
/** Subscribes to a room event. Returns an unsubscribe function. */
|
|
272
|
+
on(event, handler) {
|
|
273
|
+
const name = EVENT_NAMES[event];
|
|
274
|
+
this.room.on(name, handler);
|
|
275
|
+
return () => {
|
|
276
|
+
const target = this.room;
|
|
277
|
+
target.off?.(name, handler);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** Leaves the room and releases the local devices. */
|
|
281
|
+
disconnect() {
|
|
282
|
+
if (!this.connected) return;
|
|
283
|
+
this.connected = false;
|
|
284
|
+
void this.room.disconnect();
|
|
285
|
+
}
|
|
286
|
+
requireConnected() {
|
|
287
|
+
if (!this.connected) {
|
|
288
|
+
throw new RelyvoError("not_connected", "the call is not connected");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// src/index.ts
|
|
294
|
+
var VERSION = "0.1.0";
|
|
295
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
296
|
+
0 && (module.exports = {
|
|
297
|
+
RelyvoCall,
|
|
298
|
+
RelyvoChat,
|
|
299
|
+
RelyvoError,
|
|
300
|
+
VERSION,
|
|
301
|
+
isRelyvoError
|
|
302
|
+
});
|
|
303
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/chat.ts","../src/errors.ts","../src/video.ts"],"sourcesContent":["/**\n * Relyvo browser client SDK.\n *\n * The browser holds only short-lived credentials minted by your backend: a\n * Centrifugo connection token for chat and a LiveKit participant token for\n * media. Chat operations travel as RPC frames on the WebSocket, so this SDK\n * never calls Relyvo REST and never needs a CORS allowance.\n *\n * @example\n * ```ts\n * const chat = new RelyvoChat(credentials);\n * await chat.connect();\n * chat.on(\"message\", ({ message }) => console.log(message.content));\n * await chat.send({ content: \"hello\", clientMessageId: crypto.randomUUID() });\n * ```\n */\n\nexport { RelyvoChat } from \"./chat.js\";\nexport type {\n CentrifugeClient,\n CentrifugeFactory,\n CentrifugeSubscription,\n RelyvoChatOptions,\n} from \"./chat.js\";\nexport { RelyvoError, isRelyvoError } from \"./errors.js\";\nexport { RelyvoCall } from \"./video.js\";\nexport type { RelyvoCallOptions, RoomFactory, RoomLike } from \"./video.js\";\nexport type {\n CallCredentials,\n CallEvent,\n ChatChannel,\n ChatCredentials,\n ChatEventHandler,\n ChatMessage,\n ChatMessagePage,\n ChatMessageType,\n MessageEvent,\n TypingEvent,\n} from \"./types.js\";\n\nexport const VERSION = \"0.1.0\";\n","import { Centrifuge } from \"centrifuge\";\n\nimport { RelyvoError } from \"./errors.js\";\nimport type {\n ChatChannel,\n ChatCredentials,\n ChatEventHandler,\n ChatMessage,\n ChatMessagePage,\n MessageEvent,\n TypingEvent,\n} from \"./types.js\";\n\n/** The subset of the Centrifuge client this SDK depends on. */\nexport interface CentrifugeSubscription {\n subscribe(): Promise<unknown>;\n unsubscribe(): void;\n on(event: \"publication\" | \"subscribed\" | \"error\", handler: (ctx: any) => void): void;\n}\n\n/** Centrifuge fetches a replacement token through getToken when one expires. */\nexport interface ConnectionTokenOptions {\n token?: string;\n getToken?: () => Promise<string>;\n}\n\nexport interface SubscriptionTokenOptions {\n token?: string;\n getToken?: () => Promise<string>;\n}\n\nexport interface CentrifugeClient {\n connect(): void;\n disconnect(): void;\n on(event: \"connected\" | \"disconnected\" | \"error\", handler: (ctx: any) => void): void;\n newSubscription(channel: string, options: SubscriptionTokenOptions): CentrifugeSubscription;\n rpc(method: string, data: unknown): Promise<{ data: unknown }>;\n}\n\nexport type CentrifugeFactory = (wsUrl: string, options: ConnectionTokenOptions) => CentrifugeClient;\n\nconst defaultFactory: CentrifugeFactory = (wsUrl, options) =>\n new Centrifuge(wsUrl, {\n token: options.token,\n ...(options.getToken === undefined ? {} : { getToken: options.getToken }),\n }) as unknown as CentrifugeClient;\n\nexport interface RelyvoChatOptions {\n /** Injectable transport, used by tests. */\n createCentrifuge?: CentrifugeFactory;\n connectTimeoutMs?: number;\n /**\n * Called when the connection token expires. Return a fresh Centrifugo\n * connection token from your backend; the transport reconnects by itself.\n */\n getConnectionToken?: () => Promise<string>;\n /**\n * Called when a subscription token expires. Receive the channel name and\n * return a fresh subscription token for it.\n */\n getSubscriptionToken?: (channel: string) => Promise<string>;\n}\n\ntype RpcEnvelope<T> = {\n ok?: boolean;\n data?: T;\n error?: { code?: string; message?: string };\n};\n\n/**\n * Realtime chat over the Centrifugo connection.\n *\n * Every operation is an RPC frame on the WebSocket the client already holds, so\n * the browser never calls Relyvo REST and needs no CORS allowance. Identity comes\n * from the connection token; this class never sends a project or user id.\n */\nexport class RelyvoChat {\n readonly credentials: ChatCredentials;\n\n private readonly client: CentrifugeClient;\n private readonly connectTimeoutMs: number;\n private readonly getSubscriptionToken?: (channel: string) => Promise<string>;\n private readonly handlers: { [K in keyof ChatEventHandler]: Set<ChatEventHandler[K]> } = {\n connected: new Set(),\n disconnected: new Set(),\n message: new Set(),\n typing: new Set(),\n error: new Set(),\n };\n private typingSubscription?: CentrifugeSubscription;\n private conversationSubscription?: CentrifugeSubscription;\n\n constructor(credentials: ChatCredentials, options: RelyvoChatOptions = {}) {\n this.credentials = credentials;\n this.connectTimeoutMs = options.connectTimeoutMs ?? 10_000;\n this.getSubscriptionToken = options.getSubscriptionToken;\n this.client = (options.createCentrifuge ?? defaultFactory)(credentials.wsUrl, {\n token: credentials.token,\n ...(options.getConnectionToken === undefined\n ? {}\n : { getToken: options.getConnectionToken }),\n });\n\n this.client.on(\"connected\", () => this.emit(\"connected\"));\n this.client.on(\"disconnected\", (ctx) => this.emit(\"disconnected\", ctx?.reason));\n this.client.on(\"error\", (ctx) =>\n this.emit(\"error\", new RelyvoError(\"transport_error\", String(ctx?.error ?? \"connection error\"))),\n );\n }\n\n /** Opens the socket and subscribes to the conversation and typing channels. */\n async connect(): Promise<void> {\n const connected = new Promise<void>((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new RelyvoError(\"connect_timeout\", \"the realtime connection timed out\")),\n this.connectTimeoutMs,\n );\n\n this.handlers.connected.add(() => {\n clearTimeout(timer);\n resolve();\n });\n this.handlers.error.add((error) => {\n clearTimeout(timer);\n reject(error);\n });\n });\n\n this.client.connect();\n await connected;\n\n this.conversationSubscription = this.client.newSubscription(\n this.credentials.channel,\n this.subscriptionTokenOptions(this.credentials.channel, this.credentials.subscriptionToken),\n );\n this.conversationSubscription.on(\"publication\", (ctx) => this.handlePublication(ctx?.data));\n await this.conversationSubscription.subscribe();\n\n if (this.credentials.typingChannel && this.credentials.typingSubscriptionToken) {\n this.typingSubscription = this.client.newSubscription(\n this.credentials.typingChannel,\n this.subscriptionTokenOptions(\n this.credentials.typingChannel,\n this.credentials.typingSubscriptionToken,\n ),\n );\n this.typingSubscription.on(\"publication\", (ctx) => this.handleTyping(ctx?.data));\n await this.typingSubscription.subscribe();\n }\n }\n\n on<K extends keyof ChatEventHandler>(event: K, handler: ChatEventHandler[K]): () => void {\n this.handlers[event].add(handler);\n return () => this.handlers[event].delete(handler);\n }\n\n /** Sends a message. The same clientMessageId returns the original message. */\n async send(input: {\n content: string;\n clientMessageId: string;\n type?: string;\n metadata?: Record<string, unknown>;\n }): Promise<{ message: ChatMessage; created: boolean }> {\n return this.rpc(\"chat.send\", {\n channel_id: this.channelId,\n client_message_id: input.clientMessageId,\n content: input.content,\n type: input.type ?? \"text\",\n ...(input.metadata === undefined ? {} : { metadata: input.metadata }),\n });\n }\n\n /** Reads history backwards from a cursor, or forwards with `direction: \"after\"`. */\n history(input: { before?: string; limit?: number; direction?: \"before\" | \"after\" } = {}): Promise<ChatMessagePage> {\n return this.rpc(\"chat.history\", {\n channel_id: this.channelId,\n ...(input.before === undefined ? {} : { cursor: input.before }),\n ...(input.limit === undefined ? {} : { limit: input.limit }),\n ...(input.direction === undefined ? {} : { direction: input.direction }),\n });\n }\n\n /** Marks the channel read up to a message. */\n async markRead(messageId: string): Promise<void> {\n await this.rpc(\"chat.read\", { channel_id: this.channelId, message_id: messageId });\n }\n\n /** Lists the channels this user belongs to. */\n channels(input: { cursor?: string; limit?: number } = {}): Promise<ChatChannel[]> {\n return this.rpc<{ items: ChatChannel[] }>(\"chat.channels\", input).then((page) => page.items ?? []);\n }\n\n disconnect(): void {\n this.conversationSubscription?.unsubscribe();\n this.typingSubscription?.unsubscribe();\n this.client.disconnect();\n }\n\n /** Channel id extracted from the subscription channel name. */\n private get channelId(): string {\n const parts = this.credentials.channel.split(\":\");\n const id = parts[parts.length - 1];\n if (!id) {\n throw new RelyvoError(\"invalid_credentials\", \"the channel name does not contain an id\");\n }\n return id;\n }\n\n private subscriptionTokenOptions(\n channel: string,\n token: string,\n ): SubscriptionTokenOptions {\n return {\n token,\n ...(this.getSubscriptionToken === undefined\n ? {}\n : { getToken: () => this.getSubscriptionToken!(channel) }),\n };\n }\n\n private async rpc<T>(method: string, data: unknown): Promise<T> {\n let response: { data: unknown };\n try {\n response = await this.client.rpc(method, data);\n } catch (cause) {\n throw new RelyvoError(\"transport_error\", `rpc ${method} failed`, cause);\n }\n\n const envelope = response?.data as RpcEnvelope<T> | undefined;\n if (!envelope || envelope.ok !== true) {\n throw new RelyvoError(\n envelope?.error?.code ?? \"rpc_failed\",\n envelope?.error?.message ?? `rpc ${method} failed`,\n );\n }\n\n return envelope.data as T;\n }\n\n private handlePublication(payload: any): void {\n if (!payload || payload.type !== \"chat.message.created\") return;\n\n const data = payload.data ?? {};\n if (data.channel_id !== this.channelId || !data.message) return;\n\n const event: MessageEvent = {\n message: data.message,\n channelId: data.channel_id,\n senderId: data.sender?.id ?? data.message.sender_id,\n senderName: data.sender?.display_name,\n };\n this.emit(\"message\", event);\n }\n\n private handleTyping(payload: any): void {\n if (!payload || payload.type !== \"chat.typing\") return;\n\n const data = payload.data ?? {};\n if (data.channel_id !== this.channelId || !data.user_id) return;\n\n const event: TypingEvent = {\n channel_id: data.channel_id,\n user_id: data.user_id,\n display_name: data.display_name,\n };\n this.emit(\"typing\", event);\n }\n\n private emit<K extends keyof ChatEventHandler>(event: K, ...args: Parameters<ChatEventHandler[K]>): void {\n for (const handler of this.handlers[event]) {\n (handler as (...values: unknown[]) => void)(...args);\n }\n }\n}\n","/** One error type for every failure this SDK produces. */\nexport class RelyvoError extends Error {\n /** Stable machine code. RPC failures carry the chat service's code. */\n readonly code: string;\n /** Transport-level cause, when there is one. */\n readonly cause?: unknown;\n\n constructor(code: string, message: string, cause?: unknown) {\n super(message, cause === undefined ? undefined : { cause });\n this.name = \"RelyvoError\";\n this.code = code;\n this.cause = cause;\n }\n}\n\nexport function isRelyvoError(error: unknown): error is RelyvoError {\n return error instanceof RelyvoError;\n}\n","import { Room, RoomEvent } from \"livekit-client\";\n\nimport { RelyvoError } from \"./errors.js\";\nimport type { CallCredentials, CallEvent } from \"./types.js\";\n\n/** The subset of the LiveKit Room this SDK depends on. */\nexport interface RoomLike {\n connect(url: string, token: string): Promise<void>;\n disconnect(): Promise<void> | void;\n on(event: string, handler: (...args: any[]) => void): unknown;\n localParticipant: {\n setCameraEnabled(enabled: boolean): Promise<void>;\n setMicrophoneEnabled(enabled: boolean): Promise<void>;\n setScreenShareEnabled(enabled: boolean): Promise<void>;\n };\n}\n\nexport type RoomFactory = () => RoomLike;\n\nconst defaultFactory: RoomFactory = () =>\n new Room({ adaptiveStream: true, dynacast: true }) as unknown as RoomLike;\n\n/** LiveKit event names, mapped so callers do not import the media library. */\nconst EVENT_NAMES: Record<CallEvent, RoomEvent> = {\n connected: RoomEvent.Connected,\n disconnected: RoomEvent.Disconnected,\n participantConnected: RoomEvent.ParticipantConnected,\n participantDisconnected: RoomEvent.ParticipantDisconnected,\n trackSubscribed: RoomEvent.TrackSubscribed,\n trackUnsubscribed: RoomEvent.TrackUnsubscribed,\n connectionQualityChanged: RoomEvent.ConnectionQualityChanged,\n mediaDevicesError: RoomEvent.MediaDevicesError,\n};\n\nexport interface RelyvoCallOptions {\n /** Injectable room, used by tests. */\n createRoom?: RoomFactory;\n}\n\n/**\n * One video or voice call.\n *\n * The participant token is minted by your backend with the Relyvo server SDK; the\n * grant is join-only and already scoped to the room, so this class cannot ask for\n * more than that token allows.\n */\nexport class RelyvoCall {\n readonly room: RoomLike;\n private connected = false;\n\n private constructor(room: RoomLike) {\n this.room = room;\n }\n\n /** Connects to the room the token was minted for. */\n static async connect(\n credentials: CallCredentials,\n options: RelyvoCallOptions = {},\n ): Promise<RelyvoCall> {\n if (!credentials.serverUrl || !credentials.token) {\n throw new RelyvoError(\"invalid_credentials\", \"serverUrl and token are required\");\n }\n\n const call = new RelyvoCall((options.createRoom ?? defaultFactory)());\n\n try {\n await call.room.connect(credentials.serverUrl, credentials.token);\n } catch (cause) {\n throw new RelyvoError(\"connect_failed\", \"the media connection could not be established\", cause);\n }\n\n call.connected = true;\n return call;\n }\n\n /** Publishes or stops the camera. */\n async enableCamera(enabled = true): Promise<void> {\n this.requireConnected();\n await this.room.localParticipant.setCameraEnabled(enabled);\n }\n\n /** Publishes or stops the microphone. */\n async enableMicrophone(enabled = true): Promise<void> {\n this.requireConnected();\n await this.room.localParticipant.setMicrophoneEnabled(enabled);\n }\n\n /** Starts or stops screen sharing where the browser supports it. */\n async shareScreen(enabled = true): Promise<void> {\n this.requireConnected();\n if (enabled && typeof navigator !== \"undefined\" && !navigator.mediaDevices?.getDisplayMedia) {\n throw new RelyvoError(\"unsupported\", \"this browser does not support screen sharing\");\n }\n await this.room.localParticipant.setScreenShareEnabled(enabled);\n }\n\n /** Subscribes to a room event. Returns an unsubscribe function. */\n on(event: CallEvent, handler: (...args: any[]) => void): () => void {\n const name = EVENT_NAMES[event];\n this.room.on(name, handler);\n return () => {\n const target = this.room as unknown as {\n off?: (event: string, callback: (...args: any[]) => void) => void;\n };\n target.off?.(name, handler);\n };\n }\n\n /** Leaves the room and releases the local devices. */\n disconnect(): void {\n if (!this.connected) return;\n this.connected = false;\n void this.room.disconnect();\n }\n\n private requireConnected(): void {\n if (!this.connected) {\n throw new RelyvoError(\"not_connected\", \"the call is not connected\");\n }\n }\n}\n"],"mappings":"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","names":["defaultFactory"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
type ChatMessageType = "text" | "system" | "attachment";
|
|
2
|
+
interface ChatMessage {
|
|
3
|
+
id: string;
|
|
4
|
+
channel_id: string;
|
|
5
|
+
channel_seq: number;
|
|
6
|
+
sender_id: string;
|
|
7
|
+
client_message_id?: string;
|
|
8
|
+
type: ChatMessageType;
|
|
9
|
+
content: string;
|
|
10
|
+
metadata?: Record<string, unknown> | null;
|
|
11
|
+
reply_to_message_id?: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
updated_at?: string;
|
|
14
|
+
deleted_at?: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface ChatMessagePage {
|
|
17
|
+
items: ChatMessage[];
|
|
18
|
+
has_more: boolean;
|
|
19
|
+
next_cursor?: string;
|
|
20
|
+
}
|
|
21
|
+
interface ChatChannel {
|
|
22
|
+
id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
name: string;
|
|
25
|
+
last_seq: number;
|
|
26
|
+
external_id?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Credentials your backend mints with the Relyvo server SDK. */
|
|
29
|
+
interface ChatCredentials {
|
|
30
|
+
/** Centrifugo WebSocket URL, e.g. wss://chat.example.com/connection/websocket */
|
|
31
|
+
wsUrl: string;
|
|
32
|
+
/** Centrifugo connection token for this user. */
|
|
33
|
+
token: string;
|
|
34
|
+
/** Conversation channel, e.g. project:<projectID>:channel:<channelID> */
|
|
35
|
+
channel: string;
|
|
36
|
+
/** Subscription token for the conversation channel. */
|
|
37
|
+
subscriptionToken: string;
|
|
38
|
+
/** Optional sibling channel that carries typing events. */
|
|
39
|
+
typingChannel?: string;
|
|
40
|
+
/** Subscription token for the typing channel. */
|
|
41
|
+
typingSubscriptionToken?: string;
|
|
42
|
+
}
|
|
43
|
+
interface CallCredentials {
|
|
44
|
+
/** Browser-facing LiveKit URL, e.g. wss://rtc.example.com */
|
|
45
|
+
serverUrl: string;
|
|
46
|
+
/** Short-lived participant token minted by your backend. */
|
|
47
|
+
token: string;
|
|
48
|
+
}
|
|
49
|
+
interface TypingEvent {
|
|
50
|
+
channel_id: string;
|
|
51
|
+
user_id: string;
|
|
52
|
+
display_name?: string;
|
|
53
|
+
}
|
|
54
|
+
interface MessageEvent {
|
|
55
|
+
message: ChatMessage;
|
|
56
|
+
senderId: string;
|
|
57
|
+
senderName?: string;
|
|
58
|
+
channelId: string;
|
|
59
|
+
}
|
|
60
|
+
type ChatEventHandler = {
|
|
61
|
+
connected: () => void;
|
|
62
|
+
disconnected: (reason?: string) => void;
|
|
63
|
+
message: (event: MessageEvent) => void;
|
|
64
|
+
typing: (event: TypingEvent) => void;
|
|
65
|
+
error: (error: Error) => void;
|
|
66
|
+
};
|
|
67
|
+
type CallEvent = "connected" | "disconnected" | "participantConnected" | "participantDisconnected" | "trackSubscribed" | "trackUnsubscribed" | "connectionQualityChanged" | "mediaDevicesError";
|
|
68
|
+
|
|
69
|
+
/** The subset of the Centrifuge client this SDK depends on. */
|
|
70
|
+
interface CentrifugeSubscription {
|
|
71
|
+
subscribe(): Promise<unknown>;
|
|
72
|
+
unsubscribe(): void;
|
|
73
|
+
on(event: "publication" | "subscribed" | "error", handler: (ctx: any) => void): void;
|
|
74
|
+
}
|
|
75
|
+
/** Centrifuge fetches a replacement token through getToken when one expires. */
|
|
76
|
+
interface ConnectionTokenOptions {
|
|
77
|
+
token?: string;
|
|
78
|
+
getToken?: () => Promise<string>;
|
|
79
|
+
}
|
|
80
|
+
interface SubscriptionTokenOptions {
|
|
81
|
+
token?: string;
|
|
82
|
+
getToken?: () => Promise<string>;
|
|
83
|
+
}
|
|
84
|
+
interface CentrifugeClient {
|
|
85
|
+
connect(): void;
|
|
86
|
+
disconnect(): void;
|
|
87
|
+
on(event: "connected" | "disconnected" | "error", handler: (ctx: any) => void): void;
|
|
88
|
+
newSubscription(channel: string, options: SubscriptionTokenOptions): CentrifugeSubscription;
|
|
89
|
+
rpc(method: string, data: unknown): Promise<{
|
|
90
|
+
data: unknown;
|
|
91
|
+
}>;
|
|
92
|
+
}
|
|
93
|
+
type CentrifugeFactory = (wsUrl: string, options: ConnectionTokenOptions) => CentrifugeClient;
|
|
94
|
+
interface RelyvoChatOptions {
|
|
95
|
+
/** Injectable transport, used by tests. */
|
|
96
|
+
createCentrifuge?: CentrifugeFactory;
|
|
97
|
+
connectTimeoutMs?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Called when the connection token expires. Return a fresh Centrifugo
|
|
100
|
+
* connection token from your backend; the transport reconnects by itself.
|
|
101
|
+
*/
|
|
102
|
+
getConnectionToken?: () => Promise<string>;
|
|
103
|
+
/**
|
|
104
|
+
* Called when a subscription token expires. Receive the channel name and
|
|
105
|
+
* return a fresh subscription token for it.
|
|
106
|
+
*/
|
|
107
|
+
getSubscriptionToken?: (channel: string) => Promise<string>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Realtime chat over the Centrifugo connection.
|
|
111
|
+
*
|
|
112
|
+
* Every operation is an RPC frame on the WebSocket the client already holds, so
|
|
113
|
+
* the browser never calls Relyvo REST and needs no CORS allowance. Identity comes
|
|
114
|
+
* from the connection token; this class never sends a project or user id.
|
|
115
|
+
*/
|
|
116
|
+
declare class RelyvoChat {
|
|
117
|
+
readonly credentials: ChatCredentials;
|
|
118
|
+
private readonly client;
|
|
119
|
+
private readonly connectTimeoutMs;
|
|
120
|
+
private readonly getSubscriptionToken?;
|
|
121
|
+
private readonly handlers;
|
|
122
|
+
private typingSubscription?;
|
|
123
|
+
private conversationSubscription?;
|
|
124
|
+
constructor(credentials: ChatCredentials, options?: RelyvoChatOptions);
|
|
125
|
+
/** Opens the socket and subscribes to the conversation and typing channels. */
|
|
126
|
+
connect(): Promise<void>;
|
|
127
|
+
on<K extends keyof ChatEventHandler>(event: K, handler: ChatEventHandler[K]): () => void;
|
|
128
|
+
/** Sends a message. The same clientMessageId returns the original message. */
|
|
129
|
+
send(input: {
|
|
130
|
+
content: string;
|
|
131
|
+
clientMessageId: string;
|
|
132
|
+
type?: string;
|
|
133
|
+
metadata?: Record<string, unknown>;
|
|
134
|
+
}): Promise<{
|
|
135
|
+
message: ChatMessage;
|
|
136
|
+
created: boolean;
|
|
137
|
+
}>;
|
|
138
|
+
/** Reads history backwards from a cursor, or forwards with `direction: "after"`. */
|
|
139
|
+
history(input?: {
|
|
140
|
+
before?: string;
|
|
141
|
+
limit?: number;
|
|
142
|
+
direction?: "before" | "after";
|
|
143
|
+
}): Promise<ChatMessagePage>;
|
|
144
|
+
/** Marks the channel read up to a message. */
|
|
145
|
+
markRead(messageId: string): Promise<void>;
|
|
146
|
+
/** Lists the channels this user belongs to. */
|
|
147
|
+
channels(input?: {
|
|
148
|
+
cursor?: string;
|
|
149
|
+
limit?: number;
|
|
150
|
+
}): Promise<ChatChannel[]>;
|
|
151
|
+
disconnect(): void;
|
|
152
|
+
/** Channel id extracted from the subscription channel name. */
|
|
153
|
+
private get channelId();
|
|
154
|
+
private subscriptionTokenOptions;
|
|
155
|
+
private rpc;
|
|
156
|
+
private handlePublication;
|
|
157
|
+
private handleTyping;
|
|
158
|
+
private emit;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** One error type for every failure this SDK produces. */
|
|
162
|
+
declare class RelyvoError extends Error {
|
|
163
|
+
/** Stable machine code. RPC failures carry the chat service's code. */
|
|
164
|
+
readonly code: string;
|
|
165
|
+
/** Transport-level cause, when there is one. */
|
|
166
|
+
readonly cause?: unknown;
|
|
167
|
+
constructor(code: string, message: string, cause?: unknown);
|
|
168
|
+
}
|
|
169
|
+
declare function isRelyvoError(error: unknown): error is RelyvoError;
|
|
170
|
+
|
|
171
|
+
/** The subset of the LiveKit Room this SDK depends on. */
|
|
172
|
+
interface RoomLike {
|
|
173
|
+
connect(url: string, token: string): Promise<void>;
|
|
174
|
+
disconnect(): Promise<void> | void;
|
|
175
|
+
on(event: string, handler: (...args: any[]) => void): unknown;
|
|
176
|
+
localParticipant: {
|
|
177
|
+
setCameraEnabled(enabled: boolean): Promise<void>;
|
|
178
|
+
setMicrophoneEnabled(enabled: boolean): Promise<void>;
|
|
179
|
+
setScreenShareEnabled(enabled: boolean): Promise<void>;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
type RoomFactory = () => RoomLike;
|
|
183
|
+
interface RelyvoCallOptions {
|
|
184
|
+
/** Injectable room, used by tests. */
|
|
185
|
+
createRoom?: RoomFactory;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* One video or voice call.
|
|
189
|
+
*
|
|
190
|
+
* The participant token is minted by your backend with the Relyvo server SDK; the
|
|
191
|
+
* grant is join-only and already scoped to the room, so this class cannot ask for
|
|
192
|
+
* more than that token allows.
|
|
193
|
+
*/
|
|
194
|
+
declare class RelyvoCall {
|
|
195
|
+
readonly room: RoomLike;
|
|
196
|
+
private connected;
|
|
197
|
+
private constructor();
|
|
198
|
+
/** Connects to the room the token was minted for. */
|
|
199
|
+
static connect(credentials: CallCredentials, options?: RelyvoCallOptions): Promise<RelyvoCall>;
|
|
200
|
+
/** Publishes or stops the camera. */
|
|
201
|
+
enableCamera(enabled?: boolean): Promise<void>;
|
|
202
|
+
/** Publishes or stops the microphone. */
|
|
203
|
+
enableMicrophone(enabled?: boolean): Promise<void>;
|
|
204
|
+
/** Starts or stops screen sharing where the browser supports it. */
|
|
205
|
+
shareScreen(enabled?: boolean): Promise<void>;
|
|
206
|
+
/** Subscribes to a room event. Returns an unsubscribe function. */
|
|
207
|
+
on(event: CallEvent, handler: (...args: any[]) => void): () => void;
|
|
208
|
+
/** Leaves the room and releases the local devices. */
|
|
209
|
+
disconnect(): void;
|
|
210
|
+
private requireConnected;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Relyvo browser client SDK.
|
|
215
|
+
*
|
|
216
|
+
* The browser holds only short-lived credentials minted by your backend: a
|
|
217
|
+
* Centrifugo connection token for chat and a LiveKit participant token for
|
|
218
|
+
* media. Chat operations travel as RPC frames on the WebSocket, so this SDK
|
|
219
|
+
* never calls Relyvo REST and never needs a CORS allowance.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const chat = new RelyvoChat(credentials);
|
|
224
|
+
* await chat.connect();
|
|
225
|
+
* chat.on("message", ({ message }) => console.log(message.content));
|
|
226
|
+
* await chat.send({ content: "hello", clientMessageId: crypto.randomUUID() });
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
declare const VERSION = "0.1.0";
|
|
231
|
+
|
|
232
|
+
export { type CallCredentials, type CallEvent, type CentrifugeClient, type CentrifugeFactory, type CentrifugeSubscription, type ChatChannel, type ChatCredentials, type ChatEventHandler, type ChatMessage, type ChatMessagePage, type ChatMessageType, type MessageEvent, RelyvoCall, type RelyvoCallOptions, RelyvoChat, type RelyvoChatOptions, RelyvoError, type RoomFactory, type RoomLike, type TypingEvent, VERSION, isRelyvoError };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
type ChatMessageType = "text" | "system" | "attachment";
|
|
2
|
+
interface ChatMessage {
|
|
3
|
+
id: string;
|
|
4
|
+
channel_id: string;
|
|
5
|
+
channel_seq: number;
|
|
6
|
+
sender_id: string;
|
|
7
|
+
client_message_id?: string;
|
|
8
|
+
type: ChatMessageType;
|
|
9
|
+
content: string;
|
|
10
|
+
metadata?: Record<string, unknown> | null;
|
|
11
|
+
reply_to_message_id?: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
updated_at?: string;
|
|
14
|
+
deleted_at?: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface ChatMessagePage {
|
|
17
|
+
items: ChatMessage[];
|
|
18
|
+
has_more: boolean;
|
|
19
|
+
next_cursor?: string;
|
|
20
|
+
}
|
|
21
|
+
interface ChatChannel {
|
|
22
|
+
id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
name: string;
|
|
25
|
+
last_seq: number;
|
|
26
|
+
external_id?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Credentials your backend mints with the Relyvo server SDK. */
|
|
29
|
+
interface ChatCredentials {
|
|
30
|
+
/** Centrifugo WebSocket URL, e.g. wss://chat.example.com/connection/websocket */
|
|
31
|
+
wsUrl: string;
|
|
32
|
+
/** Centrifugo connection token for this user. */
|
|
33
|
+
token: string;
|
|
34
|
+
/** Conversation channel, e.g. project:<projectID>:channel:<channelID> */
|
|
35
|
+
channel: string;
|
|
36
|
+
/** Subscription token for the conversation channel. */
|
|
37
|
+
subscriptionToken: string;
|
|
38
|
+
/** Optional sibling channel that carries typing events. */
|
|
39
|
+
typingChannel?: string;
|
|
40
|
+
/** Subscription token for the typing channel. */
|
|
41
|
+
typingSubscriptionToken?: string;
|
|
42
|
+
}
|
|
43
|
+
interface CallCredentials {
|
|
44
|
+
/** Browser-facing LiveKit URL, e.g. wss://rtc.example.com */
|
|
45
|
+
serverUrl: string;
|
|
46
|
+
/** Short-lived participant token minted by your backend. */
|
|
47
|
+
token: string;
|
|
48
|
+
}
|
|
49
|
+
interface TypingEvent {
|
|
50
|
+
channel_id: string;
|
|
51
|
+
user_id: string;
|
|
52
|
+
display_name?: string;
|
|
53
|
+
}
|
|
54
|
+
interface MessageEvent {
|
|
55
|
+
message: ChatMessage;
|
|
56
|
+
senderId: string;
|
|
57
|
+
senderName?: string;
|
|
58
|
+
channelId: string;
|
|
59
|
+
}
|
|
60
|
+
type ChatEventHandler = {
|
|
61
|
+
connected: () => void;
|
|
62
|
+
disconnected: (reason?: string) => void;
|
|
63
|
+
message: (event: MessageEvent) => void;
|
|
64
|
+
typing: (event: TypingEvent) => void;
|
|
65
|
+
error: (error: Error) => void;
|
|
66
|
+
};
|
|
67
|
+
type CallEvent = "connected" | "disconnected" | "participantConnected" | "participantDisconnected" | "trackSubscribed" | "trackUnsubscribed" | "connectionQualityChanged" | "mediaDevicesError";
|
|
68
|
+
|
|
69
|
+
/** The subset of the Centrifuge client this SDK depends on. */
|
|
70
|
+
interface CentrifugeSubscription {
|
|
71
|
+
subscribe(): Promise<unknown>;
|
|
72
|
+
unsubscribe(): void;
|
|
73
|
+
on(event: "publication" | "subscribed" | "error", handler: (ctx: any) => void): void;
|
|
74
|
+
}
|
|
75
|
+
/** Centrifuge fetches a replacement token through getToken when one expires. */
|
|
76
|
+
interface ConnectionTokenOptions {
|
|
77
|
+
token?: string;
|
|
78
|
+
getToken?: () => Promise<string>;
|
|
79
|
+
}
|
|
80
|
+
interface SubscriptionTokenOptions {
|
|
81
|
+
token?: string;
|
|
82
|
+
getToken?: () => Promise<string>;
|
|
83
|
+
}
|
|
84
|
+
interface CentrifugeClient {
|
|
85
|
+
connect(): void;
|
|
86
|
+
disconnect(): void;
|
|
87
|
+
on(event: "connected" | "disconnected" | "error", handler: (ctx: any) => void): void;
|
|
88
|
+
newSubscription(channel: string, options: SubscriptionTokenOptions): CentrifugeSubscription;
|
|
89
|
+
rpc(method: string, data: unknown): Promise<{
|
|
90
|
+
data: unknown;
|
|
91
|
+
}>;
|
|
92
|
+
}
|
|
93
|
+
type CentrifugeFactory = (wsUrl: string, options: ConnectionTokenOptions) => CentrifugeClient;
|
|
94
|
+
interface RelyvoChatOptions {
|
|
95
|
+
/** Injectable transport, used by tests. */
|
|
96
|
+
createCentrifuge?: CentrifugeFactory;
|
|
97
|
+
connectTimeoutMs?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Called when the connection token expires. Return a fresh Centrifugo
|
|
100
|
+
* connection token from your backend; the transport reconnects by itself.
|
|
101
|
+
*/
|
|
102
|
+
getConnectionToken?: () => Promise<string>;
|
|
103
|
+
/**
|
|
104
|
+
* Called when a subscription token expires. Receive the channel name and
|
|
105
|
+
* return a fresh subscription token for it.
|
|
106
|
+
*/
|
|
107
|
+
getSubscriptionToken?: (channel: string) => Promise<string>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Realtime chat over the Centrifugo connection.
|
|
111
|
+
*
|
|
112
|
+
* Every operation is an RPC frame on the WebSocket the client already holds, so
|
|
113
|
+
* the browser never calls Relyvo REST and needs no CORS allowance. Identity comes
|
|
114
|
+
* from the connection token; this class never sends a project or user id.
|
|
115
|
+
*/
|
|
116
|
+
declare class RelyvoChat {
|
|
117
|
+
readonly credentials: ChatCredentials;
|
|
118
|
+
private readonly client;
|
|
119
|
+
private readonly connectTimeoutMs;
|
|
120
|
+
private readonly getSubscriptionToken?;
|
|
121
|
+
private readonly handlers;
|
|
122
|
+
private typingSubscription?;
|
|
123
|
+
private conversationSubscription?;
|
|
124
|
+
constructor(credentials: ChatCredentials, options?: RelyvoChatOptions);
|
|
125
|
+
/** Opens the socket and subscribes to the conversation and typing channels. */
|
|
126
|
+
connect(): Promise<void>;
|
|
127
|
+
on<K extends keyof ChatEventHandler>(event: K, handler: ChatEventHandler[K]): () => void;
|
|
128
|
+
/** Sends a message. The same clientMessageId returns the original message. */
|
|
129
|
+
send(input: {
|
|
130
|
+
content: string;
|
|
131
|
+
clientMessageId: string;
|
|
132
|
+
type?: string;
|
|
133
|
+
metadata?: Record<string, unknown>;
|
|
134
|
+
}): Promise<{
|
|
135
|
+
message: ChatMessage;
|
|
136
|
+
created: boolean;
|
|
137
|
+
}>;
|
|
138
|
+
/** Reads history backwards from a cursor, or forwards with `direction: "after"`. */
|
|
139
|
+
history(input?: {
|
|
140
|
+
before?: string;
|
|
141
|
+
limit?: number;
|
|
142
|
+
direction?: "before" | "after";
|
|
143
|
+
}): Promise<ChatMessagePage>;
|
|
144
|
+
/** Marks the channel read up to a message. */
|
|
145
|
+
markRead(messageId: string): Promise<void>;
|
|
146
|
+
/** Lists the channels this user belongs to. */
|
|
147
|
+
channels(input?: {
|
|
148
|
+
cursor?: string;
|
|
149
|
+
limit?: number;
|
|
150
|
+
}): Promise<ChatChannel[]>;
|
|
151
|
+
disconnect(): void;
|
|
152
|
+
/** Channel id extracted from the subscription channel name. */
|
|
153
|
+
private get channelId();
|
|
154
|
+
private subscriptionTokenOptions;
|
|
155
|
+
private rpc;
|
|
156
|
+
private handlePublication;
|
|
157
|
+
private handleTyping;
|
|
158
|
+
private emit;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** One error type for every failure this SDK produces. */
|
|
162
|
+
declare class RelyvoError extends Error {
|
|
163
|
+
/** Stable machine code. RPC failures carry the chat service's code. */
|
|
164
|
+
readonly code: string;
|
|
165
|
+
/** Transport-level cause, when there is one. */
|
|
166
|
+
readonly cause?: unknown;
|
|
167
|
+
constructor(code: string, message: string, cause?: unknown);
|
|
168
|
+
}
|
|
169
|
+
declare function isRelyvoError(error: unknown): error is RelyvoError;
|
|
170
|
+
|
|
171
|
+
/** The subset of the LiveKit Room this SDK depends on. */
|
|
172
|
+
interface RoomLike {
|
|
173
|
+
connect(url: string, token: string): Promise<void>;
|
|
174
|
+
disconnect(): Promise<void> | void;
|
|
175
|
+
on(event: string, handler: (...args: any[]) => void): unknown;
|
|
176
|
+
localParticipant: {
|
|
177
|
+
setCameraEnabled(enabled: boolean): Promise<void>;
|
|
178
|
+
setMicrophoneEnabled(enabled: boolean): Promise<void>;
|
|
179
|
+
setScreenShareEnabled(enabled: boolean): Promise<void>;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
type RoomFactory = () => RoomLike;
|
|
183
|
+
interface RelyvoCallOptions {
|
|
184
|
+
/** Injectable room, used by tests. */
|
|
185
|
+
createRoom?: RoomFactory;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* One video or voice call.
|
|
189
|
+
*
|
|
190
|
+
* The participant token is minted by your backend with the Relyvo server SDK; the
|
|
191
|
+
* grant is join-only and already scoped to the room, so this class cannot ask for
|
|
192
|
+
* more than that token allows.
|
|
193
|
+
*/
|
|
194
|
+
declare class RelyvoCall {
|
|
195
|
+
readonly room: RoomLike;
|
|
196
|
+
private connected;
|
|
197
|
+
private constructor();
|
|
198
|
+
/** Connects to the room the token was minted for. */
|
|
199
|
+
static connect(credentials: CallCredentials, options?: RelyvoCallOptions): Promise<RelyvoCall>;
|
|
200
|
+
/** Publishes or stops the camera. */
|
|
201
|
+
enableCamera(enabled?: boolean): Promise<void>;
|
|
202
|
+
/** Publishes or stops the microphone. */
|
|
203
|
+
enableMicrophone(enabled?: boolean): Promise<void>;
|
|
204
|
+
/** Starts or stops screen sharing where the browser supports it. */
|
|
205
|
+
shareScreen(enabled?: boolean): Promise<void>;
|
|
206
|
+
/** Subscribes to a room event. Returns an unsubscribe function. */
|
|
207
|
+
on(event: CallEvent, handler: (...args: any[]) => void): () => void;
|
|
208
|
+
/** Leaves the room and releases the local devices. */
|
|
209
|
+
disconnect(): void;
|
|
210
|
+
private requireConnected;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Relyvo browser client SDK.
|
|
215
|
+
*
|
|
216
|
+
* The browser holds only short-lived credentials minted by your backend: a
|
|
217
|
+
* Centrifugo connection token for chat and a LiveKit participant token for
|
|
218
|
+
* media. Chat operations travel as RPC frames on the WebSocket, so this SDK
|
|
219
|
+
* never calls Relyvo REST and never needs a CORS allowance.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const chat = new RelyvoChat(credentials);
|
|
224
|
+
* await chat.connect();
|
|
225
|
+
* chat.on("message", ({ message }) => console.log(message.content));
|
|
226
|
+
* await chat.send({ content: "hello", clientMessageId: crypto.randomUUID() });
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
declare const VERSION = "0.1.0";
|
|
231
|
+
|
|
232
|
+
export { type CallCredentials, type CallEvent, type CentrifugeClient, type CentrifugeFactory, type CentrifugeSubscription, type ChatChannel, type ChatCredentials, type ChatEventHandler, type ChatMessage, type ChatMessagePage, type ChatMessageType, type MessageEvent, RelyvoCall, type RelyvoCallOptions, RelyvoChat, type RelyvoChatOptions, RelyvoError, type RoomFactory, type RoomLike, type TypingEvent, VERSION, isRelyvoError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/chat.ts
|
|
6
|
+
import { Centrifuge } from "centrifuge";
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var RelyvoError = class extends Error {
|
|
10
|
+
constructor(code, message, cause) {
|
|
11
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
12
|
+
/** Stable machine code. RPC failures carry the chat service's code. */
|
|
13
|
+
__publicField(this, "code");
|
|
14
|
+
/** Transport-level cause, when there is one. */
|
|
15
|
+
__publicField(this, "cause");
|
|
16
|
+
this.name = "RelyvoError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.cause = cause;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function isRelyvoError(error) {
|
|
22
|
+
return error instanceof RelyvoError;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/chat.ts
|
|
26
|
+
var defaultFactory = (wsUrl, options) => new Centrifuge(wsUrl, {
|
|
27
|
+
token: options.token,
|
|
28
|
+
...options.getToken === void 0 ? {} : { getToken: options.getToken }
|
|
29
|
+
});
|
|
30
|
+
var RelyvoChat = class {
|
|
31
|
+
constructor(credentials, options = {}) {
|
|
32
|
+
__publicField(this, "credentials");
|
|
33
|
+
__publicField(this, "client");
|
|
34
|
+
__publicField(this, "connectTimeoutMs");
|
|
35
|
+
__publicField(this, "getSubscriptionToken");
|
|
36
|
+
__publicField(this, "handlers", {
|
|
37
|
+
connected: /* @__PURE__ */ new Set(),
|
|
38
|
+
disconnected: /* @__PURE__ */ new Set(),
|
|
39
|
+
message: /* @__PURE__ */ new Set(),
|
|
40
|
+
typing: /* @__PURE__ */ new Set(),
|
|
41
|
+
error: /* @__PURE__ */ new Set()
|
|
42
|
+
});
|
|
43
|
+
__publicField(this, "typingSubscription");
|
|
44
|
+
__publicField(this, "conversationSubscription");
|
|
45
|
+
this.credentials = credentials;
|
|
46
|
+
this.connectTimeoutMs = options.connectTimeoutMs ?? 1e4;
|
|
47
|
+
this.getSubscriptionToken = options.getSubscriptionToken;
|
|
48
|
+
this.client = (options.createCentrifuge ?? defaultFactory)(credentials.wsUrl, {
|
|
49
|
+
token: credentials.token,
|
|
50
|
+
...options.getConnectionToken === void 0 ? {} : { getToken: options.getConnectionToken }
|
|
51
|
+
});
|
|
52
|
+
this.client.on("connected", () => this.emit("connected"));
|
|
53
|
+
this.client.on("disconnected", (ctx) => this.emit("disconnected", ctx?.reason));
|
|
54
|
+
this.client.on(
|
|
55
|
+
"error",
|
|
56
|
+
(ctx) => this.emit("error", new RelyvoError("transport_error", String(ctx?.error ?? "connection error")))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
/** Opens the socket and subscribes to the conversation and typing channels. */
|
|
60
|
+
async connect() {
|
|
61
|
+
const connected = new Promise((resolve, reject) => {
|
|
62
|
+
const timer = setTimeout(
|
|
63
|
+
() => reject(new RelyvoError("connect_timeout", "the realtime connection timed out")),
|
|
64
|
+
this.connectTimeoutMs
|
|
65
|
+
);
|
|
66
|
+
this.handlers.connected.add(() => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
this.handlers.error.add((error) => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(error);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
this.client.connect();
|
|
76
|
+
await connected;
|
|
77
|
+
this.conversationSubscription = this.client.newSubscription(
|
|
78
|
+
this.credentials.channel,
|
|
79
|
+
this.subscriptionTokenOptions(this.credentials.channel, this.credentials.subscriptionToken)
|
|
80
|
+
);
|
|
81
|
+
this.conversationSubscription.on("publication", (ctx) => this.handlePublication(ctx?.data));
|
|
82
|
+
await this.conversationSubscription.subscribe();
|
|
83
|
+
if (this.credentials.typingChannel && this.credentials.typingSubscriptionToken) {
|
|
84
|
+
this.typingSubscription = this.client.newSubscription(
|
|
85
|
+
this.credentials.typingChannel,
|
|
86
|
+
this.subscriptionTokenOptions(
|
|
87
|
+
this.credentials.typingChannel,
|
|
88
|
+
this.credentials.typingSubscriptionToken
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
this.typingSubscription.on("publication", (ctx) => this.handleTyping(ctx?.data));
|
|
92
|
+
await this.typingSubscription.subscribe();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
on(event, handler) {
|
|
96
|
+
this.handlers[event].add(handler);
|
|
97
|
+
return () => this.handlers[event].delete(handler);
|
|
98
|
+
}
|
|
99
|
+
/** Sends a message. The same clientMessageId returns the original message. */
|
|
100
|
+
async send(input) {
|
|
101
|
+
return this.rpc("chat.send", {
|
|
102
|
+
channel_id: this.channelId,
|
|
103
|
+
client_message_id: input.clientMessageId,
|
|
104
|
+
content: input.content,
|
|
105
|
+
type: input.type ?? "text",
|
|
106
|
+
...input.metadata === void 0 ? {} : { metadata: input.metadata }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Reads history backwards from a cursor, or forwards with `direction: "after"`. */
|
|
110
|
+
history(input = {}) {
|
|
111
|
+
return this.rpc("chat.history", {
|
|
112
|
+
channel_id: this.channelId,
|
|
113
|
+
...input.before === void 0 ? {} : { cursor: input.before },
|
|
114
|
+
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
115
|
+
...input.direction === void 0 ? {} : { direction: input.direction }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** Marks the channel read up to a message. */
|
|
119
|
+
async markRead(messageId) {
|
|
120
|
+
await this.rpc("chat.read", { channel_id: this.channelId, message_id: messageId });
|
|
121
|
+
}
|
|
122
|
+
/** Lists the channels this user belongs to. */
|
|
123
|
+
channels(input = {}) {
|
|
124
|
+
return this.rpc("chat.channels", input).then((page) => page.items ?? []);
|
|
125
|
+
}
|
|
126
|
+
disconnect() {
|
|
127
|
+
this.conversationSubscription?.unsubscribe();
|
|
128
|
+
this.typingSubscription?.unsubscribe();
|
|
129
|
+
this.client.disconnect();
|
|
130
|
+
}
|
|
131
|
+
/** Channel id extracted from the subscription channel name. */
|
|
132
|
+
get channelId() {
|
|
133
|
+
const parts = this.credentials.channel.split(":");
|
|
134
|
+
const id = parts[parts.length - 1];
|
|
135
|
+
if (!id) {
|
|
136
|
+
throw new RelyvoError("invalid_credentials", "the channel name does not contain an id");
|
|
137
|
+
}
|
|
138
|
+
return id;
|
|
139
|
+
}
|
|
140
|
+
subscriptionTokenOptions(channel, token) {
|
|
141
|
+
return {
|
|
142
|
+
token,
|
|
143
|
+
...this.getSubscriptionToken === void 0 ? {} : { getToken: () => this.getSubscriptionToken(channel) }
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async rpc(method, data) {
|
|
147
|
+
let response;
|
|
148
|
+
try {
|
|
149
|
+
response = await this.client.rpc(method, data);
|
|
150
|
+
} catch (cause) {
|
|
151
|
+
throw new RelyvoError("transport_error", `rpc ${method} failed`, cause);
|
|
152
|
+
}
|
|
153
|
+
const envelope = response?.data;
|
|
154
|
+
if (!envelope || envelope.ok !== true) {
|
|
155
|
+
throw new RelyvoError(
|
|
156
|
+
envelope?.error?.code ?? "rpc_failed",
|
|
157
|
+
envelope?.error?.message ?? `rpc ${method} failed`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return envelope.data;
|
|
161
|
+
}
|
|
162
|
+
handlePublication(payload) {
|
|
163
|
+
if (!payload || payload.type !== "chat.message.created") return;
|
|
164
|
+
const data = payload.data ?? {};
|
|
165
|
+
if (data.channel_id !== this.channelId || !data.message) return;
|
|
166
|
+
const event = {
|
|
167
|
+
message: data.message,
|
|
168
|
+
channelId: data.channel_id,
|
|
169
|
+
senderId: data.sender?.id ?? data.message.sender_id,
|
|
170
|
+
senderName: data.sender?.display_name
|
|
171
|
+
};
|
|
172
|
+
this.emit("message", event);
|
|
173
|
+
}
|
|
174
|
+
handleTyping(payload) {
|
|
175
|
+
if (!payload || payload.type !== "chat.typing") return;
|
|
176
|
+
const data = payload.data ?? {};
|
|
177
|
+
if (data.channel_id !== this.channelId || !data.user_id) return;
|
|
178
|
+
const event = {
|
|
179
|
+
channel_id: data.channel_id,
|
|
180
|
+
user_id: data.user_id,
|
|
181
|
+
display_name: data.display_name
|
|
182
|
+
};
|
|
183
|
+
this.emit("typing", event);
|
|
184
|
+
}
|
|
185
|
+
emit(event, ...args) {
|
|
186
|
+
for (const handler of this.handlers[event]) {
|
|
187
|
+
handler(...args);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/video.ts
|
|
193
|
+
import { Room, RoomEvent } from "livekit-client";
|
|
194
|
+
var defaultFactory2 = () => new Room({ adaptiveStream: true, dynacast: true });
|
|
195
|
+
var EVENT_NAMES = {
|
|
196
|
+
connected: RoomEvent.Connected,
|
|
197
|
+
disconnected: RoomEvent.Disconnected,
|
|
198
|
+
participantConnected: RoomEvent.ParticipantConnected,
|
|
199
|
+
participantDisconnected: RoomEvent.ParticipantDisconnected,
|
|
200
|
+
trackSubscribed: RoomEvent.TrackSubscribed,
|
|
201
|
+
trackUnsubscribed: RoomEvent.TrackUnsubscribed,
|
|
202
|
+
connectionQualityChanged: RoomEvent.ConnectionQualityChanged,
|
|
203
|
+
mediaDevicesError: RoomEvent.MediaDevicesError
|
|
204
|
+
};
|
|
205
|
+
var RelyvoCall = class _RelyvoCall {
|
|
206
|
+
constructor(room) {
|
|
207
|
+
__publicField(this, "room");
|
|
208
|
+
__publicField(this, "connected", false);
|
|
209
|
+
this.room = room;
|
|
210
|
+
}
|
|
211
|
+
/** Connects to the room the token was minted for. */
|
|
212
|
+
static async connect(credentials, options = {}) {
|
|
213
|
+
if (!credentials.serverUrl || !credentials.token) {
|
|
214
|
+
throw new RelyvoError("invalid_credentials", "serverUrl and token are required");
|
|
215
|
+
}
|
|
216
|
+
const call = new _RelyvoCall((options.createRoom ?? defaultFactory2)());
|
|
217
|
+
try {
|
|
218
|
+
await call.room.connect(credentials.serverUrl, credentials.token);
|
|
219
|
+
} catch (cause) {
|
|
220
|
+
throw new RelyvoError("connect_failed", "the media connection could not be established", cause);
|
|
221
|
+
}
|
|
222
|
+
call.connected = true;
|
|
223
|
+
return call;
|
|
224
|
+
}
|
|
225
|
+
/** Publishes or stops the camera. */
|
|
226
|
+
async enableCamera(enabled = true) {
|
|
227
|
+
this.requireConnected();
|
|
228
|
+
await this.room.localParticipant.setCameraEnabled(enabled);
|
|
229
|
+
}
|
|
230
|
+
/** Publishes or stops the microphone. */
|
|
231
|
+
async enableMicrophone(enabled = true) {
|
|
232
|
+
this.requireConnected();
|
|
233
|
+
await this.room.localParticipant.setMicrophoneEnabled(enabled);
|
|
234
|
+
}
|
|
235
|
+
/** Starts or stops screen sharing where the browser supports it. */
|
|
236
|
+
async shareScreen(enabled = true) {
|
|
237
|
+
this.requireConnected();
|
|
238
|
+
if (enabled && typeof navigator !== "undefined" && !navigator.mediaDevices?.getDisplayMedia) {
|
|
239
|
+
throw new RelyvoError("unsupported", "this browser does not support screen sharing");
|
|
240
|
+
}
|
|
241
|
+
await this.room.localParticipant.setScreenShareEnabled(enabled);
|
|
242
|
+
}
|
|
243
|
+
/** Subscribes to a room event. Returns an unsubscribe function. */
|
|
244
|
+
on(event, handler) {
|
|
245
|
+
const name = EVENT_NAMES[event];
|
|
246
|
+
this.room.on(name, handler);
|
|
247
|
+
return () => {
|
|
248
|
+
const target = this.room;
|
|
249
|
+
target.off?.(name, handler);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/** Leaves the room and releases the local devices. */
|
|
253
|
+
disconnect() {
|
|
254
|
+
if (!this.connected) return;
|
|
255
|
+
this.connected = false;
|
|
256
|
+
void this.room.disconnect();
|
|
257
|
+
}
|
|
258
|
+
requireConnected() {
|
|
259
|
+
if (!this.connected) {
|
|
260
|
+
throw new RelyvoError("not_connected", "the call is not connected");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/index.ts
|
|
266
|
+
var VERSION = "0.1.0";
|
|
267
|
+
export {
|
|
268
|
+
RelyvoCall,
|
|
269
|
+
RelyvoChat,
|
|
270
|
+
RelyvoError,
|
|
271
|
+
VERSION,
|
|
272
|
+
isRelyvoError
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/chat.ts","../src/errors.ts","../src/video.ts","../src/index.ts"],"sourcesContent":["import { Centrifuge } from \"centrifuge\";\n\nimport { RelyvoError } from \"./errors.js\";\nimport type {\n ChatChannel,\n ChatCredentials,\n ChatEventHandler,\n ChatMessage,\n ChatMessagePage,\n MessageEvent,\n TypingEvent,\n} from \"./types.js\";\n\n/** The subset of the Centrifuge client this SDK depends on. */\nexport interface CentrifugeSubscription {\n subscribe(): Promise<unknown>;\n unsubscribe(): void;\n on(event: \"publication\" | \"subscribed\" | \"error\", handler: (ctx: any) => void): void;\n}\n\n/** Centrifuge fetches a replacement token through getToken when one expires. */\nexport interface ConnectionTokenOptions {\n token?: string;\n getToken?: () => Promise<string>;\n}\n\nexport interface SubscriptionTokenOptions {\n token?: string;\n getToken?: () => Promise<string>;\n}\n\nexport interface CentrifugeClient {\n connect(): void;\n disconnect(): void;\n on(event: \"connected\" | \"disconnected\" | \"error\", handler: (ctx: any) => void): void;\n newSubscription(channel: string, options: SubscriptionTokenOptions): CentrifugeSubscription;\n rpc(method: string, data: unknown): Promise<{ data: unknown }>;\n}\n\nexport type CentrifugeFactory = (wsUrl: string, options: ConnectionTokenOptions) => CentrifugeClient;\n\nconst defaultFactory: CentrifugeFactory = (wsUrl, options) =>\n new Centrifuge(wsUrl, {\n token: options.token,\n ...(options.getToken === undefined ? {} : { getToken: options.getToken }),\n }) as unknown as CentrifugeClient;\n\nexport interface RelyvoChatOptions {\n /** Injectable transport, used by tests. */\n createCentrifuge?: CentrifugeFactory;\n connectTimeoutMs?: number;\n /**\n * Called when the connection token expires. Return a fresh Centrifugo\n * connection token from your backend; the transport reconnects by itself.\n */\n getConnectionToken?: () => Promise<string>;\n /**\n * Called when a subscription token expires. Receive the channel name and\n * return a fresh subscription token for it.\n */\n getSubscriptionToken?: (channel: string) => Promise<string>;\n}\n\ntype RpcEnvelope<T> = {\n ok?: boolean;\n data?: T;\n error?: { code?: string; message?: string };\n};\n\n/**\n * Realtime chat over the Centrifugo connection.\n *\n * Every operation is an RPC frame on the WebSocket the client already holds, so\n * the browser never calls Relyvo REST and needs no CORS allowance. Identity comes\n * from the connection token; this class never sends a project or user id.\n */\nexport class RelyvoChat {\n readonly credentials: ChatCredentials;\n\n private readonly client: CentrifugeClient;\n private readonly connectTimeoutMs: number;\n private readonly getSubscriptionToken?: (channel: string) => Promise<string>;\n private readonly handlers: { [K in keyof ChatEventHandler]: Set<ChatEventHandler[K]> } = {\n connected: new Set(),\n disconnected: new Set(),\n message: new Set(),\n typing: new Set(),\n error: new Set(),\n };\n private typingSubscription?: CentrifugeSubscription;\n private conversationSubscription?: CentrifugeSubscription;\n\n constructor(credentials: ChatCredentials, options: RelyvoChatOptions = {}) {\n this.credentials = credentials;\n this.connectTimeoutMs = options.connectTimeoutMs ?? 10_000;\n this.getSubscriptionToken = options.getSubscriptionToken;\n this.client = (options.createCentrifuge ?? defaultFactory)(credentials.wsUrl, {\n token: credentials.token,\n ...(options.getConnectionToken === undefined\n ? {}\n : { getToken: options.getConnectionToken }),\n });\n\n this.client.on(\"connected\", () => this.emit(\"connected\"));\n this.client.on(\"disconnected\", (ctx) => this.emit(\"disconnected\", ctx?.reason));\n this.client.on(\"error\", (ctx) =>\n this.emit(\"error\", new RelyvoError(\"transport_error\", String(ctx?.error ?? \"connection error\"))),\n );\n }\n\n /** Opens the socket and subscribes to the conversation and typing channels. */\n async connect(): Promise<void> {\n const connected = new Promise<void>((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new RelyvoError(\"connect_timeout\", \"the realtime connection timed out\")),\n this.connectTimeoutMs,\n );\n\n this.handlers.connected.add(() => {\n clearTimeout(timer);\n resolve();\n });\n this.handlers.error.add((error) => {\n clearTimeout(timer);\n reject(error);\n });\n });\n\n this.client.connect();\n await connected;\n\n this.conversationSubscription = this.client.newSubscription(\n this.credentials.channel,\n this.subscriptionTokenOptions(this.credentials.channel, this.credentials.subscriptionToken),\n );\n this.conversationSubscription.on(\"publication\", (ctx) => this.handlePublication(ctx?.data));\n await this.conversationSubscription.subscribe();\n\n if (this.credentials.typingChannel && this.credentials.typingSubscriptionToken) {\n this.typingSubscription = this.client.newSubscription(\n this.credentials.typingChannel,\n this.subscriptionTokenOptions(\n this.credentials.typingChannel,\n this.credentials.typingSubscriptionToken,\n ),\n );\n this.typingSubscription.on(\"publication\", (ctx) => this.handleTyping(ctx?.data));\n await this.typingSubscription.subscribe();\n }\n }\n\n on<K extends keyof ChatEventHandler>(event: K, handler: ChatEventHandler[K]): () => void {\n this.handlers[event].add(handler);\n return () => this.handlers[event].delete(handler);\n }\n\n /** Sends a message. The same clientMessageId returns the original message. */\n async send(input: {\n content: string;\n clientMessageId: string;\n type?: string;\n metadata?: Record<string, unknown>;\n }): Promise<{ message: ChatMessage; created: boolean }> {\n return this.rpc(\"chat.send\", {\n channel_id: this.channelId,\n client_message_id: input.clientMessageId,\n content: input.content,\n type: input.type ?? \"text\",\n ...(input.metadata === undefined ? {} : { metadata: input.metadata }),\n });\n }\n\n /** Reads history backwards from a cursor, or forwards with `direction: \"after\"`. */\n history(input: { before?: string; limit?: number; direction?: \"before\" | \"after\" } = {}): Promise<ChatMessagePage> {\n return this.rpc(\"chat.history\", {\n channel_id: this.channelId,\n ...(input.before === undefined ? {} : { cursor: input.before }),\n ...(input.limit === undefined ? {} : { limit: input.limit }),\n ...(input.direction === undefined ? {} : { direction: input.direction }),\n });\n }\n\n /** Marks the channel read up to a message. */\n async markRead(messageId: string): Promise<void> {\n await this.rpc(\"chat.read\", { channel_id: this.channelId, message_id: messageId });\n }\n\n /** Lists the channels this user belongs to. */\n channels(input: { cursor?: string; limit?: number } = {}): Promise<ChatChannel[]> {\n return this.rpc<{ items: ChatChannel[] }>(\"chat.channels\", input).then((page) => page.items ?? []);\n }\n\n disconnect(): void {\n this.conversationSubscription?.unsubscribe();\n this.typingSubscription?.unsubscribe();\n this.client.disconnect();\n }\n\n /** Channel id extracted from the subscription channel name. */\n private get channelId(): string {\n const parts = this.credentials.channel.split(\":\");\n const id = parts[parts.length - 1];\n if (!id) {\n throw new RelyvoError(\"invalid_credentials\", \"the channel name does not contain an id\");\n }\n return id;\n }\n\n private subscriptionTokenOptions(\n channel: string,\n token: string,\n ): SubscriptionTokenOptions {\n return {\n token,\n ...(this.getSubscriptionToken === undefined\n ? {}\n : { getToken: () => this.getSubscriptionToken!(channel) }),\n };\n }\n\n private async rpc<T>(method: string, data: unknown): Promise<T> {\n let response: { data: unknown };\n try {\n response = await this.client.rpc(method, data);\n } catch (cause) {\n throw new RelyvoError(\"transport_error\", `rpc ${method} failed`, cause);\n }\n\n const envelope = response?.data as RpcEnvelope<T> | undefined;\n if (!envelope || envelope.ok !== true) {\n throw new RelyvoError(\n envelope?.error?.code ?? \"rpc_failed\",\n envelope?.error?.message ?? `rpc ${method} failed`,\n );\n }\n\n return envelope.data as T;\n }\n\n private handlePublication(payload: any): void {\n if (!payload || payload.type !== \"chat.message.created\") return;\n\n const data = payload.data ?? {};\n if (data.channel_id !== this.channelId || !data.message) return;\n\n const event: MessageEvent = {\n message: data.message,\n channelId: data.channel_id,\n senderId: data.sender?.id ?? data.message.sender_id,\n senderName: data.sender?.display_name,\n };\n this.emit(\"message\", event);\n }\n\n private handleTyping(payload: any): void {\n if (!payload || payload.type !== \"chat.typing\") return;\n\n const data = payload.data ?? {};\n if (data.channel_id !== this.channelId || !data.user_id) return;\n\n const event: TypingEvent = {\n channel_id: data.channel_id,\n user_id: data.user_id,\n display_name: data.display_name,\n };\n this.emit(\"typing\", event);\n }\n\n private emit<K extends keyof ChatEventHandler>(event: K, ...args: Parameters<ChatEventHandler[K]>): void {\n for (const handler of this.handlers[event]) {\n (handler as (...values: unknown[]) => void)(...args);\n }\n }\n}\n","/** One error type for every failure this SDK produces. */\nexport class RelyvoError extends Error {\n /** Stable machine code. RPC failures carry the chat service's code. */\n readonly code: string;\n /** Transport-level cause, when there is one. */\n readonly cause?: unknown;\n\n constructor(code: string, message: string, cause?: unknown) {\n super(message, cause === undefined ? undefined : { cause });\n this.name = \"RelyvoError\";\n this.code = code;\n this.cause = cause;\n }\n}\n\nexport function isRelyvoError(error: unknown): error is RelyvoError {\n return error instanceof RelyvoError;\n}\n","import { Room, RoomEvent } from \"livekit-client\";\n\nimport { RelyvoError } from \"./errors.js\";\nimport type { CallCredentials, CallEvent } from \"./types.js\";\n\n/** The subset of the LiveKit Room this SDK depends on. */\nexport interface RoomLike {\n connect(url: string, token: string): Promise<void>;\n disconnect(): Promise<void> | void;\n on(event: string, handler: (...args: any[]) => void): unknown;\n localParticipant: {\n setCameraEnabled(enabled: boolean): Promise<void>;\n setMicrophoneEnabled(enabled: boolean): Promise<void>;\n setScreenShareEnabled(enabled: boolean): Promise<void>;\n };\n}\n\nexport type RoomFactory = () => RoomLike;\n\nconst defaultFactory: RoomFactory = () =>\n new Room({ adaptiveStream: true, dynacast: true }) as unknown as RoomLike;\n\n/** LiveKit event names, mapped so callers do not import the media library. */\nconst EVENT_NAMES: Record<CallEvent, RoomEvent> = {\n connected: RoomEvent.Connected,\n disconnected: RoomEvent.Disconnected,\n participantConnected: RoomEvent.ParticipantConnected,\n participantDisconnected: RoomEvent.ParticipantDisconnected,\n trackSubscribed: RoomEvent.TrackSubscribed,\n trackUnsubscribed: RoomEvent.TrackUnsubscribed,\n connectionQualityChanged: RoomEvent.ConnectionQualityChanged,\n mediaDevicesError: RoomEvent.MediaDevicesError,\n};\n\nexport interface RelyvoCallOptions {\n /** Injectable room, used by tests. */\n createRoom?: RoomFactory;\n}\n\n/**\n * One video or voice call.\n *\n * The participant token is minted by your backend with the Relyvo server SDK; the\n * grant is join-only and already scoped to the room, so this class cannot ask for\n * more than that token allows.\n */\nexport class RelyvoCall {\n readonly room: RoomLike;\n private connected = false;\n\n private constructor(room: RoomLike) {\n this.room = room;\n }\n\n /** Connects to the room the token was minted for. */\n static async connect(\n credentials: CallCredentials,\n options: RelyvoCallOptions = {},\n ): Promise<RelyvoCall> {\n if (!credentials.serverUrl || !credentials.token) {\n throw new RelyvoError(\"invalid_credentials\", \"serverUrl and token are required\");\n }\n\n const call = new RelyvoCall((options.createRoom ?? defaultFactory)());\n\n try {\n await call.room.connect(credentials.serverUrl, credentials.token);\n } catch (cause) {\n throw new RelyvoError(\"connect_failed\", \"the media connection could not be established\", cause);\n }\n\n call.connected = true;\n return call;\n }\n\n /** Publishes or stops the camera. */\n async enableCamera(enabled = true): Promise<void> {\n this.requireConnected();\n await this.room.localParticipant.setCameraEnabled(enabled);\n }\n\n /** Publishes or stops the microphone. */\n async enableMicrophone(enabled = true): Promise<void> {\n this.requireConnected();\n await this.room.localParticipant.setMicrophoneEnabled(enabled);\n }\n\n /** Starts or stops screen sharing where the browser supports it. */\n async shareScreen(enabled = true): Promise<void> {\n this.requireConnected();\n if (enabled && typeof navigator !== \"undefined\" && !navigator.mediaDevices?.getDisplayMedia) {\n throw new RelyvoError(\"unsupported\", \"this browser does not support screen sharing\");\n }\n await this.room.localParticipant.setScreenShareEnabled(enabled);\n }\n\n /** Subscribes to a room event. Returns an unsubscribe function. */\n on(event: CallEvent, handler: (...args: any[]) => void): () => void {\n const name = EVENT_NAMES[event];\n this.room.on(name, handler);\n return () => {\n const target = this.room as unknown as {\n off?: (event: string, callback: (...args: any[]) => void) => void;\n };\n target.off?.(name, handler);\n };\n }\n\n /** Leaves the room and releases the local devices. */\n disconnect(): void {\n if (!this.connected) return;\n this.connected = false;\n void this.room.disconnect();\n }\n\n private requireConnected(): void {\n if (!this.connected) {\n throw new RelyvoError(\"not_connected\", \"the call is not connected\");\n }\n }\n}\n","/**\n * Relyvo browser client SDK.\n *\n * The browser holds only short-lived credentials minted by your backend: a\n * Centrifugo connection token for chat and a LiveKit participant token for\n * media. Chat operations travel as RPC frames on the WebSocket, so this SDK\n * never calls Relyvo REST and never needs a CORS allowance.\n *\n * @example\n * ```ts\n * const chat = new RelyvoChat(credentials);\n * await chat.connect();\n * chat.on(\"message\", ({ message }) => console.log(message.content));\n * await chat.send({ content: \"hello\", clientMessageId: crypto.randomUUID() });\n * ```\n */\n\nexport { RelyvoChat } from \"./chat.js\";\nexport type {\n CentrifugeClient,\n CentrifugeFactory,\n CentrifugeSubscription,\n RelyvoChatOptions,\n} from \"./chat.js\";\nexport { RelyvoError, isRelyvoError } from \"./errors.js\";\nexport { RelyvoCall } from \"./video.js\";\nexport type { RelyvoCallOptions, RoomFactory, RoomLike } from \"./video.js\";\nexport type {\n CallCredentials,\n CallEvent,\n ChatChannel,\n ChatCredentials,\n ChatEventHandler,\n ChatMessage,\n ChatMessagePage,\n ChatMessageType,\n MessageEvent,\n TypingEvent,\n} from \"./types.js\";\n\nexport const VERSION = \"0.1.0\";\n"],"mappings":"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","names":["defaultFactory"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relyvolabs/web-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser client SDK for Relyvo: realtime chat over Centrifugo and video calls over LiveKit, with no REST and no API key in the browser.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"relyvo",
|
|
27
|
+
"rtc",
|
|
28
|
+
"chat",
|
|
29
|
+
"sdk",
|
|
30
|
+
"client",
|
|
31
|
+
"webrtc"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/relyvolabs/relyvo-web-sdk.git"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"pack:check": "npm pack --dry-run"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"centrifuge": ">=5",
|
|
45
|
+
"livekit-client": ">=2"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.2",
|
|
49
|
+
"centrifuge": "5.8.0",
|
|
50
|
+
"livekit-client": "2.23.0",
|
|
51
|
+
"tsup": "^8.3.5",
|
|
52
|
+
"typescript": "^5.7.2",
|
|
53
|
+
"vitest": "^2.1.8"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"pnpm": {
|
|
59
|
+
"onlyBuiltDependencies": [
|
|
60
|
+
"esbuild"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
}
|