@pmate/chat-sdk 0.12.0-next.67 → 0.12.0-next.69
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 +73 -65
- package/lib/advanced.d.ts +10 -0
- package/lib/advanced.js +10 -0
- package/lib/chat.d.ts +4 -130
- package/lib/chat.js +9 -217
- package/lib/chatContract.d.ts +144 -0
- package/lib/chatContract.js +1 -0
- package/lib/chatError.d.ts +11 -0
- package/lib/chatError.js +13 -0
- package/lib/chatTypes.d.ts +119 -0
- package/lib/chatTypes.js +1 -0
- package/lib/credentials.d.ts +26 -0
- package/lib/credentials.js +109 -0
- package/lib/framework.d.ts +11 -0
- package/lib/framework.js +12 -0
- package/lib/history.d.ts +4 -0
- package/lib/history.js +8 -0
- package/lib/hooks/useChatSnapshot.js +4 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/internal/chatEngine.d.ts +131 -0
- package/lib/internal/chatEngine.js +280 -0
- package/lib/internal/chatRegistry.d.ts +4 -0
- package/lib/internal/chatRegistry.js +4 -0
- package/lib/internal/createPublicChat.d.ts +4 -0
- package/lib/internal/createPublicChat.js +52 -0
- package/lib/messageView.d.ts +2 -33
- package/lib/messageView.js +1 -1
- package/lib/runtime/chatClient.d.ts +1 -0
- package/lib/runtime/chatClient.js +7 -2
- package/lib/runtime/chatError.d.ts +2 -11
- package/lib/runtime/chatError.js +3 -14
- package/lib/runtime/chatSession.d.ts +1 -0
- package/lib/runtime/chatSession.js +14 -1
- package/lib/runtime/createChat.d.ts +6 -2
- package/lib/runtime/createChat.js +17 -7
- package/lib/runtime/messageClient.d.ts +5 -0
- package/lib/runtime/messageClient.js +32 -0
- package/lib/runtime/messageStore.d.ts +2 -0
- package/lib/runtime/messageStore.js +12 -0
- package/lib/runtime/readCounts.js +1 -1
- package/lib/runtime/readSync.js +1 -1
- package/lib/runtime/socialReadClient.d.ts +1 -1
- package/lib/runtime/socialReadClient.js +4 -4
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -138,50 +138,65 @@ to the SDK. The default API presents one semantic message list, including delive
|
|
|
138
138
|
recalls and reactions. It never presents a partial migration as a ready window.
|
|
139
139
|
|
|
140
140
|
```ts
|
|
141
|
-
import {createChat
|
|
142
|
-
const chat = createChat({appId, accountId, profileId, teamId,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const threadId = await chat.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
if (error instanceof ChatError && error.retryable) showRetry()
|
|
152
|
-
}
|
|
153
|
-
const off = chat.subscribe(threadId, state => render(state.messages))
|
|
141
|
+
import {createChat} from "@pmate/chat-sdk/chat"
|
|
142
|
+
const chat = createChat({appId, accountId, profileId, teamId,
|
|
143
|
+
getToken: ({forceRefresh, signal}) => auth.getAccessToken({forceRefresh, signal}),
|
|
144
|
+
})
|
|
145
|
+
await chat.start()
|
|
146
|
+
const threadId = await chat.resolveThreadId({peerId}) // or {groupId}; no remote creation
|
|
147
|
+
const off = chat.subscribeThread(threadId, state => {
|
|
148
|
+
if (state.status === "ready") render(state.messages)
|
|
149
|
+
else if (state.status === "error") showRetry(state.error)
|
|
150
|
+
}) // listens first, opens local state, then owns background synchronization
|
|
154
151
|
await chat.sendMessage(threadId, {text: "Hello"})
|
|
155
|
-
await chat.
|
|
152
|
+
await chat.loadEarlier(threadId)
|
|
156
153
|
off()
|
|
157
154
|
await chat.close()
|
|
158
155
|
```
|
|
159
156
|
|
|
160
|
-
`
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
`
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Object references
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`@pmate/chat-sdk
|
|
157
|
+
`getThread(id)` is the one-shot equivalent; it returns the same stable snapshot
|
|
158
|
+
as `subscribeThread`. Cache hits do not repeat history requests. `loadEarlier(id)`
|
|
159
|
+
extends the SDK window without limits or cursors. `refreshThread(id)` explicitly
|
|
160
|
+
reconciles authority; ordinary reconnect/wake synchronization is automatic.
|
|
161
|
+
For a cold route, keep the usable list visible until `getThread` resolves, or
|
|
162
|
+
until a subscription delivers `status: "ready"`; no intermediate message loader
|
|
163
|
+
is necessary. Subscription initialization has four bounded concurrent lanes.
|
|
164
|
+
Multiple consumers share work; disposing one does not stop another consumer.
|
|
165
|
+
|
|
166
|
+
Thread state is `pending`, `ready`, or `error`. A failed initial read is `error`;
|
|
167
|
+
a failed background refresh retains `ready` messages and includes `error`.
|
|
168
|
+
`ready` is retained as a derived compatibility field; use `status` in new code.
|
|
169
|
+
Connection state is independent. A snapshot also contains `hasEarlier`, `unread`,
|
|
170
|
+
`group`, `readCounts` and scoped target identity. Unknown unread stays undefined.
|
|
171
|
+
`subscribeChat` delivers known conversation summaries and aggregate reading state.
|
|
172
|
+
Subscriptions emit their current snapshot first, then meaningful changes only;
|
|
173
|
+
`fields` filters notifications. Callbacks cannot corrupt a shared cache read.
|
|
174
|
+
Opening or subscribing does not mark messages read.
|
|
175
|
+
|
|
176
|
+
Messages provide kind/text/image/voice/quote, reactions and reading/delivery state.
|
|
177
|
+
No raw wire or control revision is needed. The public models do not derive their
|
|
178
|
+
types from Session, storage or projection internals. Object references stay stable
|
|
179
|
+
when content has not changed. React `useChatSnapshot(chat, threadId)` manages the
|
|
180
|
+
external-store subscription; custom integrations use `getChatStore` from
|
|
181
|
+
`@pmate/chat-sdk/framework`, where synchronous memory snapshots and change-only
|
|
182
|
+
notifications are explicitly separated from ordinary asynchronous reads.
|
|
183
|
+
|
|
184
|
+
`getThread`, `loadEarlier`, `refreshThread`, `getGroup`, `refreshGroup`,
|
|
185
|
+
`resolveThreadId`, and `getVoiceAttachment` accept `{signal}`. Reader queries accept
|
|
186
|
+
`{afterProfileId, revision, signal}`. Aborting rejects that consumer with the signal
|
|
187
|
+
reason (normally AbortError); it does not abort shared work, publish a fake cache
|
|
188
|
+
failure, or undo a durable send. A late abandoned voice lease is released.
|
|
189
|
+
Complete-history search/export is an explicit, potentially large operation:
|
|
190
|
+
`loadThreadHistory(chat, id, {signal})` from `@pmate/chat-sdk/history`. It traverses
|
|
191
|
+
accessible history and rejects incomplete scans. It is not a flag on normal reads.
|
|
192
|
+
|
|
193
|
+
The credential provider is invoked on connection/reconnection and authenticated
|
|
194
|
+
HTTP reads; concurrent requests share acquisition. HTTP 401 gets one coalesced
|
|
195
|
+
forced refresh and at most one replay. HTTP 403 is never retried automatically.
|
|
196
|
+
Providers must preserve account/app/profile identity. A fixed `token` remains
|
|
197
|
+
supported as an alternative; select one source. An ambiguous WebSocket AUTH_FAILED
|
|
198
|
+
requires authentication rather than guessing that a permission failure expired.
|
|
199
|
+
Closing fences late providers and prevents further authenticated network work.
|
|
185
200
|
|
|
186
201
|
Sending accepts text/mentions/quotes, images, voice, recalls, reactions and
|
|
187
202
|
invitations through the same typed content union. The SDK resolves the target
|
|
@@ -217,35 +232,28 @@ wait for confirmed delivery and a causal server version. SDK-owned requests shar
|
|
|
217
232
|
bounded queues and time windows; Apps do not poll. `watchReaders(threadId,ids,receiver)`
|
|
218
233
|
selects receipt counts for the current viewport; clear with `watchReaders(undefined,[])`.
|
|
219
234
|
|
|
220
|
-
`ChatError` exposes
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
data. Dispose and recreate
|
|
226
|
-
the SDK when account, profile, team or token changes.
|
|
235
|
+
`ChatError` exposes code, retryable, optional HTTP status and cause. Categories are
|
|
236
|
+
unavailable, authentication-required, access-denied, storage-unavailable,
|
|
237
|
+
invalid-input, conflict, closed and busy. Token expiry requires authentication;
|
|
238
|
+
a 403 withdraws the scoped session and its caches. Account/profile/team changes
|
|
239
|
+
require a new SDK instance. Token renewal alone uses the provider.
|
|
227
240
|
|
|
228
241
|
### Advanced integration and migration
|
|
229
242
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
`
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
use `createGroup/updateGroup`; UI never assigns reaction versions or recipients.
|
|
245
|
-
|
|
246
|
-
The advanced storage and transport classes remain exported from `/runtime`.
|
|
247
|
-
They must share one authenticated scope and must not bypass scope authority.
|
|
248
|
-
Normal UI code should not maintain another message cache or outbox.
|
|
243
|
+
The default client has no `advanced`, `getSnapshot`, `getMessages`, `getConversation`
|
|
244
|
+
or generic `subscribe`. Use resolveThreadId, getThread, subscribeThread,
|
|
245
|
+
loadEarlier and refreshThread. Public readers no longer accept a separate fourth
|
|
246
|
+
signal argument. Use `{signal}` together with the query instead.
|
|
247
|
+
|
|
248
|
+
Host-owned calls/presence transports use `createChatWithTransport` from
|
|
249
|
+
`@pmate/chat-sdk/advanced`; it returns the same public client. `getChatRuntime(chat)`
|
|
250
|
+
provides explicit one-time migration/diagnostic access. `handleChatReach` and
|
|
251
|
+
`revokeChatAccess` are advanced integration helpers. A deprecated
|
|
252
|
+
`getLegacyChat(chat)` adapter is available only from this entry during migration.
|
|
253
|
+
The raw factory is named `createChatSession` in `/runtime`; its old `createChat`
|
|
254
|
+
name is deprecated. Raw storage/transport clients remain advanced capabilities.
|
|
255
|
+
They must share identity and respect scope authority. Apps do not maintain a second
|
|
256
|
+
message cache, outbox, recipient roster or control revision generator.
|
|
249
257
|
|
|
250
258
|
### Reading storage recovery
|
|
251
259
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Chat } from "./chatContract.js";
|
|
2
|
+
import type { CreateChatOptions } from "./runtime/createChat.js";
|
|
3
|
+
/** Host-owned transport integration. The default factory never accepts raw transports. */
|
|
4
|
+
export declare function createChatWithTransport(options: CreateChatOptions): Chat;
|
|
5
|
+
/** Explicit migration/diagnostic access, outside the ordinary client. */
|
|
6
|
+
export declare function getChatRuntime(chat: Chat): import("./runtime/chatSession.js").ChatSession<import("./runtime/cachedMessage.js").CachedChatMessage>;
|
|
7
|
+
export declare function handleChatReach(chat: Chat, event: Parameters<ReturnType<typeof getChatRuntime>["handleReach"]>[0]): Promise<"handled" | undefined>;
|
|
8
|
+
export declare function revokeChatAccess(chat: Chat): Promise<void>;
|
|
9
|
+
/** @deprecated Transitional next.68 adapter. New App code must use the public client. */
|
|
10
|
+
export declare function getLegacyChat(chat: Chat): import("./internal/chatEngine.js").ChatEngine;
|
package/lib/advanced.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createPublicChat } from "./internal/createPublicChat.js";
|
|
2
|
+
import { chatEngine } from "./internal/chatRegistry.js";
|
|
3
|
+
/** Host-owned transport integration. The default factory never accepts raw transports. */
|
|
4
|
+
export function createChatWithTransport(options) { return createPublicChat(options); }
|
|
5
|
+
/** Explicit migration/diagnostic access, outside the ordinary client. */
|
|
6
|
+
export function getChatRuntime(chat) { return chatEngine(chat).advanced; }
|
|
7
|
+
export function handleChatReach(chat, event) { return chatEngine(chat).handleReach(event); }
|
|
8
|
+
export function revokeChatAccess(chat) { return chatEngine(chat).revokeAccess(); }
|
|
9
|
+
/** @deprecated Transitional next.68 adapter. New App code must use the public client. */
|
|
10
|
+
export function getLegacyChat(chat) { return chatEngine(chat); }
|
package/lib/chat.d.ts
CHANGED
|
@@ -1,131 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
import type { ChatSession, ChatSummarySnapshot as SessionSummary, ChatThreadSnapshot as SessionThread } from "./runtime/chatSession.js";
|
|
6
|
-
import type { CachedChatMessage } from "./runtime/cachedMessage.js";
|
|
7
|
-
import type { ConversationTarget } from "./runtime/conversationDirectory.js";
|
|
8
|
-
import type { OutgoingContent as RuntimeContent } from "./runtime/outgoingContent.js";
|
|
9
|
-
import { ChatError } from "./runtime/chatError.js";
|
|
10
|
-
export { ChatError } from "./runtime/chatError.js";
|
|
11
|
-
export type { ChatErrorCode } from "./runtime/chatError.js";
|
|
12
|
-
export type { CreateChatOptions, ConversationTarget };
|
|
13
|
-
export type OutgoingContent = Exclude<RuntimeContent, {
|
|
14
|
-
groupCreate: unknown;
|
|
15
|
-
} | {
|
|
16
|
-
groupUpdate: unknown;
|
|
17
|
-
}>;
|
|
18
|
-
export type ChatGroup = Readonly<Omit<RuntimeGroup, "versionNumber">>;
|
|
19
|
-
export type ConversationInfo = Readonly<{
|
|
20
|
-
kind: "dm" | "group";
|
|
21
|
-
targetId: string;
|
|
22
|
-
}>;
|
|
23
|
-
export type ChatThreadSnapshot = Readonly<{
|
|
24
|
-
threadId: string;
|
|
25
|
-
messages: readonly ChatMessage[];
|
|
26
|
-
ready: boolean;
|
|
27
|
-
hasEarlier: boolean;
|
|
28
|
-
conversation?: ConversationInfo;
|
|
29
|
-
unread: SessionThread<CachedChatMessage>["unread"];
|
|
30
|
-
group: ChatGroup | undefined;
|
|
31
|
-
readCounts: SessionThread<CachedChatMessage>["readCounts"];
|
|
32
|
-
error?: ChatError;
|
|
33
|
-
}>;
|
|
34
|
-
export type ChatSnapshot = Readonly<{
|
|
35
|
-
threads: readonly {
|
|
36
|
-
threadId: string;
|
|
37
|
-
lastMessage?: ChatMessage;
|
|
38
|
-
group: ChatGroup | undefined;
|
|
39
|
-
unread: ChatThreadSnapshot["unread"];
|
|
40
|
-
outgoingCount: number;
|
|
41
|
-
}[];
|
|
42
|
-
unreadMessageCount: number | undefined;
|
|
43
|
-
badgeCount: number | undefined;
|
|
44
|
-
connection: SessionSummary<CachedChatMessage>["connection"];
|
|
45
|
-
}>;
|
|
46
|
-
export type MessageQuery = {
|
|
47
|
-
earlier?: boolean;
|
|
48
|
-
all?: boolean;
|
|
49
|
-
refresh?: boolean;
|
|
50
|
-
};
|
|
51
|
-
export type ReadersQuery = {
|
|
52
|
-
afterProfileId?: string;
|
|
53
|
-
revision?: number;
|
|
54
|
-
};
|
|
55
|
-
export type GroupChanges = {
|
|
56
|
-
expectedRevision?: string;
|
|
57
|
-
addMembers?: readonly string[];
|
|
58
|
-
removeMembers?: readonly string[];
|
|
59
|
-
title?: string;
|
|
60
|
-
avatar?: string;
|
|
61
|
-
};
|
|
62
|
-
export type SendOptions = {
|
|
63
|
-
waitFor?: "accepted" | "delivered";
|
|
64
|
-
};
|
|
65
|
-
export type SentMessage = Readonly<{
|
|
66
|
-
id: string;
|
|
67
|
-
timestamp: number;
|
|
68
|
-
}>;
|
|
69
|
-
type SubscribeOptions<T> = {
|
|
70
|
-
fields?: readonly (keyof T)[];
|
|
71
|
-
emitCurrent?: boolean;
|
|
72
|
-
};
|
|
73
|
-
/** App-facing API. Cache coverage, raw protocol and synchronization stay in advanced. */
|
|
74
|
-
export declare class Chat {
|
|
75
|
-
readonly advanced: ChatSession<CachedChatMessage>;
|
|
76
|
-
private options;
|
|
77
|
-
private groups;
|
|
78
|
-
private group;
|
|
79
|
-
private present;
|
|
80
|
-
private redactedQuotes;
|
|
81
|
-
private messageLists;
|
|
82
|
-
private messages;
|
|
83
|
-
private views;
|
|
84
|
-
private summary?;
|
|
85
|
-
private targets;
|
|
86
|
-
private errors;
|
|
87
|
-
private listeners;
|
|
88
|
-
private closed;
|
|
89
|
-
constructor(advanced: ChatSession<CachedChatMessage>, options: CreateChatOptions);
|
|
90
|
-
private operation;
|
|
91
|
-
private deny;
|
|
92
|
-
private threadOperation;
|
|
93
|
-
start(): Promise<void>;
|
|
94
|
-
getConversation(target: ConversationTarget): Promise<string>;
|
|
95
|
-
getSnapshot(): ChatSnapshot;
|
|
96
|
-
getSnapshot(threadId: string): ChatThreadSnapshot;
|
|
97
|
-
getMessages(threadId: string, query?: MessageQuery): Promise<ChatThreadSnapshot>;
|
|
98
|
-
sendMessage(threadId: string, content: OutgoingContent, options?: SendOptions): Promise<SentMessage>;
|
|
99
|
-
retryMessage(messageId: string): Promise<SentMessage>;
|
|
100
|
-
subscribe(listener: (snapshot: ChatSnapshot) => void, options?: SubscribeOptions<ChatSnapshot>): () => void;
|
|
101
|
-
subscribe(threadId: string, listener: (snapshot: ChatThreadSnapshot) => void, options?: SubscribeOptions<ChatThreadSnapshot>): () => void;
|
|
102
|
-
private sessionFields;
|
|
103
|
-
private notify;
|
|
104
|
-
observeMessages(threadId: string, ids: string[], event: "visible" | "played"): Promise<void>;
|
|
105
|
-
markRead(threadId: string): Promise<void>;
|
|
106
|
-
getReaders(threadId: string, messageId: string, page?: ReadersQuery, signal?: AbortSignal): Promise<import("./runtime/messageReaders.js").MessageReadersPage>;
|
|
107
|
-
watchReaders(threadId: string | undefined, ids: string[], receiver?: string): Promise<void>;
|
|
108
|
-
handleReach(event: Parameters<ChatSession<CachedChatMessage>["handleReach"]>[0]): Promise<"handled" | undefined>;
|
|
109
|
-
getVoiceAttachment(threadId: string, messageId: string): Promise<import("./runtime/durableSender.js").VoiceAttachmentLease>;
|
|
110
|
-
getGroup(threadId: string, options?: {
|
|
111
|
-
refresh?: boolean;
|
|
112
|
-
}): Promise<Readonly<Omit<RuntimeGroup, "versionNumber">>>;
|
|
113
|
-
createGroup(input: {
|
|
114
|
-
groupId?: string;
|
|
115
|
-
title: string;
|
|
116
|
-
members: readonly string[];
|
|
117
|
-
}): Promise<{
|
|
118
|
-
threadId: string;
|
|
119
|
-
revision: string;
|
|
120
|
-
teamId: string;
|
|
121
|
-
title: string;
|
|
122
|
-
groupId: string;
|
|
123
|
-
avatar?: string;
|
|
124
|
-
ownerId: string;
|
|
125
|
-
memberIds: readonly string[];
|
|
126
|
-
}>;
|
|
127
|
-
updateGroup(threadId: string, changes: GroupChanges): Promise<Readonly<Omit<RuntimeGroup, "versionNumber">>>;
|
|
128
|
-
revokeAccess(): Promise<void>;
|
|
129
|
-
close(): Promise<void>;
|
|
130
|
-
}
|
|
1
|
+
import type { Chat, CreateChatOptions } from "./chatContract.js";
|
|
2
|
+
export type * from "./chatContract.js";
|
|
3
|
+
export type * from "./chatTypes.js";
|
|
4
|
+
export { ChatError, type ChatErrorCode } from "./chatError.js";
|
|
131
5
|
export declare function createChat(options: CreateChatOptions): Chat;
|
package/lib/chat.js
CHANGED
|
@@ -1,219 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export { ChatError } from "./runtime/chatError.js";
|
|
5
|
-
const emptyMessages = Object.freeze([]);
|
|
6
|
-
/** App-facing API. Cache coverage, raw protocol and synchronization stay in advanced. */
|
|
7
|
-
export class Chat {
|
|
8
|
-
advanced;
|
|
9
|
-
options;
|
|
10
|
-
groups = new WeakMap();
|
|
11
|
-
group(value) {
|
|
12
|
-
if (!value)
|
|
13
|
-
return undefined;
|
|
14
|
-
let projected = this.groups.get(value);
|
|
15
|
-
if (!projected) {
|
|
16
|
-
const { versionNumber: _, ...visible } = value;
|
|
17
|
-
projected = visible;
|
|
18
|
-
this.groups.set(value, projected);
|
|
19
|
-
}
|
|
20
|
-
return projected;
|
|
21
|
-
}
|
|
22
|
-
present = createMessagePresenter();
|
|
23
|
-
redactedQuotes = new WeakMap();
|
|
24
|
-
messageLists = new WeakMap();
|
|
25
|
-
messages(items) {
|
|
26
|
-
let values = this.messageLists.get(items);
|
|
27
|
-
if (!values) {
|
|
28
|
-
const recalled = new Set(items.filter(item => item.recalled).map(item => item.id));
|
|
29
|
-
values = items.map(item => {
|
|
30
|
-
const value = this.present(item);
|
|
31
|
-
if (!value.quote?.messageId || !recalled.has(value.quote.messageId))
|
|
32
|
-
return value;
|
|
33
|
-
let hidden = this.redactedQuotes.get(value);
|
|
34
|
-
if (!hidden) {
|
|
35
|
-
hidden = { ...value, quote: { ...value.quote, content: "", recalled: true } };
|
|
36
|
-
this.redactedQuotes.set(value, hidden);
|
|
37
|
-
}
|
|
38
|
-
return hidden;
|
|
39
|
-
});
|
|
40
|
-
this.messageLists.set(items, values);
|
|
41
|
-
}
|
|
42
|
-
return values;
|
|
43
|
-
}
|
|
44
|
-
views = new Map();
|
|
45
|
-
summary;
|
|
46
|
-
targets = new Map();
|
|
47
|
-
errors = new Map();
|
|
48
|
-
listeners = new Map();
|
|
49
|
-
closed = false;
|
|
50
|
-
constructor(advanced, options) {
|
|
51
|
-
this.advanced = advanced;
|
|
52
|
-
this.options = options;
|
|
53
|
-
}
|
|
54
|
-
async operation(run) { try {
|
|
55
|
-
return await run();
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
throw asChatError(error);
|
|
59
|
-
} }
|
|
60
|
-
async deny(threadId, failure) {
|
|
61
|
-
this.errors.set(threadId, failure);
|
|
62
|
-
this.targets.delete(threadId);
|
|
63
|
-
const retiring = this.advanced.revokeAccess();
|
|
64
|
-
this.notify(threadId);
|
|
65
|
-
try {
|
|
66
|
-
await retiring;
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
failure.cause = new AggregateError([failure.cause, error], "Access withdrawn; cache cleanup incomplete");
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
threadOperation(threadId, run) {
|
|
73
|
-
return this.operation(async () => {
|
|
74
|
-
const denied = threadId ? this.errors.get(threadId) : undefined;
|
|
75
|
-
if (denied?.code === "access-denied")
|
|
76
|
-
throw denied;
|
|
77
|
-
try {
|
|
78
|
-
return await run();
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
const failure = asChatError(error);
|
|
82
|
-
if (threadId && failure.code === "access-denied")
|
|
83
|
-
await this.deny(threadId, failure);
|
|
84
|
-
throw failure;
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
start() { return this.operation(() => this.options.connection ? this.advanced.initialize() : this.advanced.start()); }
|
|
89
|
-
getConversation(target) {
|
|
90
|
-
return this.operation(async () => {
|
|
91
|
-
const id = await this.advanced.getConversation(target);
|
|
92
|
-
const value = target.groupId !== undefined ? { kind: "group", targetId: target.groupId } : { kind: "dm", targetId: target.peerId };
|
|
93
|
-
this.targets.delete(id);
|
|
94
|
-
this.targets.set(id, value);
|
|
95
|
-
if (this.targets.size > 256)
|
|
96
|
-
this.targets.delete(this.targets.keys().next().value);
|
|
97
|
-
this.notify(id);
|
|
98
|
-
return id;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
getSnapshot(threadId) {
|
|
102
|
-
if (threadId === undefined) {
|
|
103
|
-
const source = this.advanced.getSnapshot(), previous = this.summary;
|
|
104
|
-
const threads = source.threads.filter(thread => this.errors.get(thread.threadId)?.code !== "access-denied").map(thread => ({ threadId: thread.threadId, lastMessage: this.messages(this.advanced.getSnapshot(thread.threadId).timeline).at(-1), group: this.group(thread.group), unread: thread.unread, outgoingCount: thread.outgoingCount }));
|
|
105
|
-
const unreadMessageCount = source.unreadMessageCount === undefined ? undefined : threads.reduce((sum, thread) => sum + (thread.unread?.unreadMessageCount ?? 0), 0);
|
|
106
|
-
const badgeCount = source.badgeCount === undefined ? undefined : threads.reduce((sum, thread) => sum + (thread.unread?.badgeCount ?? 0), 0);
|
|
107
|
-
if (previous && previous.connection === source.connection && previous.badgeCount === badgeCount && previous.unreadMessageCount === unreadMessageCount && previous.threads.length === threads.length && threads.every((thread, i) => { const old = previous.threads[i]; return thread.threadId === old.threadId && thread.lastMessage === old.lastMessage && thread.group === old.group && thread.unread === old.unread && thread.outgoingCount === old.outgoingCount; }))
|
|
108
|
-
return previous;
|
|
109
|
-
return this.summary = { threads, connection: source.connection, badgeCount, unreadMessageCount };
|
|
110
|
-
}
|
|
111
|
-
const source = this.advanced.getSnapshot(threadId), previous = this.views.get(threadId);
|
|
112
|
-
const error = this.closed ? undefined : this.errors.get(threadId), denied = error?.code === "access-denied";
|
|
113
|
-
const target = this.closed || !this.advanced.active || denied ? undefined : this.advanced.getConversationTarget(threadId) ?? this.targets.get(threadId);
|
|
114
|
-
const conversation = previous?.conversation?.kind === target?.kind && previous?.conversation?.targetId === target?.targetId ? previous?.conversation : target ? { kind: target.kind, targetId: target.targetId } : undefined;
|
|
115
|
-
// Partial migrated rows never masquerade as a trusted recent window.
|
|
116
|
-
const next = { threadId, conversation, messages: !denied && source.complete ? this.messages(source.timeline) : emptyMessages, ready: !denied && source.complete, hasEarlier: !denied && source.hasEarlier, unread: denied ? undefined : source.unread, group: denied ? undefined : this.group(source.group), readCounts: denied ? undefined : source.readCounts, error };
|
|
117
|
-
if (previous && Object.keys(next).every(key => next[key] === previous[key]))
|
|
118
|
-
return previous;
|
|
119
|
-
this.views.set(threadId, next);
|
|
120
|
-
return next;
|
|
121
|
-
}
|
|
122
|
-
async getMessages(threadId, query = {}) {
|
|
123
|
-
try {
|
|
124
|
-
if (Object.keys(query).some(key => !["earlier", "all", "refresh"].includes(key)))
|
|
125
|
-
throw new ChatError("invalid-input", "Unsupported message query");
|
|
126
|
-
const page = await this.advanced.getMessages(threadId, query);
|
|
127
|
-
if (!page.complete)
|
|
128
|
-
throw new ChatError("unavailable", "Messages are not ready; retry when connected");
|
|
129
|
-
this.errors.delete(threadId);
|
|
130
|
-
this.notify(threadId);
|
|
131
|
-
return this.getSnapshot(threadId);
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
const failure = asChatError(error);
|
|
135
|
-
if (!this.closed) {
|
|
136
|
-
if (failure.code === "access-denied")
|
|
137
|
-
await this.deny(threadId, failure);
|
|
138
|
-
else {
|
|
139
|
-
this.errors.set(threadId, failure);
|
|
140
|
-
this.notify(threadId);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
throw failure;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
sendMessage(threadId, content, options = {}) {
|
|
147
|
-
return this.threadOperation(threadId, async () => {
|
|
148
|
-
if (content && ("groupCreate" in content || "groupUpdate" in content))
|
|
149
|
-
throw new ChatError("invalid-input", "Use createGroup or updateGroup for group changes");
|
|
150
|
-
const result = await this.advanced.sendMessage(threadId, content, options);
|
|
151
|
-
return { id: result.hash, timestamp: result.t };
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
retryMessage(messageId) { return this.operation(async () => { const result = await this.advanced.retryMessage(messageId); return { id: result.hash, timestamp: result.t }; }); }
|
|
155
|
-
subscribe(target, callback, options = {}) {
|
|
156
|
-
const global = typeof target === "function", key = global ? "" : target;
|
|
157
|
-
const config = global ? callback : options;
|
|
158
|
-
const read = () => global ? this.getSnapshot() : this.getSnapshot(key);
|
|
159
|
-
let previous = read();
|
|
160
|
-
const emit = () => {
|
|
161
|
-
const value = read();
|
|
162
|
-
const fields = config?.fields;
|
|
163
|
-
const changed = fields ? fields.some(field => previous[field] !== value[field]) : value !== previous;
|
|
164
|
-
previous = value;
|
|
165
|
-
if (changed)
|
|
166
|
-
(global ? target : callback)(value);
|
|
167
|
-
};
|
|
168
|
-
let listeners = this.listeners.get(key);
|
|
169
|
-
if (!listeners) {
|
|
170
|
-
listeners = new Set();
|
|
171
|
-
this.listeners.set(key, listeners);
|
|
172
|
-
}
|
|
173
|
-
listeners.add(emit);
|
|
174
|
-
const off = global ? this.advanced.subscribeChanges(emit) : this.advanced.subscribeChanges(key, emit, this.sessionFields(options.fields));
|
|
175
|
-
const dispose = () => { off(); listeners.delete(emit); if (!listeners.size)
|
|
176
|
-
this.listeners.delete(key); };
|
|
177
|
-
if (config?.emitCurrent !== false) {
|
|
178
|
-
try {
|
|
179
|
-
(global ? target : callback)(previous);
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
dispose();
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return dispose;
|
|
187
|
-
}
|
|
188
|
-
sessionFields(fields) {
|
|
189
|
-
return fields ? [...new Set(fields.flatMap(field => field === "messages" ? ["timeline", "complete"] : field === "ready" ? ["complete"] : field === "conversation" ? ["messages", "group"] : field === "error" || field === "threadId" ? [] : [field]))] : undefined;
|
|
190
|
-
}
|
|
191
|
-
notify(threadId) { this.listeners.get(threadId)?.forEach(listener => listener()); this.listeners.get("")?.forEach(listener => listener()); }
|
|
192
|
-
observeMessages(threadId, ids, event) { return this.threadOperation(threadId, async () => { await this.advanced.observeMessages(threadId, ids, event); }); }
|
|
193
|
-
markRead(threadId) { return this.threadOperation(threadId, async () => { await this.advanced.markRead(threadId); }); }
|
|
194
|
-
getReaders(threadId, messageId, page = {}, signal) {
|
|
195
|
-
return this.threadOperation(threadId, async () => {
|
|
196
|
-
if (Object.keys(page).some(key => !["afterProfileId", "revision"].includes(key)))
|
|
197
|
-
throw new ChatError("invalid-input", "Unsupported reader query");
|
|
198
|
-
return this.advanced.getReaders(threadId, messageId, page, signal);
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
watchReaders(threadId, ids, receiver) { return this.threadOperation(threadId, () => this.advanced.observeMessages(threadId, ids, "receipts", receiver)); }
|
|
202
|
-
handleReach(event) { return this.operation(() => this.advanced.handleReach(event)); }
|
|
203
|
-
getVoiceAttachment(threadId, messageId) { return this.threadOperation(threadId, () => this.advanced.getVoiceAttachment(threadId, messageId)); }
|
|
204
|
-
getGroup(threadId, options = {}) { return this.threadOperation(threadId, async () => this.group(await this.advanced.getGroup(threadId, options))); }
|
|
205
|
-
createGroup(input) {
|
|
206
|
-
return this.operation(async () => {
|
|
207
|
-
const groupId = input.groupId ?? crypto.randomUUID(), threadId = await this.getConversation({ groupId });
|
|
208
|
-
const group = await this.advanced.createGroup({ id: threadId, kind: "group", targetId: groupId, teamId: this.options.teamId }, { title: input.title, members: input.members });
|
|
209
|
-
return { ...this.group(group), threadId };
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
updateGroup(threadId, changes) { return this.threadOperation(threadId, async () => this.group(await this.advanced.updateGroup(threadId, changes))); }
|
|
213
|
-
revokeAccess() { return this.operation(() => this.advanced.revokeAccess()); }
|
|
214
|
-
close() { this.closed = true; return this.operation(() => this.advanced.close()).finally(() => { this.errors.clear(); this.targets.clear(); this.listeners.clear(); this.views.clear(); this.summary = undefined; this.present = createMessagePresenter(); this.messageLists = new WeakMap(); this.groups = new WeakMap(); this.redactedQuotes = new WeakMap(); }); }
|
|
215
|
-
}
|
|
1
|
+
import { createPublicChat } from "./internal/createPublicChat.js";
|
|
2
|
+
import { ChatError } from "./chatError.js";
|
|
3
|
+
export { ChatError } from "./chatError.js";
|
|
216
4
|
export function createChat(options) {
|
|
217
|
-
const
|
|
218
|
-
|
|
5
|
+
const allowed = ["appId", "accountId", "profileId", "teamId", "threadId", "endpoint", "uploadEndpoint", "clientInstanceId", "fetch", "onError", "token", "getToken"];
|
|
6
|
+
if (Object.keys(options).some(key => !allowed.includes(key)))
|
|
7
|
+
throw new ChatError("invalid-input", "Unsupported chat option; transport integration belongs to /advanced");
|
|
8
|
+
if (options.token !== undefined && options.getToken !== undefined)
|
|
9
|
+
throw new ChatError("invalid-input", "Select one credential source");
|
|
10
|
+
return createPublicChat({ ...options, onError: error => options.onError?.(error) });
|
|
219
11
|
}
|