@pmate/chat-sdk 0.11.5 → 0.11.7

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.
@@ -4,6 +4,7 @@ export type MentionCandidate = {
4
4
  name: string;
5
5
  avatar?: string;
6
6
  description?: string;
7
+ aliases?: string[];
7
8
  };
8
9
  /** UTF-16 offsets [start, end) in the sent text, including @. */
9
10
  export type ChatMention = {
@@ -12,7 +12,7 @@ export function filterMentionCandidates(candidates, query) {
12
12
  if (seen.has(candidate.id))
13
13
  return false;
14
14
  seen.add(candidate.id);
15
- return candidate.name.toLocaleLowerCase().includes(query.toLocaleLowerCase());
15
+ return [candidate.name, ...(candidate.aliases ?? [])].some(name => name.toLocaleLowerCase().includes(query.toLocaleLowerCase()));
16
16
  });
17
17
  }
18
18
  /** Rebase untouched mentions; editing any part of a mention removes its identity. */
@@ -18,3 +18,4 @@ export * from "./reach.js";
18
18
  export * from "./teamScope.js";
19
19
  export * from "./teamMigrations.js";
20
20
  export * from "./socialReadClient.js";
21
+ export * from "./readCounts.js";
@@ -18,3 +18,4 @@ export * from "./reach.js";
18
18
  export * from "./teamScope.js";
19
19
  export * from "./teamMigrations.js";
20
20
  export * from "./socialReadClient.js";
21
+ export * from "./readCounts.js";
@@ -1,5 +1,5 @@
1
1
  import type { ChatRuntimeMessage } from "./protocol.js";
2
- export type ReachEventType = "call.incoming" | "call.ended" | "social.readStateChanged" | "meeting.invitation" | "meeting.ended";
2
+ export type ReachEventType = "call.incoming" | "call.ended" | "social.readStateChanged" | "social.readCountsChanged" | "meeting.invitation" | "meeting.ended";
3
3
  export type OnlineReachEvent = {
4
4
  eventId: string;
5
5
  eventType: ReachEventType;
@@ -1,5 +1,5 @@
1
1
  import { OnlineNotificationReceiver } from "./notification.js";
2
- const types = ["call.incoming", "call.ended", "social.readStateChanged", "meeting.invitation", "meeting.ended"];
2
+ const types = ["call.incoming", "call.ended", "social.readStateChanged", "social.readCountsChanged", "meeting.invitation", "meeting.ended"];
3
3
  /** Uses the verified Hub envelope; handling confirms business processing, never reading. */
4
4
  export class OnlineReachReceiver {
5
5
  receiver;
@@ -0,0 +1,40 @@
1
+ export type ReadCountsResult = {
2
+ appId: string;
3
+ threadId: string;
4
+ revision: number;
5
+ counts: Record<string, number>;
6
+ };
7
+ export type ReadCountsOptions = {
8
+ appId: string;
9
+ profileId: string;
10
+ token: string;
11
+ clientInstanceId: string;
12
+ hubHttpUrl: string;
13
+ fetch?: typeof fetch;
14
+ onChange: (value: ReadCountsResult) => void;
15
+ onError?: (error: unknown) => void;
16
+ };
17
+ /** One active conversation; fixed event window, bounded batches, no count polling. */
18
+ export declare class ReadCountsClient {
19
+ private options;
20
+ private thread?;
21
+ private ids;
22
+ private timer?;
23
+ private heartbeat?;
24
+ private controller?;
25
+ private generation;
26
+ private retries;
27
+ private closed;
28
+ private revision;
29
+ private running;
30
+ private dirty;
31
+ constructor(options: ReadCountsOptions);
32
+ private post;
33
+ private subscription;
34
+ watch(threadId: string | undefined, messageIds: string[]): void;
35
+ changed(threadId: string, revision: number): void;
36
+ reconnect(): void;
37
+ private schedule;
38
+ private refresh;
39
+ close(): void;
40
+ }
@@ -0,0 +1,139 @@
1
+ let subscriptionSequence = 0;
2
+ function nextSubscriptionSequence() { subscriptionSequence = Math.max(Date.now() * 1000, subscriptionSequence + 1); return subscriptionSequence; }
3
+ /** One active conversation; fixed event window, bounded batches, no count polling. */
4
+ export class ReadCountsClient {
5
+ options;
6
+ thread;
7
+ ids = [];
8
+ timer;
9
+ heartbeat;
10
+ controller;
11
+ generation = 0;
12
+ retries = 0;
13
+ closed = false;
14
+ revision = -1;
15
+ running = false;
16
+ dirty = false;
17
+ constructor(options) {
18
+ this.options = options;
19
+ }
20
+ async post(path, body, signal) {
21
+ const response = await (this.options.fetch ?? fetch)(`${this.options.hubHttpUrl.replace(/\/$/, "")}${path}`, { method: "POST", headers: { authorization: `Bearer ${this.options.token}`, "content-type": "application/json" }, body: JSON.stringify(body), signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(8000)]) : AbortSignal.timeout(8000), redirect: "error" });
22
+ if (!response.ok)
23
+ throw new Error(`READ_COUNTS_HTTP_${response.status}`);
24
+ const envelope = await response.json();
25
+ if (envelope.success !== true)
26
+ throw new Error("INVALID_READ_COUNTS_RESPONSE");
27
+ return envelope.data;
28
+ }
29
+ subscription(threadId, release) {
30
+ return this.post("/api/social/reading/subscription", { appId: this.options.appId, profileId: this.options.profileId, threadId, clientInstanceId: this.options.clientInstanceId, sequence: nextSubscriptionSequence(), release });
31
+ }
32
+ watch(threadId, messageIds) {
33
+ if (this.closed)
34
+ return;
35
+ const ids = [...new Set(messageIds)].slice(-100);
36
+ if (this.thread === threadId) {
37
+ if (JSON.stringify(ids) !== JSON.stringify(this.ids)) {
38
+ this.ids = ids;
39
+ this.schedule(0);
40
+ }
41
+ return;
42
+ }
43
+ const old = this.thread;
44
+ this.generation++;
45
+ this.controller?.abort();
46
+ clearTimeout(this.timer);
47
+ clearInterval(this.heartbeat);
48
+ this.timer = undefined;
49
+ this.thread = threadId;
50
+ this.ids = ids;
51
+ this.revision = -1;
52
+ this.dirty = false;
53
+ this.retries = 0;
54
+ if (old)
55
+ void this.subscription(old, true).catch(error => this.options.onError?.(error));
56
+ if (!threadId)
57
+ return;
58
+ void this.subscription(threadId, false).catch(error => this.options.onError?.(error));
59
+ this.heartbeat = setInterval(() => { if (!this.closed && this.thread === threadId)
60
+ void this.subscription(threadId, false).catch(error => this.options.onError?.(error)); }, 20_000);
61
+ this.schedule(0);
62
+ }
63
+ changed(threadId, revision) {
64
+ if (!Number.isSafeInteger(revision) || revision < 0)
65
+ throw new Error("INVALID_READ_COUNTS_EVENT");
66
+ if (threadId !== this.thread || revision <= this.revision || this.closed)
67
+ return;
68
+ this.schedule(3000);
69
+ }
70
+ reconnect() {
71
+ if (!this.thread || this.closed)
72
+ return;
73
+ void this.subscription(this.thread, false).catch(error => this.options.onError?.(error));
74
+ this.schedule(0);
75
+ }
76
+ schedule(delay) {
77
+ if (!this.thread || this.closed)
78
+ return;
79
+ if (this.running) {
80
+ this.dirty = true;
81
+ return;
82
+ }
83
+ if (this.timer !== undefined)
84
+ return;
85
+ this.timer = setTimeout(() => { this.timer = undefined; void this.refresh(); }, delay);
86
+ }
87
+ async refresh() {
88
+ if (!this.thread || !this.ids.length || this.closed)
89
+ return;
90
+ if (this.running) {
91
+ this.dirty = true;
92
+ return;
93
+ }
94
+ const threadId = this.thread, generation = this.generation, ids = [...this.ids];
95
+ this.running = true;
96
+ let retryDelay;
97
+ const controller = new AbortController();
98
+ this.controller = controller;
99
+ try {
100
+ const value = await this.post("/api/social/reading/counts", { appId: this.options.appId, profileId: this.options.profileId, threadId, messageIds: ids }, controller.signal);
101
+ if (generation !== this.generation || this.closed)
102
+ return;
103
+ if (value?.appId !== this.options.appId || value.threadId !== threadId || !Number.isSafeInteger(value.revision) || value.revision < 0 || !value.counts || typeof value.counts !== "object" || ids.some(id => !Number.isSafeInteger(value.counts[id]) || value.counts[id] < 0) || Object.keys(value.counts).some(id => !ids.includes(id)))
104
+ throw new Error("INVALID_READ_COUNTS_RESULT");
105
+ this.retries = 0;
106
+ this.revision = value.revision;
107
+ this.options.onChange(value);
108
+ }
109
+ catch (error) {
110
+ if (!controller.signal.aborted && generation === this.generation && !this.closed) {
111
+ this.options.onError?.(error);
112
+ if (!/READ_COUNTS_HTTP_4\d\d|INVALID_READ_COUNTS/.test(String(error)) && this.retries < 3) {
113
+ retryDelay = 1000 * 2 ** this.retries++;
114
+ this.dirty = true;
115
+ }
116
+ }
117
+ }
118
+ finally {
119
+ this.running = false;
120
+ if (!this.closed && (this.dirty || generation !== this.generation)) {
121
+ this.dirty = false;
122
+ this.schedule(retryDelay ?? 3000);
123
+ }
124
+ }
125
+ }
126
+ close() {
127
+ if (this.closed)
128
+ return;
129
+ const old = this.thread;
130
+ this.closed = true;
131
+ this.generation++;
132
+ clearTimeout(this.timer);
133
+ clearInterval(this.heartbeat);
134
+ this.controller?.abort();
135
+ if (old)
136
+ void this.subscription(old, true).catch(() => { });
137
+ this.thread = undefined;
138
+ }
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.11.5",
3
+ "version": "0.11.7",
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",
@@ -52,16 +43,24 @@
52
43
  "devDependencies": {
53
44
  "@storybook/addon-docs": "^9.0.17",
54
45
  "@storybook/react-vite": "9.0.17",
55
- "@tailwindcss/vite": "catalog:",
56
- "@types/react": "catalog:",
57
- "@types/react-dom": "catalog:",
46
+ "@tailwindcss/vite": "^4.1.3",
47
+ "@types/react": "^19.2.7",
48
+ "@types/react-dom": "^19.2.3",
58
49
  "fake-indexeddb": "^6.2.5",
59
- "react": "catalog:",
60
- "react-dom": "catalog:",
50
+ "react": "^19.1.1",
51
+ "react-dom": "^19.1.1",
61
52
  "rimraf": "^6.1.2",
62
53
  "storybook": "^9.0.17",
63
- "tailwindcss": "catalog:",
54
+ "tailwindcss": "^4.0.14",
64
55
  "typescript": "5.9.2",
65
56
  "vitest": "^4.0.17"
57
+ },
58
+ "scripts": {
59
+ "clean": "rimraf lib",
60
+ "build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
61
+ "npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
62
+ "test": "vitest run",
63
+ "storybook": "storybook dev -p 6009",
64
+ "build-storybook": "storybook build --output-dir dist"
66
65
  }
67
- }
66
+ }