@pmate/chat-sdk 0.9.7 → 0.9.9
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/lib/runtime/call.d.ts +13 -0
- package/lib/runtime/call.js +129 -2
- package/lib/runtime/chatClient.d.ts +3 -0
- package/lib/runtime/chatClient.js +5 -2
- package/lib/runtime/index.d.ts +1 -0
- package/lib/runtime/index.js +1 -0
- package/lib/runtime/notification.d.ts +2 -1
- package/lib/runtime/notification.js +3 -3
- package/lib/runtime/reach.d.ts +23 -0
- package/lib/runtime/reach.js +18 -0
- package/package.json +16 -17
package/lib/runtime/call.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OnlineReachEvent } from "./reach.js";
|
|
1
2
|
export type MeetingRoom = {
|
|
2
3
|
groupId: string;
|
|
3
4
|
meetingId: string;
|
|
@@ -36,6 +37,7 @@ export type CallEnded = {
|
|
|
36
37
|
};
|
|
37
38
|
export type CallClientOptions = {
|
|
38
39
|
profileId: string;
|
|
40
|
+
clientInstanceId?: string;
|
|
39
41
|
token: string;
|
|
40
42
|
url?: string;
|
|
41
43
|
iceTransportPolicy?: RTCIceTransportPolicy;
|
|
@@ -58,6 +60,9 @@ export declare class CallClient {
|
|
|
58
60
|
private generation;
|
|
59
61
|
private closed;
|
|
60
62
|
private capturePending;
|
|
63
|
+
private attachId?;
|
|
64
|
+
private attachWaiters;
|
|
65
|
+
private terminalCalls;
|
|
61
66
|
private requestId?;
|
|
62
67
|
private groupIds;
|
|
63
68
|
private heartbeat?;
|
|
@@ -68,6 +73,14 @@ export declare class CallClient {
|
|
|
68
73
|
snapshot(): CallSnapshot;
|
|
69
74
|
subscribe(listener: (state: CallSnapshot) => void): () => void;
|
|
70
75
|
connect(): void;
|
|
76
|
+
/** Notification/reach entry: query authoritative state before showing a call. */
|
|
77
|
+
attach(callId: string): void;
|
|
78
|
+
/** Resolves only after an authoritative response has been applied, never after socket send. */
|
|
79
|
+
attachAndWait(callId: string, timeoutMs?: number): Promise<"handled" | "suppressed">;
|
|
80
|
+
private settleAttach;
|
|
81
|
+
/** Pass only events received through the authenticated OnlineReachReceiver. */
|
|
82
|
+
handleReach(event: OnlineReachEvent): Promise<"handled" | "suppressed">;
|
|
83
|
+
private rememberTerminal;
|
|
71
84
|
watchPeer(peerId?: string): void;
|
|
72
85
|
watchGroups(groupIds: string[]): void;
|
|
73
86
|
start(peerId: string): Promise<void>;
|
package/lib/runtime/call.js
CHANGED
|
@@ -12,6 +12,9 @@ export class CallClient {
|
|
|
12
12
|
generation = 0;
|
|
13
13
|
closed = false;
|
|
14
14
|
capturePending = false;
|
|
15
|
+
attachId;
|
|
16
|
+
attachWaiters = new Map();
|
|
17
|
+
terminalCalls = new Map();
|
|
15
18
|
requestId;
|
|
16
19
|
groupIds = [];
|
|
17
20
|
heartbeat;
|
|
@@ -29,7 +32,7 @@ export class CallClient {
|
|
|
29
32
|
const ws = (this.options.createWebSocket ?? (url => new WebSocket(url)))(this.options.url ?? "wss://calls.pmate.chat/call");
|
|
30
33
|
this.ws = ws;
|
|
31
34
|
ws.onopen = () => { if (this.ws === ws)
|
|
32
|
-
this.send({ type: "auth", token: this.options.token, profileId: this.options.profileId }); };
|
|
35
|
+
this.send({ type: "auth", token: this.options.token, profileId: this.options.profileId, ...(this.options.clientInstanceId ? { clientInstanceId: this.options.clientInstanceId } : {}) }); };
|
|
33
36
|
ws.onmessage = event => {
|
|
34
37
|
if (this.ws !== ws)
|
|
35
38
|
return;
|
|
@@ -59,6 +62,87 @@ export class CallClient {
|
|
|
59
62
|
this.reconnect = setTimeout(() => this.connect(), 3000);
|
|
60
63
|
};
|
|
61
64
|
}
|
|
65
|
+
/** Notification/reach entry: query authoritative state before showing a call. */
|
|
66
|
+
attach(callId) {
|
|
67
|
+
if (this.closed || !callId || callId.length > 128)
|
|
68
|
+
throw new Error("INVALID_CALL_ID");
|
|
69
|
+
this.attachId = callId;
|
|
70
|
+
if (this.state.ready)
|
|
71
|
+
this.send({ type: "call.attach", callId });
|
|
72
|
+
else
|
|
73
|
+
this.connect();
|
|
74
|
+
}
|
|
75
|
+
/** Resolves only after an authoritative response has been applied, never after socket send. */
|
|
76
|
+
attachAndWait(callId, timeoutMs = 4000) {
|
|
77
|
+
if (this.closed || !callId || callId.length > 128)
|
|
78
|
+
return Promise.reject(new Error("INVALID_CALL_ID"));
|
|
79
|
+
if (this.terminalCalls.has(callId))
|
|
80
|
+
return Promise.resolve("suppressed");
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const finish = () => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
const waiters = this.attachWaiters.get(callId);
|
|
85
|
+
waiters?.delete(waiter);
|
|
86
|
+
if (!waiters?.size)
|
|
87
|
+
this.attachWaiters.delete(callId);
|
|
88
|
+
};
|
|
89
|
+
const waiter = { resolve: (result) => { finish(); resolve(result); }, reject: (error) => { finish(); reject(error); } };
|
|
90
|
+
const timer = setTimeout(() => waiter.reject(new Error("CALL_ATTACH_TIMEOUT")), timeoutMs);
|
|
91
|
+
const waiters = this.attachWaiters.get(callId) ?? new Set();
|
|
92
|
+
waiters.add(waiter);
|
|
93
|
+
this.attachWaiters.set(callId, waiters);
|
|
94
|
+
try {
|
|
95
|
+
this.attach(callId);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
waiter.reject(error instanceof Error ? error : new Error("CALL_ATTACH_FAILED"));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
settleAttach(callId, result) {
|
|
103
|
+
for (const waiter of [...(this.attachWaiters.get(callId) ?? [])]) {
|
|
104
|
+
if (result instanceof Error)
|
|
105
|
+
waiter.reject(result);
|
|
106
|
+
else
|
|
107
|
+
waiter.resolve(result);
|
|
108
|
+
}
|
|
109
|
+
if (this.attachId === callId)
|
|
110
|
+
this.attachId = undefined;
|
|
111
|
+
}
|
|
112
|
+
/** Pass only events received through the authenticated OnlineReachReceiver. */
|
|
113
|
+
async handleReach(event) {
|
|
114
|
+
if (this.closed)
|
|
115
|
+
throw new Error("CALL_CLIENT_CLOSED");
|
|
116
|
+
const { callId, callerId, calleeId, reason, winningClientInstanceId } = event.payload;
|
|
117
|
+
if (typeof callId !== "string" || !callId || callId.length > 128 || (callerId !== this.options.profileId && calleeId !== this.options.profileId))
|
|
118
|
+
throw new Error("INVALID_CALL_REACH");
|
|
119
|
+
if (event.expiresAtMs <= Date.now())
|
|
120
|
+
return "suppressed";
|
|
121
|
+
if (event.eventType === "call.incoming") {
|
|
122
|
+
if (calleeId !== this.options.profileId)
|
|
123
|
+
throw new Error("INVALID_CALL_RECIPIENT");
|
|
124
|
+
return this.attachAndWait(callId);
|
|
125
|
+
}
|
|
126
|
+
if (event.eventType !== "call.ended" || typeof reason !== "string" || !reason || reason.length > 128)
|
|
127
|
+
throw new Error("INVALID_CALL_REACH");
|
|
128
|
+
if (reason === "answered_elsewhere" && winningClientInstanceId && winningClientInstanceId === this.options.clientInstanceId)
|
|
129
|
+
return "suppressed";
|
|
130
|
+
this.rememberTerminal(callId);
|
|
131
|
+
if (this.state.callId === callId && !["idle", "ended"].includes(this.state.phase)) {
|
|
132
|
+
this.release();
|
|
133
|
+
this.update({ phase: "ended", reason });
|
|
134
|
+
}
|
|
135
|
+
this.settleAttach(callId, "suppressed");
|
|
136
|
+
return "handled";
|
|
137
|
+
}
|
|
138
|
+
rememberTerminal(callId) {
|
|
139
|
+
this.terminalCalls.set(callId, Date.now());
|
|
140
|
+
for (const [id, time] of this.terminalCalls)
|
|
141
|
+
if (Date.now() - time > 300000)
|
|
142
|
+
this.terminalCalls.delete(id);
|
|
143
|
+
while (this.terminalCalls.size > 2000)
|
|
144
|
+
this.terminalCalls.delete(this.terminalCalls.keys().next().value);
|
|
145
|
+
}
|
|
62
146
|
watchPeer(peerId) {
|
|
63
147
|
this.update({ watchedPeerId: peerId, peerOnline: undefined, peerInCall: undefined });
|
|
64
148
|
if (this.state.ready)
|
|
@@ -113,6 +197,8 @@ export class CallClient {
|
|
|
113
197
|
}
|
|
114
198
|
}
|
|
115
199
|
hangup() {
|
|
200
|
+
if (this.state.callId)
|
|
201
|
+
this.rememberTerminal(this.state.callId);
|
|
116
202
|
if (this.state.callId && !["idle", "ended"].includes(this.state.phase))
|
|
117
203
|
this.send({ type: "hangup", callId: this.state.callId });
|
|
118
204
|
this.release();
|
|
@@ -186,6 +272,8 @@ export class CallClient {
|
|
|
186
272
|
this.closed = true;
|
|
187
273
|
clearTimeout(this.reconnect);
|
|
188
274
|
clearInterval(this.heartbeat);
|
|
275
|
+
for (const callId of [...this.attachWaiters.keys()])
|
|
276
|
+
this.settleAttach(callId, new Error("CALL_CLIENT_CLOSED"));
|
|
189
277
|
this.hangup();
|
|
190
278
|
this.ws?.close();
|
|
191
279
|
this.ws = undefined;
|
|
@@ -225,6 +313,8 @@ export class CallClient {
|
|
|
225
313
|
async receive(message) {
|
|
226
314
|
if (message.type === "ready") {
|
|
227
315
|
this.update({ ready: true, error: undefined });
|
|
316
|
+
for (const callId of new Set([...this.attachWaiters.keys(), ...(this.attachId ? [this.attachId] : [])]))
|
|
317
|
+
this.send({ type: "call.attach", callId });
|
|
228
318
|
if (this.state.watchedPeerId)
|
|
229
319
|
this.send({ type: "presence.watch", peerId: this.state.watchedPeerId });
|
|
230
320
|
if (this.groupIds.length)
|
|
@@ -256,26 +346,63 @@ export class CallClient {
|
|
|
256
346
|
}
|
|
257
347
|
if (message.requestId && message.requestId !== this.requestId)
|
|
258
348
|
return;
|
|
349
|
+
if (typeof message.callId === "string" && this.attachWaiters.has(message.callId))
|
|
350
|
+
this.settleAttach(message.callId, new Error(message.code ?? "CALL_ATTACH_FAILED"));
|
|
259
351
|
if (!message.callId || message.callId === this.state.callId)
|
|
260
352
|
this.fail(message.code);
|
|
261
353
|
return;
|
|
262
354
|
}
|
|
355
|
+
if (message.type === "call.status") {
|
|
356
|
+
if (message.callId !== this.attachId && !this.attachWaiters.has(message.callId))
|
|
357
|
+
return;
|
|
358
|
+
if (message.state === "ended") {
|
|
359
|
+
this.rememberTerminal(message.callId);
|
|
360
|
+
if (["idle", "ended"].includes(this.state.phase) || this.state.callId === message.callId) {
|
|
361
|
+
this.release();
|
|
362
|
+
this.update({ phase: "ended", callId: message.callId, reason: message.reason });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
this.settleAttach(message.callId, message.state === "accepted" && message.callId === this.state.callId ? "handled" : "suppressed");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
263
368
|
if (message.type === "ringing") {
|
|
369
|
+
if (this.terminalCalls.has(message.callId)) {
|
|
370
|
+
this.settleAttach(message.callId, "suppressed");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (message.callId === this.state.callId && ["incoming", "ringing", "connecting", "connected"].includes(this.state.phase)) {
|
|
374
|
+
this.settleAttach(message.callId, "handled");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (typeof message.expiresAt === "number" && message.expiresAt <= Date.now()) {
|
|
378
|
+
this.rememberTerminal(message.callId);
|
|
379
|
+
this.settleAttach(message.callId, "suppressed");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (this.attachId === message.callId)
|
|
383
|
+
this.attachId = undefined;
|
|
264
384
|
if (message.direction === "outgoing" && (this.state.phase !== "requesting" || message.requestId !== this.requestId)) {
|
|
265
385
|
this.send({ type: "hangup", callId: message.callId });
|
|
266
386
|
return;
|
|
267
387
|
}
|
|
268
388
|
if (message.direction === "incoming" && !["idle", "ended"].includes(this.state.phase)) {
|
|
269
389
|
this.send({ type: "hangup", callId: message.callId });
|
|
390
|
+
this.settleAttach(message.callId, "suppressed");
|
|
270
391
|
return;
|
|
271
392
|
}
|
|
272
393
|
this.update({ phase: message.direction === "incoming" ? "incoming" : "ringing", callId: message.callId, peerId: message.peerId, direction: message.direction, error: undefined, reason: undefined, startedAt: undefined });
|
|
273
|
-
this.armTimeout(35000);
|
|
394
|
+
this.armTimeout(typeof message.expiresAt === "number" ? Math.max(1, message.expiresAt - Date.now()) : 35000);
|
|
395
|
+
this.settleAttach(message.callId, "handled");
|
|
274
396
|
return;
|
|
275
397
|
}
|
|
398
|
+
if (message.type === "ended" && typeof message.callId === "string") {
|
|
399
|
+
this.rememberTerminal(message.callId);
|
|
400
|
+
this.settleAttach(message.callId, "suppressed");
|
|
401
|
+
}
|
|
276
402
|
if (message.callId !== this.state.callId)
|
|
277
403
|
return;
|
|
278
404
|
if (message.type === "ended") {
|
|
405
|
+
this.rememberTerminal(message.callId);
|
|
279
406
|
const event = { callId: message.callId, peerId: this.state.peerId, direction: this.state.direction, reason: message.reason, durationMs: message.durationMs };
|
|
280
407
|
this.release();
|
|
281
408
|
this.update({ phase: "ended", reason: message.reason });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OnlineReachHandler } from "./reach.js";
|
|
1
2
|
import { type OnlineNotificationHandler } from "./notification.js";
|
|
2
3
|
import { type ChatPresence } from "./presence.js";
|
|
3
4
|
import { type ChatUpdate } from "./updates.js";
|
|
@@ -10,6 +11,7 @@ export type ChatConnectionState = "connecting" | "reconnecting" | "authenticatin
|
|
|
10
11
|
export type ChatClientOptions = {
|
|
11
12
|
appId: string;
|
|
12
13
|
clientInstanceId?: string;
|
|
14
|
+
onReach?: OnlineReachHandler;
|
|
13
15
|
onNotification?: OnlineNotificationHandler;
|
|
14
16
|
profileId?: string;
|
|
15
17
|
botKey?: PmateBotKeyFile | string | unknown;
|
|
@@ -27,6 +29,7 @@ export type ChatClientOptions = {
|
|
|
27
29
|
export declare class ChatClient {
|
|
28
30
|
private readonly options;
|
|
29
31
|
private readonly instanceId;
|
|
32
|
+
private readonly reach;
|
|
30
33
|
private readonly notifications;
|
|
31
34
|
private presence?;
|
|
32
35
|
private presenceProfiles;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OnlineReachReceiver } from "./reach.js";
|
|
1
2
|
import { OnlineNotificationReceiver } from "./notification.js";
|
|
2
3
|
import { ChatPresenceClient } from "./presence.js";
|
|
3
4
|
import { parseChatUpdate } from "./updates.js";
|
|
@@ -11,6 +12,7 @@ const DEFAULT_ACK_TIMEOUT_MS = 5_000;
|
|
|
11
12
|
export class ChatClient {
|
|
12
13
|
options;
|
|
13
14
|
instanceId;
|
|
15
|
+
reach;
|
|
14
16
|
notifications;
|
|
15
17
|
presence;
|
|
16
18
|
presenceProfiles = [];
|
|
@@ -36,6 +38,7 @@ export class ChatClient {
|
|
|
36
38
|
constructor(options) {
|
|
37
39
|
this.options = options;
|
|
38
40
|
this.instanceId = options.clientInstanceId ?? crypto.randomUUID();
|
|
41
|
+
this.reach = new OnlineReachReceiver({ appId: options.appId, profileId: () => this.profileId(), send: message => this.requireClient().sendMsg(message), handler: options.onReach });
|
|
39
42
|
this.notifications = new OnlineNotificationReceiver({ appId: options.appId, profileId: () => this.profileId(),
|
|
40
43
|
send: message => this.requireClient().sendMsg(message), handler: options.onNotification });
|
|
41
44
|
if (options.botKey) {
|
|
@@ -268,7 +271,7 @@ export class ChatClient {
|
|
|
268
271
|
const auth = createRuntimeMsg(profileId, "@hub", ChatMsgOp.AUTH, {
|
|
269
272
|
token,
|
|
270
273
|
appId: this.options.appId,
|
|
271
|
-
notificationReceiver: !!this.options.onNotification,
|
|
274
|
+
notificationReceiver: !!(this.options.onNotification || this.options.onReach),
|
|
272
275
|
as: "user",
|
|
273
276
|
profileId,
|
|
274
277
|
clientInstanceId: this.instanceId,
|
|
@@ -303,7 +306,7 @@ export class ChatClient {
|
|
|
303
306
|
handleIncoming(message) {
|
|
304
307
|
if (message.opcode === ChatMsgOp.NOTIFICATION) {
|
|
305
308
|
if (this.connection === "connected")
|
|
306
|
-
void this.notifications.receive(message).catch(error => this.options.onActivityError?.(error));
|
|
309
|
+
void (message.from === "@hub/reach" ? this.reach.receive(message) : this.notifications.receive(message)).catch(error => this.options.onActivityError?.(error));
|
|
307
310
|
return;
|
|
308
311
|
}
|
|
309
312
|
const update = parseChatUpdate(message, this.options.appId);
|
package/lib/runtime/index.d.ts
CHANGED
package/lib/runtime/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export type OnlineNotification = {
|
|
|
9
9
|
createdAtMs?: number;
|
|
10
10
|
expiresAtMs?: number;
|
|
11
11
|
};
|
|
12
|
-
export type NotificationHandling = "displayed" | "suppressed";
|
|
12
|
+
export type NotificationHandling = "displayed" | "suppressed" | "handled";
|
|
13
13
|
export type OnlineNotificationHandler = (notification: OnlineNotification) => Promise<NotificationHandling>;
|
|
14
14
|
/** Device-local receipt and deduplication; only the host can confirm presentation. */
|
|
15
15
|
export declare class OnlineNotificationReceiver {
|
|
@@ -20,6 +20,7 @@ export declare class OnlineNotificationReceiver {
|
|
|
20
20
|
appId: string;
|
|
21
21
|
profileId: () => string;
|
|
22
22
|
send: (message: ChatRuntimeMessage) => Promise<void>;
|
|
23
|
+
source?: "@hub/notifications" | "@hub/reach";
|
|
23
24
|
handler?: OnlineNotificationHandler;
|
|
24
25
|
now?: () => number;
|
|
25
26
|
});
|
|
@@ -8,13 +8,13 @@ export class OnlineNotificationReceiver {
|
|
|
8
8
|
this.options = options;
|
|
9
9
|
}
|
|
10
10
|
async receive(message) {
|
|
11
|
-
if (message.opcode !== ChatMsgOp.NOTIFICATION || message.from !== "@hub/notifications" || message.to !== this.options.profileId())
|
|
11
|
+
if (message.opcode !== ChatMsgOp.NOTIFICATION || message.from !== (this.options.source ?? "@hub/notifications") || message.to !== this.options.profileId())
|
|
12
12
|
return;
|
|
13
13
|
const body = message.body;
|
|
14
14
|
if (!body || body.kind !== "delivery" || typeof body.attemptId !== "string" || !body.attemptId
|
|
15
15
|
|| typeof body.notificationId !== "string" || !body.notificationId)
|
|
16
16
|
return;
|
|
17
|
-
const ack = (status, code) => this.options.send(createRuntimeMsg(this.options.profileId(), "@hub/notifications", ChatMsgOp.NOTIFICATION, {
|
|
17
|
+
const ack = (status, code) => this.options.send(createRuntimeMsg(this.options.profileId(), this.options.source ?? "@hub/notifications", ChatMsgOp.NOTIFICATION, {
|
|
18
18
|
kind: "ack", attemptId: body.attemptId, notificationId: body.notificationId, status,
|
|
19
19
|
displayed: status === "displayed", code, receivedAtMs: this.now(),
|
|
20
20
|
}));
|
|
@@ -44,7 +44,7 @@ export class OnlineNotificationReceiver {
|
|
|
44
44
|
if (!this.options.handler)
|
|
45
45
|
throw new Error("Notification host unavailable");
|
|
46
46
|
const status = await this.options.handler(structuredClone(body));
|
|
47
|
-
if (status !== "displayed" && status !== "suppressed")
|
|
47
|
+
if (status !== "displayed" && status !== "suppressed" && status !== "handled")
|
|
48
48
|
throw new Error("Invalid notification handling result");
|
|
49
49
|
if (this.handled.size >= 10_000)
|
|
50
50
|
this.handled.delete(this.handled.keys().next().value);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ChatRuntimeMessage } from "./protocol.js";
|
|
2
|
+
export type ReachEventType = "call.incoming" | "call.ended" | "social.readStateChanged" | "meeting.invitation" | "meeting.ended";
|
|
3
|
+
export type OnlineReachEvent = {
|
|
4
|
+
eventId: string;
|
|
5
|
+
eventType: ReachEventType;
|
|
6
|
+
appId: string;
|
|
7
|
+
payload: Record<string, unknown>;
|
|
8
|
+
createdAtMs?: number;
|
|
9
|
+
expiresAtMs: number;
|
|
10
|
+
};
|
|
11
|
+
export type OnlineReachHandler = (event: OnlineReachEvent) => Promise<"handled" | "suppressed">;
|
|
12
|
+
/** Uses the verified Hub envelope; handling confirms business processing, never reading. */
|
|
13
|
+
export declare class OnlineReachReceiver {
|
|
14
|
+
private readonly receiver;
|
|
15
|
+
constructor(options: {
|
|
16
|
+
appId: string;
|
|
17
|
+
profileId: () => string;
|
|
18
|
+
send: (message: ChatRuntimeMessage) => Promise<void>;
|
|
19
|
+
handler?: OnlineReachHandler;
|
|
20
|
+
now?: () => number;
|
|
21
|
+
});
|
|
22
|
+
receive(message: ChatRuntimeMessage): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OnlineNotificationReceiver } from "./notification.js";
|
|
2
|
+
const types = ["call.incoming", "call.ended", "social.readStateChanged", "meeting.invitation", "meeting.ended"];
|
|
3
|
+
/** Uses the verified Hub envelope; handling confirms business processing, never reading. */
|
|
4
|
+
export class OnlineReachReceiver {
|
|
5
|
+
receiver;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.receiver = new OnlineNotificationReceiver({ ...options, source: "@hub/reach", handler: async (notification) => {
|
|
8
|
+
const { eventId, eventType, appId, payload } = notification.data;
|
|
9
|
+
if (eventId !== notification.notificationId || !types.includes(eventType) || typeof appId !== "string"
|
|
10
|
+
|| !payload || typeof payload !== "object" || Array.isArray(payload) || !Number.isFinite(notification.expiresAtMs))
|
|
11
|
+
throw new Error("INVALID_REACH_EVENT");
|
|
12
|
+
if (!options.handler)
|
|
13
|
+
throw new Error("REACH_HANDLER_UNAVAILABLE");
|
|
14
|
+
return options.handler({ eventId: notification.notificationId, eventType: eventType, appId, payload: payload, createdAtMs: notification.createdAtMs, expiresAtMs: notification.expiresAtMs });
|
|
15
|
+
} });
|
|
16
|
+
}
|
|
17
|
+
receive(message) { return this.receiver.receive(message); }
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmate/chat-sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -28,15 +28,6 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"clean": "rimraf lib",
|
|
33
|
-
"build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
|
|
34
|
-
"prepack": "pnpm run build",
|
|
35
|
-
"npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"storybook": "storybook dev -p 6009",
|
|
38
|
-
"build-storybook": "storybook build --output-dir dist"
|
|
39
|
-
},
|
|
40
31
|
"dependencies": {
|
|
41
32
|
"@emoji-mart/data": "^1.2.1",
|
|
42
33
|
"@emoji-mart/react": "^1.1.1",
|
|
@@ -51,16 +42,24 @@
|
|
|
51
42
|
"devDependencies": {
|
|
52
43
|
"@storybook/addon-docs": "^9.0.17",
|
|
53
44
|
"@storybook/react-vite": "9.0.17",
|
|
54
|
-
"@tailwindcss/vite": "
|
|
55
|
-
"@types/react": "
|
|
56
|
-
"@types/react-dom": "
|
|
45
|
+
"@tailwindcss/vite": "^4.1.3",
|
|
46
|
+
"@types/react": "^19.2.7",
|
|
47
|
+
"@types/react-dom": "^19.2.3",
|
|
57
48
|
"fake-indexeddb": "^6.2.5",
|
|
58
|
-
"react": "
|
|
59
|
-
"react-dom": "
|
|
49
|
+
"react": "^19.1.1",
|
|
50
|
+
"react-dom": "^19.1.1",
|
|
60
51
|
"rimraf": "^6.1.2",
|
|
61
52
|
"storybook": "^9.0.17",
|
|
62
|
-
"tailwindcss": "
|
|
53
|
+
"tailwindcss": "^4.0.14",
|
|
63
54
|
"typescript": "5.9.2",
|
|
64
55
|
"vitest": "^4.0.17"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"clean": "rimraf lib",
|
|
59
|
+
"build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
|
|
60
|
+
"npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"storybook": "storybook dev -p 6009",
|
|
63
|
+
"build-storybook": "storybook build --output-dir dist"
|
|
65
64
|
}
|
|
66
|
-
}
|
|
65
|
+
}
|