@chitchat/sdk-web 0.2.0-dev.2 → 0.3.0-dev.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 +12 -2
- package/index.d.ts +4 -2
- package/package.json +55 -6
- package/react.d.ts +4 -0
- package/src/index.js +49 -35
- package/src/react.js +20 -0
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# ChitChat Web SDK
|
|
2
2
|
|
|
3
|
+
## 0.3.0-dev.0 source release
|
|
4
|
+
|
|
5
|
+
Adds createChitChatWebCalls and @chitchat/sdk-web/react (useCallState, CallVideo). Browser signalling remains binary protobuf over WSS; media uses WebRTC.
|
|
6
|
+
|
|
7
|
+
This version is **not yet published or production-certified**. See the [calling quickstart](../docs/CALLING.md), [all-five SDK audit](../docs/GAP_AUDIT.md), [migration/support targets](../docs/RELEASE.md), and [local tarball installation](../README.md). Existing npm development tags may still resolve to older SDKs.
|
|
8
|
+
|
|
9
|
+
|
|
3
10
|
`@chitchat/sdk-web` connects a browser to ChitChat realtime using secure WebSocket (**WSS**) and binary Protobuf frames. Browsers cannot use raw TCP, so this package never attempts it.
|
|
4
11
|
|
|
5
12
|
This package is currently a **development prerelease**. Pin the version you have tested before shipping an application.
|
|
@@ -65,7 +72,10 @@ Use `createRealtimeEnvelopeCodec({ deviceId, platform })`; it produces the hands
|
|
|
65
72
|
```js
|
|
66
73
|
const localMessageId = await client.send('chat.message', {
|
|
67
74
|
conversationId: 'conversation_123',
|
|
68
|
-
|
|
75
|
+
receiverId: recipientGatewayId,
|
|
76
|
+
ciphertext: encryptedPayload.ciphertext,
|
|
77
|
+
iv: encryptedPayload.iv,
|
|
78
|
+
authTag: encryptedPayload.authTag
|
|
69
79
|
});
|
|
70
80
|
|
|
71
81
|
client.on('ack', (messageId) => {
|
|
@@ -114,4 +124,4 @@ This local exception works only for `localhost`, `127.0.0.1`, or `::1`; never en
|
|
|
114
124
|
|
|
115
125
|
## Current scope
|
|
116
126
|
|
|
117
|
-
The
|
|
127
|
+
The 0.3 source release includes the calling additions described above. See the [gap audit](../docs/GAP_AUDIT.md) for supported behavior and remaining service requirements. Calling is a foreground small-mesh implementation. Server-authoritative rooms, roles, moderation, recording, signed call webhooks, SFU meetings, background native calling and device/browser production certification remain unimplemented or unverified.
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ReliableRealtimeClient, ReliableRealtimeClientOptions, RealtimeFrame } from '@chitchat/sdk-core';
|
|
2
|
-
export interface ProtobufCodec { encode(frame: RealtimeFrame): ArrayBuffer | ArrayBufferView; decode(binary: ArrayBuffer |
|
|
2
|
+
export interface ProtobufCodec { encode(frame: RealtimeFrame): ArrayBuffer | ArrayBufferView; decode(binary: ArrayBuffer | ArrayBufferView): RealtimeFrame; encodeAuth?(input: { token: string; protocol: 'chitchat.protobuf.v1' }): ArrayBuffer | ArrayBufferView; isAuthenticationAccepted(frame: RealtimeFrame): boolean; authenticationError?(frame: RealtimeFrame): string | null; openTimeoutMs?: number; maxBufferedAmount?: number; }
|
|
3
3
|
export function toWssEndpoint(endpoint: string, options?: { allowInsecureWs?: boolean }): string;
|
|
4
4
|
export function createWssTransportFactory(options: ProtobufCodec & { WebSocketImpl?: typeof WebSocket; allowInsecureWs?: boolean }): ReliableRealtimeClientOptions['webTransportFactory'];
|
|
5
|
-
export function createChitChatWebClient(options: Omit<ReliableRealtimeClientOptions, 'runtime' | 'webTransportFactory'> & { protobuf: ProtobufCodec; allowInsecureWs?: boolean }): ReliableRealtimeClient;
|
|
5
|
+
export function createChitChatWebClient(options: Omit<ReliableRealtimeClientOptions, 'runtime' | 'webTransportFactory'> & { protobuf: ProtobufCodec; WebSocketImpl?: typeof WebSocket; allowInsecureWs?: boolean }): ReliableRealtimeClient;
|
|
6
|
+
|
|
7
|
+
export function createChitChatWebCalls(options: Omit<import('@chitchat/sdk-core').CallsOptions, 'rtc'> & { rtc?: import('@chitchat/sdk-core').WebRtcRuntime }): import('@chitchat/sdk-core').CallsClient;
|
package/package.json
CHANGED
|
@@ -1,12 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chitchat/sdk-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-dev.0",
|
|
4
|
+
"description": "Browser WSS transport for the ChitChat Developer Platform",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/07Akashh/E2E-Chat.git",
|
|
8
|
+
"directory": "developer-web/sdk/chitchat-web"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/07Akashh/E2E-Chat/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/07Akashh/E2E-Chat/tree/main/docs/developer-platform",
|
|
4
14
|
"main": "src/index.js",
|
|
5
|
-
"exports": {
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./index.d.ts",
|
|
18
|
+
"require": "./src/index.js",
|
|
19
|
+
"import": "./src/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./react": {
|
|
22
|
+
"types": "./react.d.ts",
|
|
23
|
+
"require": "./src/react.js",
|
|
24
|
+
"import": "./src/react.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
6
27
|
"types": "index.d.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
|
|
9
|
-
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"index.d.ts",
|
|
31
|
+
"README.md",
|
|
32
|
+
"react.d.ts"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@chitchat/sdk-core": "0.3.0-dev.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"chitchat",
|
|
42
|
+
"realtime",
|
|
43
|
+
"websocket",
|
|
44
|
+
"wss",
|
|
45
|
+
"protobuf"
|
|
46
|
+
],
|
|
47
|
+
"sideEffects": false,
|
|
10
48
|
"license": "UNLICENSED",
|
|
11
|
-
"publishConfig": {
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"tag": "development"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"react": ">=18 <20"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"react": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
12
61
|
}
|
package/react.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CallsClient, CallSnapshot } from '@chitchat/sdk-core';
|
|
2
|
+
import type { ForwardRefExoticComponent, RefAttributes, VideoHTMLAttributes } from 'react';
|
|
3
|
+
export function useCallState(client: CallsClient): CallSnapshot;
|
|
4
|
+
export const CallVideo: ForwardRefExoticComponent<VideoHTMLAttributes<HTMLVideoElement> & RefAttributes<HTMLVideoElement> & { stream: MediaStream | null; onPlaybackError?(event: { code: 'PLAYBACK_BLOCKED' }): void }>;
|
package/src/index.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
let ReliableRealtimeClient;
|
|
4
4
|
try { ({ ReliableRealtimeClient } = require('@chitchat/sdk-core')); } catch (_) { ({ ReliableRealtimeClient } = require('../../chitchat-core/src')); }
|
|
5
5
|
|
|
6
|
-
const isLocalHttpUrl = (url) => ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
|
|
6
|
+
const isLocalHttpUrl = (url) => ['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname);
|
|
7
7
|
const toWssEndpoint = (endpoint, { allowInsecureWs = false } = {}) => {
|
|
8
8
|
let url;
|
|
9
9
|
try { url = new URL(endpoint); } catch (_) { throw new Error('A valid HTTPS or WSS endpoint is required'); }
|
|
10
|
-
if (url.username || url.password) throw new Error('Realtime endpoint must not include credentials');
|
|
10
|
+
if (url.username || url.password || url.search || url.hash) throw new Error('Realtime endpoint must not include credentials');
|
|
11
11
|
if (url.protocol === 'https:') url.protocol = 'wss:';
|
|
12
12
|
if (url.protocol === 'http:' && allowInsecureWs === true && isLocalHttpUrl(url)) url.protocol = 'ws:';
|
|
13
13
|
if (url.protocol !== 'wss:' && !(url.protocol === 'ws:' && allowInsecureWs === true && isLocalHttpUrl(url))) throw new Error('Web realtime requires WSS (WS is allowed only for explicit localhost development)');
|
|
@@ -20,63 +20,77 @@ const binaryFrame = (value, name) => {
|
|
|
20
20
|
return value;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const createWssTransportFactory = ({ WebSocketImpl = globalThis.WebSocket, encode, decode, encodeAuth, isAuthenticationAccepted, authenticationError, allowInsecureWs = false, openTimeoutMs = 15_000 }) => {
|
|
23
|
+
const createWssTransportFactory = ({ WebSocketImpl = globalThis.WebSocket, encode, decode, encodeAuth, isAuthenticationAccepted, authenticationError, allowInsecureWs = false, openTimeoutMs = 15_000, maxBufferedAmount = 1024 * 1024 }) => {
|
|
24
24
|
if (!WebSocketImpl || typeof encode !== 'function' || typeof decode !== 'function') throw new Error('WebSocket implementation and protobuf encode/decode functions are required');
|
|
25
|
-
if (typeof
|
|
25
|
+
if (typeof isAuthenticationAccepted !== 'function') throw new Error('isAuthenticationAccepted is required so the SDK can verify the realtime handshake');
|
|
26
|
+
if (!Number.isInteger(openTimeoutMs) || openTimeoutMs < 100 || openTimeoutMs > 120_000) throw new Error('openTimeoutMs must be between 100 and 120000');
|
|
27
|
+
if (!Number.isInteger(maxBufferedAmount) || maxBufferedAmount < 1 || maxBufferedAmount > 16 * 1024 * 1024) throw new Error('maxBufferedAmount is invalid');
|
|
26
28
|
return async ({ endpoint, token, protocol }) => {
|
|
27
|
-
let socket; let onMessage = () => {}; let onClose = () => {}; let opened = false; let
|
|
28
|
-
const
|
|
29
|
-
const
|
|
29
|
+
let socket; let onMessage = () => {}; let onClose = () => {}; let opened = false; let closed = false; let notified = false; let timer; let rejectOpen;
|
|
30
|
+
const notifyClose = (reason) => { if (!notified) { notified = true; onClose(reason); } };
|
|
31
|
+
const shutdown = (error, code = 'TRANSPORT_CLOSED') => {
|
|
32
|
+
if (closed) return;
|
|
33
|
+
closed = true; opened = false; clearTimeout(timer);
|
|
34
|
+
rejectOpen?.(error || new Error('WSS transport closed'));
|
|
35
|
+
if (socket) { socket.onopen = socket.onmessage = socket.onerror = socket.onclose = null; try { socket.close(); } catch (_) {} }
|
|
36
|
+
notifyClose({ code });
|
|
37
|
+
};
|
|
30
38
|
return {
|
|
31
39
|
onMessage: (listener) => { onMessage = listener; },
|
|
32
40
|
onClose: (listener) => { onClose = listener; },
|
|
33
41
|
open: () => new Promise((resolve, reject) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const finish = (handler, value) => { if (settled) return; settled = true; clearTimeout(timer); handler(value); };
|
|
42
|
+
if (closed || socket) { reject(new Error('WSS transport cannot be reopened')); return; }
|
|
43
|
+
rejectOpen = reject;
|
|
37
44
|
try {
|
|
38
45
|
socket = new WebSocketImpl(toWssEndpoint(endpoint, { allowInsecureWs }), protocol);
|
|
39
46
|
socket.binaryType = 'arraybuffer';
|
|
40
|
-
timer = setTimeout(() =>
|
|
47
|
+
timer = setTimeout(() => shutdown(new Error('WSS connection timed out'), 'CONNECTION_TIMEOUT'), openTimeoutMs);
|
|
41
48
|
socket.onopen = () => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
socket.onerror = () => finish(reject, new Error('WSS connection failed'));
|
|
46
|
-
socket.onclose = (event = {}) => {
|
|
47
|
-
if (!opened) finish(reject, new Error(event.reason || 'WSS connection closed before opening'));
|
|
48
|
-
onClose({ code: event.code, reason: event.reason });
|
|
49
|
+
if (closed) return;
|
|
50
|
+
try { socket.send(binaryFrame(encodeAuth ? encodeAuth({ token, protocol }) : encode({ type: 'auth', token, protocol }), 'protobuf authentication encoder')); }
|
|
51
|
+
catch (error) { shutdown(error, 'PROTOCOL_ENCODE_ERROR'); }
|
|
49
52
|
};
|
|
53
|
+
socket.onerror = () => shutdown(new Error('WSS connection failed'), 'CONNECTION_FAILED');
|
|
54
|
+
socket.onclose = (event = {}) => shutdown(new Error('WSS connection closed'), event.code || 'CONNECTION_CLOSED');
|
|
50
55
|
socket.onmessage = (event) => {
|
|
56
|
+
if (closed) return;
|
|
51
57
|
try {
|
|
58
|
+
binaryFrame(event.data, 'incoming message');
|
|
52
59
|
const frame = decode(event.data);
|
|
53
|
-
if (
|
|
60
|
+
if (!opened) {
|
|
54
61
|
const rejection = typeof authenticationError === 'function' ? authenticationError(frame) : null;
|
|
55
|
-
if (rejection) {
|
|
56
|
-
if (isAuthenticationAccepted(frame)) {
|
|
62
|
+
if (rejection) { shutdown(new Error(rejection), 'AUTHENTICATION_REJECTED'); return; }
|
|
63
|
+
if (!isAuthenticationAccepted(frame)) { shutdown(new Error('Unexpected frame before authentication'), 'AUTHENTICATION_REQUIRED'); return; }
|
|
64
|
+
opened = true; clearTimeout(timer); rejectOpen = null; resolve();
|
|
65
|
+
// Core transitions to connected in the open() continuation.
|
|
66
|
+
queueMicrotask(() => { if (!closed) onMessage(frame); });
|
|
67
|
+
return;
|
|
57
68
|
}
|
|
58
69
|
onMessage(frame);
|
|
59
|
-
} catch (_) {
|
|
70
|
+
} catch (_) { shutdown(new Error('Invalid Protobuf frame'), 'PROTOCOL_DECODE_ERROR'); }
|
|
60
71
|
};
|
|
61
|
-
} catch (error) {
|
|
72
|
+
} catch (error) { shutdown(error, 'CONNECTION_FAILED'); }
|
|
62
73
|
}),
|
|
63
|
-
send: (frame) => {
|
|
64
|
-
if (!socket || socket.readyState !== 1)
|
|
65
|
-
|
|
74
|
+
send: async (frame) => {
|
|
75
|
+
if (!opened || closed || !socket || socket.readyState !== 1) throw new Error('WSS transport is not authenticated');
|
|
76
|
+
const bytes = binaryFrame(encode(frame), 'protobuf.encode');
|
|
77
|
+
if ((socket.bufferedAmount || 0) + bytes.byteLength > maxBufferedAmount) throw new Error('WSS backpressure limit exceeded');
|
|
78
|
+
socket.send(bytes);
|
|
66
79
|
},
|
|
67
|
-
close: () =>
|
|
68
|
-
if (!socket || socket.readyState === 3) return resolve();
|
|
69
|
-
const current = socket.onclose;
|
|
70
|
-
socket.onclose = (event) => { if (typeof current === 'function') current(event); resolve(); };
|
|
71
|
-
try { socket.close(); } catch (_) { resolve(); }
|
|
72
|
-
})
|
|
80
|
+
close: async () => shutdown()
|
|
73
81
|
};
|
|
74
82
|
};
|
|
75
83
|
};
|
|
76
84
|
|
|
77
|
-
const createChitChatWebClient = ({ endpoint, tokenProvider, protobuf, allowInsecureWs = false, ...options }) => {
|
|
85
|
+
const createChitChatWebClient = ({ endpoint, tokenProvider, protobuf, WebSocketImpl, allowInsecureWs = false, ...options }) => {
|
|
78
86
|
if (!protobuf || typeof protobuf !== 'object') throw new Error('protobuf codec is required');
|
|
79
|
-
return new ReliableRealtimeClient({ endpoint, tokenProvider, runtime: 'web', webTransportFactory: createWssTransportFactory({ ...protobuf, allowInsecureWs }), ...options });
|
|
87
|
+
return new ReliableRealtimeClient({ endpoint, tokenProvider, runtime: 'web', webTransportFactory: createWssTransportFactory({ ...protobuf, WebSocketImpl, allowInsecureWs }), ...options });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const createChitChatWebCalls = ({ rtc, ...options } = {}) => {
|
|
91
|
+
const { CallsClient } = requireCore();
|
|
92
|
+
return new CallsClient({ ...options, rtc: rtc || { RTCPeerConnection: globalThis.RTCPeerConnection, MediaStream: globalThis.MediaStream, mediaDevices: globalThis.navigator?.mediaDevices } });
|
|
80
93
|
};
|
|
94
|
+
const requireCore = () => { try { return require('@chitchat/sdk-core'); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; return require('../../chitchat-core/src'); } };
|
|
81
95
|
|
|
82
|
-
module.exports = { createChitChatWebClient, createWssTransportFactory, toWssEndpoint };
|
|
96
|
+
module.exports = { createChitChatWebCalls, createChitChatWebClient, createWssTransportFactory, toWssEndpoint };
|
package/src/react.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const React = require('react');
|
|
3
|
+
function useCallState(client) {
|
|
4
|
+
const subscribe = React.useCallback((changed) => client.on('stateChange', changed), [client]);
|
|
5
|
+
const snapshot = React.useCallback(() => client.getSnapshot(), [client]);
|
|
6
|
+
return React.useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
7
|
+
}
|
|
8
|
+
/** Rendering only; acquiring media always requires an explicit user action. */
|
|
9
|
+
const CallVideo = React.forwardRef(function CallVideo({ stream, muted = false, onPlaybackError, ...props }, ref) {
|
|
10
|
+
const element = React.useRef(null);
|
|
11
|
+
React.useImperativeHandle(ref, () => element.current);
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
const video = element.current; if (!video) return;
|
|
14
|
+
video.srcObject = stream || null;
|
|
15
|
+
if (stream) video.play().catch(() => onPlaybackError?.({ code: 'PLAYBACK_BLOCKED' }));
|
|
16
|
+
return () => { if (video.srcObject === stream) video.srcObject = null; };
|
|
17
|
+
}, [stream, onPlaybackError]);
|
|
18
|
+
return React.createElement('video', { autoPlay: true, playsInline: true, ...props, muted, ref: element });
|
|
19
|
+
});
|
|
20
|
+
module.exports = { useCallState, CallVideo };
|