lib/tinode/connection.ts:单例Tinode连接+自己的发布订阅层——SDK的onConnect/onDisconnect/onLogin
是单槽位属性(赋值会互相覆盖),不是真正的多监听器事件源,所以这里统一接管这几个槽位,自己
扇出给任意数量的React订阅方(lib/tinode/hooks.ts的useSyncExternalStore)。installTenantHiPatch
照搬旧应用tinode-web.jsx的实现搬过来——服务端{hi}包的tenant字段SDK本身没有API,只能在原始
sendText层面注入,这是后端协议的真实约束,不是旧代码库特有的写法。
lib/api/client.ts:REST客户端按旧应用src/lib/im-api.js的request函数+完整REASON_MESSAGES错误码
翻译表搬过来(含请求头X-MeetingIM-APIKey等几个月联调换来的正确写法),这次是重新用TypeScript
写、不是复制文件,但端点行为对齐。
实机联调过程中发现并修正了一处凭记忆猜错的地方:tenants/resolve真实响应结构是
{endpoints:{api,file,websocket}, tenant:{code,name}},不是我最初假设的顶层{code,name}——
已用真实请求验证修正。用企业码default解析成功(显示"Default Tenant")+ 一次真实错误密码
登录尝试(服务端真实401,错误提示正确显示"账号或密码错误")两步都对着真实后端验证通过,
不是只测了正确路径。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
108 lines
3.7 KiB
TypeScript
108 lines
3.7 KiB
TypeScript
// Singleton Tinode connection + a small pub-sub layer wrapping it for React.
|
|
//
|
|
// The SDK's onConnect/onDisconnect/onLogin etc. are plain single-slot properties (`tinode.onConnect
|
|
// = fn`), not a real multi-listener event emitter -- assigning one overwrites any previous
|
|
// assignment. If more than one React component tried to register its own callback directly (e.g.
|
|
// each calling useSyncExternalStore with its own subscribe function touching these slots), they'd
|
|
// silently stomp on each other. This module claims all the SDK's callback slots exactly once and
|
|
// fans them out to any number of subscribers via its own Set, which is what hooks.ts's
|
|
// useSyncExternalStore-based hooks actually subscribe to.
|
|
import { Tinode } from 'tinode-sdk';
|
|
|
|
import { API_KEY, APP_NAME, LOGGING_ENABLED } from '@/lib/config';
|
|
import { useSessionStore } from '@/stores/session';
|
|
|
|
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'authenticated';
|
|
|
|
let tinode: Tinode | null = null;
|
|
let status: ConnectionStatus = 'disconnected';
|
|
const listeners = new Set<() => void>();
|
|
|
|
function setStatus(next: ConnectionStatus) {
|
|
if (status === next) return;
|
|
status = next;
|
|
listeners.forEach((cb) => cb());
|
|
}
|
|
|
|
/**
|
|
* The server extends the stock {hi} packet with a `tenant` field (server/datamodel.go) that the
|
|
* vendored JS SDK has no API for. `#initPacket`/the connection's packet builder is private, so the
|
|
* only usable hook is wrapping the connection's raw sendText and injecting the field into the
|
|
* already-serialized JSON. Ported as-is from the old app's tnSetup/installTenantHiPatch -- this is
|
|
* a real backend protocol extension, not an old-codebase-specific workaround.
|
|
*/
|
|
function installTenantHiPatch(t: Tinode) {
|
|
const conn = t._connection;
|
|
if (!conn || conn.__tenantPatched) return;
|
|
const original = conn.sendText.bind(conn);
|
|
conn.sendText = (msg: unknown) => {
|
|
if (typeof msg === 'string' && msg.indexOf('"hi":') >= 0) {
|
|
try {
|
|
const parsed = JSON.parse(msg);
|
|
if (parsed.hi) {
|
|
const tenant = useSessionStore.getState().tenant;
|
|
if (tenant?.code) {
|
|
parsed.hi.tenant = tenant.code;
|
|
msg = JSON.stringify(parsed);
|
|
}
|
|
}
|
|
} catch {
|
|
// Not valid JSON -- send as is.
|
|
}
|
|
}
|
|
return original(msg as string);
|
|
};
|
|
conn.__tenantPatched = true;
|
|
}
|
|
|
|
function createTinode(): Tinode {
|
|
const tenant = useSessionStore.getState().tenant;
|
|
const host = tenant?.host || 'web.118im.com';
|
|
const secure = tenant?.secure ?? true;
|
|
const t = new Tinode({ appName: APP_NAME, host, apiKey: API_KEY, transport: 'ws', secure, persist: false });
|
|
t.setHumanLanguage('zh');
|
|
t.enableLogging(LOGGING_ENABLED, true);
|
|
installTenantHiPatch(t);
|
|
|
|
t.onConnect = () => setStatus('connected');
|
|
t.onDisconnect = () => setStatus('disconnected');
|
|
t.onLogin = (code: number) => {
|
|
if (code === 200) setStatus('authenticated');
|
|
};
|
|
|
|
return t;
|
|
}
|
|
|
|
/** Lazily creates the singleton connection. Must only be called once a tenant is resolved. */
|
|
export function getTinode(): Tinode {
|
|
if (!tinode) {
|
|
tinode = createTinode();
|
|
}
|
|
return tinode;
|
|
}
|
|
|
|
/** Tears down the singleton so the next getTinode() rebuilds against a freshly-resolved tenant. */
|
|
export function resetTinode() {
|
|
if (tinode?.isConnected()) {
|
|
tinode.disconnect();
|
|
}
|
|
tinode = null;
|
|
setStatus('disconnected');
|
|
}
|
|
|
|
export async function connect(): Promise<void> {
|
|
const t = getTinode();
|
|
if (t.isConnected()) return;
|
|
setStatus('connecting');
|
|
await t.connect();
|
|
}
|
|
|
|
export function getConnectionStatus(): ConnectionStatus {
|
|
return status;
|
|
}
|
|
|
|
export function subscribeConnectionStatus(callback: () => void): () => void {
|
|
listeners.add(callback);
|
|
return () => listeners.delete(callback);
|
|
}
|