@pmate/chat-sdk 0.12.0-next.8 → 0.12.0-next.80

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.
Files changed (111) hide show
  1. package/README.md +170 -83
  2. package/lib/advanced.d.ts +10 -0
  3. package/lib/advanced.js +10 -0
  4. package/lib/chat.d.ts +5 -0
  5. package/lib/chat.js +11 -0
  6. package/lib/chatContract.d.ts +151 -0
  7. package/lib/chatContract.js +1 -0
  8. package/lib/chatError.d.ts +14 -0
  9. package/lib/chatError.js +16 -0
  10. package/lib/chatTypes.d.ts +127 -0
  11. package/lib/chatTypes.js +1 -0
  12. package/lib/commands.d.ts +20 -0
  13. package/lib/commands.js +12 -0
  14. package/lib/components/ChatInputDesktop.js +5 -4
  15. package/lib/components/ChatInputMobile.js +5 -4
  16. package/lib/components/CommandMenu.d.ts +15 -0
  17. package/lib/components/CommandMenu.js +10 -0
  18. package/lib/credentials.d.ts +26 -0
  19. package/lib/credentials.js +110 -0
  20. package/lib/fileMetadata.d.ts +9 -0
  21. package/lib/fileMetadata.js +18 -0
  22. package/lib/framework.d.ts +11 -0
  23. package/lib/framework.js +12 -0
  24. package/lib/history.d.ts +4 -0
  25. package/lib/history.js +8 -0
  26. package/lib/hooks/useChatSnapshot.d.ts +4 -0
  27. package/lib/hooks/useChatSnapshot.js +9 -0
  28. package/lib/hooks/useComposerSuggestions.d.ts +50 -0
  29. package/lib/hooks/useComposerSuggestions.js +70 -0
  30. package/lib/index.d.ts +5 -0
  31. package/lib/index.js +5 -0
  32. package/lib/internal/chatEngine.d.ts +130 -0
  33. package/lib/internal/chatEngine.js +284 -0
  34. package/lib/internal/chatRegistry.d.ts +4 -0
  35. package/lib/internal/chatRegistry.js +4 -0
  36. package/lib/internal/createPublicChat.d.ts +4 -0
  37. package/lib/internal/createPublicChat.js +52 -0
  38. package/lib/messageView.d.ts +6 -0
  39. package/lib/messageView.js +103 -0
  40. package/lib/runtime/batchedReadIndex.d.ts +16 -3
  41. package/lib/runtime/batchedReadIndex.js +50 -4
  42. package/lib/runtime/cachedMessage.d.ts +12 -0
  43. package/lib/runtime/cachedMessage.js +18 -0
  44. package/lib/runtime/chatClient.d.ts +7 -0
  45. package/lib/runtime/chatClient.js +14 -3
  46. package/lib/runtime/chatError.d.ts +5 -0
  47. package/lib/runtime/chatError.js +20 -0
  48. package/lib/runtime/chatSession.d.ts +128 -13
  49. package/lib/runtime/chatSession.js +759 -48
  50. package/lib/runtime/connectivity.d.ts +4 -0
  51. package/lib/runtime/connectivity.js +8 -0
  52. package/lib/runtime/conversationDirectory.d.ts +37 -0
  53. package/lib/runtime/conversationDirectory.js +116 -0
  54. package/lib/runtime/conversationRead.d.ts +13 -0
  55. package/lib/runtime/conversationRead.js +34 -0
  56. package/lib/runtime/conversationReading.d.ts +28 -0
  57. package/lib/runtime/conversationReading.js +47 -0
  58. package/lib/runtime/createChat.d.ts +11 -7
  59. package/lib/runtime/createChat.js +46 -23
  60. package/lib/runtime/durableSender.d.ts +97 -14
  61. package/lib/runtime/durableSender.js +786 -71
  62. package/lib/runtime/groupClient.d.ts +91 -0
  63. package/lib/runtime/groupClient.js +380 -0
  64. package/lib/runtime/groupDenial.d.ts +2 -0
  65. package/lib/runtime/groupDenial.js +4 -0
  66. package/lib/runtime/index.d.ts +7 -0
  67. package/lib/runtime/index.js +5 -0
  68. package/lib/runtime/indexedDbRecovery.d.ts +4 -0
  69. package/lib/runtime/indexedDbRecovery.js +27 -0
  70. package/lib/runtime/messageClient.d.ts +35 -8
  71. package/lib/runtime/messageClient.js +364 -66
  72. package/lib/runtime/messageControls.d.ts +19 -0
  73. package/lib/runtime/messageControls.js +44 -0
  74. package/lib/runtime/messageLaunch.d.ts +44 -0
  75. package/lib/runtime/messageLaunch.js +137 -0
  76. package/lib/runtime/messageQuotes.d.ts +3 -0
  77. package/lib/runtime/messageQuotes.js +17 -0
  78. package/lib/runtime/messageReaders.d.ts +5 -1
  79. package/lib/runtime/messageReaders.js +16 -5
  80. package/lib/runtime/messageStore.d.ts +43 -2
  81. package/lib/runtime/messageStore.js +248 -14
  82. package/lib/runtime/messageTimeline.d.ts +50 -0
  83. package/lib/runtime/messageTimeline.js +120 -0
  84. package/lib/runtime/outboxSchema.d.ts +5 -0
  85. package/lib/runtime/outboxSchema.js +59 -0
  86. package/lib/runtime/outgoingContent.d.ts +94 -0
  87. package/lib/runtime/outgoingContent.js +118 -0
  88. package/lib/runtime/readBatchJournal.d.ts +35 -0
  89. package/lib/runtime/readBatchJournal.js +210 -0
  90. package/lib/runtime/readCounts.d.ts +15 -1
  91. package/lib/runtime/readCounts.js +97 -9
  92. package/lib/runtime/readIndex.d.ts +68 -7
  93. package/lib/runtime/readIndex.js +460 -22
  94. package/lib/runtime/readSchema.d.ts +2 -0
  95. package/lib/runtime/readSchema.js +58 -0
  96. package/lib/runtime/readSync.d.ts +1 -0
  97. package/lib/runtime/readSync.js +26 -9
  98. package/lib/runtime/readingClient.d.ts +19 -0
  99. package/lib/runtime/readingClient.js +131 -19
  100. package/lib/runtime/readingContent.d.ts +5 -0
  101. package/lib/runtime/readingContent.js +23 -0
  102. package/lib/runtime/responseCache.d.ts +40 -5
  103. package/lib/runtime/responseCache.js +182 -44
  104. package/lib/runtime/socialReadClient.d.ts +9 -4
  105. package/lib/runtime/socialReadClient.js +12 -8
  106. package/lib/runtime/voiceUpload.d.ts +2 -0
  107. package/lib/runtime/voiceUpload.js +27 -0
  108. package/lib/snapshot.d.ts +36 -0
  109. package/lib/snapshot.js +50 -0
  110. package/lib/types/chatInput.d.ts +3 -0
  111. package/package.json +32 -2
@@ -0,0 +1,4 @@
1
+ /** Browser connectivity is advisory: only suppress network errors while the
2
+ * browser explicitly reports offline. Permission and data errors stay visible. */
3
+ export declare function isOffline(): boolean;
4
+ export declare function isOfflineNetworkFailure(error: unknown): boolean;
@@ -0,0 +1,8 @@
1
+ /** Browser connectivity is advisory: only suppress network errors while the
2
+ * browser explicitly reports offline. Permission and data errors stay visible. */
3
+ export function isOffline() {
4
+ return typeof navigator !== "undefined" && navigator.onLine === false;
5
+ }
6
+ export function isOfflineNetworkFailure(error) {
7
+ return isOffline() && (error instanceof TypeError || error?.name === "AbortError");
8
+ }
@@ -0,0 +1,37 @@
1
+ import type { ResponseCacheAccess } from "./responseCache.js";
2
+ import { type MessageScope } from "./messageStore.js";
3
+ import { type ChatConversation } from "./outgoingContent.js";
4
+ export type ConversationTarget = {
5
+ peerId: string;
6
+ groupId?: never;
7
+ } | {
8
+ groupId: string;
9
+ peerId?: never;
10
+ };
11
+ /** Canonical target identities only; rosters always belong to GroupClient. */
12
+ export declare class ConversationDirectory {
13
+ private scope;
14
+ private access;
15
+ private cache;
16
+ private memory;
17
+ private pending;
18
+ private queue;
19
+ private writes;
20
+ private version;
21
+ private authority;
22
+ private scheduled;
23
+ private closed;
24
+ private retired;
25
+ constructor(scope: MessageScope, access: ResponseCacheAccess);
26
+ private authorize;
27
+ private assertOpen;
28
+ private remember;
29
+ create(target: ConversationTarget): Promise<string>;
30
+ put(thread: ChatConversation): Promise<ChatConversation>;
31
+ private persist;
32
+ get(id: string): Promise<ChatConversation | undefined>;
33
+ private lookup;
34
+ private flush;
35
+ retire(version: number): Promise<void>;
36
+ close(): Promise<void>;
37
+ }
@@ -0,0 +1,116 @@
1
+ import { IndexedDbResponseCache } from "./responseCache.js";
2
+ import { messageScopeKey } from "./messageStore.js";
3
+ import { validateConversation } from "./outgoingContent.js";
4
+ import { dmThreadId, groupThreadId } from "./thread.js";
5
+ /** Canonical target identities only; rosters always belong to GroupClient. */
6
+ export class ConversationDirectory {
7
+ scope;
8
+ access;
9
+ cache;
10
+ memory = new Map();
11
+ pending = new Map();
12
+ queue = [];
13
+ writes = new Map();
14
+ version;
15
+ authority;
16
+ scheduled = false;
17
+ closed = false;
18
+ retired = false;
19
+ constructor(scope, access) {
20
+ this.scope = scope;
21
+ this.access = access;
22
+ this.cache = new IndexedDbResponseCache(`conversations:${messageScopeKey(scope)}`, { access: () => this.authorize(), entries: 256, recordBytes: 4096, retainAll: true });
23
+ }
24
+ authorize() { return this.authority ??= this.access().finally(() => { this.authority = undefined; }); }
25
+ assertOpen() { if (this.retired)
26
+ throw new Error("CHAT_ACCESS_REVOKED"); if (this.closed)
27
+ throw new Error("CHAT_SESSION_CLOSED"); }
28
+ remember(thread) {
29
+ this.memory.delete(thread.id);
30
+ this.memory.set(thread.id, thread);
31
+ if (this.memory.size > 256)
32
+ this.memory.delete(this.memory.keys().next().value);
33
+ return thread;
34
+ }
35
+ async create(target) {
36
+ const kind = target.groupId !== undefined ? "group" : "dm", targetId = target.groupId ?? target.peerId;
37
+ if (target.groupId !== undefined && target.peerId !== undefined)
38
+ throw new Error("INVALID_MESSAGE_CONVERSATION");
39
+ const id = kind === "group" ? groupThreadId(this.scope.appId, targetId, this.scope.teamId) : dmThreadId(this.scope.appId, this.scope.profileId, targetId, this.scope.teamId);
40
+ await this.put({ id, kind, targetId, teamId: this.scope.teamId });
41
+ return id;
42
+ }
43
+ put(thread) {
44
+ this.assertOpen();
45
+ validateConversation(this.scope, thread);
46
+ const existing = this.writes.get(thread.id);
47
+ if (existing)
48
+ return existing;
49
+ if (this.writes.size >= 128)
50
+ return Promise.reject(new Error("CONVERSATION_BACKPRESSURE"));
51
+ const task = this.persist(thread).finally(() => this.writes.delete(thread.id));
52
+ this.writes.set(thread.id, task);
53
+ return task;
54
+ }
55
+ async persist(thread) {
56
+ this.assertOpen();
57
+ validateConversation(this.scope, thread);
58
+ const value = { id: thread.id, kind: thread.kind, targetId: thread.targetId, teamId: this.scope.teamId };
59
+ const row = await this.lookup(thread.id), previous = row.value;
60
+ if (previous) {
61
+ if (previous.kind !== value.kind || previous.targetId !== value.targetId)
62
+ throw new Error("MESSAGE_CONVERSATION_MISMATCH");
63
+ return previous;
64
+ }
65
+ await this.cache.write(thread.id, value, row.epoch);
66
+ this.assertOpen();
67
+ return this.remember(value);
68
+ }
69
+ async get(id) { return (await this.lookup(id)).value; }
70
+ async lookup(id) {
71
+ this.assertOpen();
72
+ const authority = await this.authorize();
73
+ this.assertOpen();
74
+ if (!authority.allowed)
75
+ throw new Error("CHAT_ACCESS_REVOKED");
76
+ if (this.version !== undefined && this.version !== authority.version)
77
+ this.memory.clear();
78
+ this.version = authority.version;
79
+ const known = this.memory.get(id);
80
+ if (known)
81
+ return { value: this.remember(known), epoch: 0 };
82
+ const existing = this.pending.get(id);
83
+ if (existing)
84
+ return existing;
85
+ if (this.pending.size >= 128)
86
+ throw new Error("CONVERSATION_BACKPRESSURE");
87
+ const task = new Promise((resolve, reject) => this.queue.push({ id, resolve, reject })).finally(() => this.pending.delete(id));
88
+ this.pending.set(id, task);
89
+ if (!this.scheduled) {
90
+ this.scheduled = true;
91
+ queueMicrotask(() => { this.scheduled = false; void this.flush(); });
92
+ }
93
+ return task;
94
+ }
95
+ async flush() {
96
+ const batch = this.queue.splice(0, 128);
97
+ try {
98
+ const rows = await this.cache.readMany(batch.map(item => item.id));
99
+ this.assertOpen();
100
+ for (const item of batch) {
101
+ const row = rows.get(item.id), value = row.cached?.value;
102
+ if (value) {
103
+ if (value.id !== item.id)
104
+ throw new Error("MESSAGE_CONVERSATION_MISMATCH");
105
+ validateConversation(this.scope, value);
106
+ }
107
+ item.resolve({ value: value ? this.remember(value) : undefined, epoch: row.epoch });
108
+ }
109
+ }
110
+ catch (error) {
111
+ batch.forEach(item => item.reject(error));
112
+ }
113
+ }
114
+ retire(version) { this.retired = true; this.memory.clear(); return this.cache.retire(version); }
115
+ async close() { this.closed = true; this.memory.clear(); await Promise.allSettled([...this.pending.values(), ...this.writes.values()]); await this.cache.close(); }
116
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReadIndexState } from "./readIndex.js";
2
+ /** Optional observations whose index metadata may still be in flight. */
3
+ export type ConversationReadMessage = {
4
+ id: string;
5
+ threadId: string;
6
+ mine?: boolean;
7
+ from?: string;
8
+ recalled?: boolean;
9
+ readPolicy?: "visible" | "explicit";
10
+ kind?: string;
11
+ };
12
+ /** Explicitly mark ordinary text history read; unheard audio stays unread. */
13
+ export declare function recordConversationRead(state: ReadIndexState, conversationId: string, messages?: readonly ConversationReadMessage[], operationId?: () => `${string}-${string}-${string}-${string}-${string}`): ReadIndexState;
@@ -0,0 +1,34 @@
1
+ import { recordReadIntent, sourcesInConversation } from "./readIndex.js";
2
+ /** Explicitly mark ordinary text history read; unheard audio stays unread. */
3
+ export function recordConversationRead(state, conversationId, messages = [], operationId = () => crypto.randomUUID()) {
4
+ if (!conversationId)
5
+ throw new Error("INVALID_READING_THREAD");
6
+ const sources = sourcesInConversation(state, conversationId);
7
+ if (new Set(sources.map(source => source.teamId)).size > 1)
8
+ throw new Error("READ_CONVERSATION_TEAM_MISMATCH");
9
+ let next = state;
10
+ const indexedIds = new Set();
11
+ for (const source of sources) {
12
+ const incoming = source.messages.filter(message => {
13
+ indexedIds.add(message.id);
14
+ return !message.recalled && message.from !== state.profileId && message.recipients.includes(state.profileId);
15
+ });
16
+ const latest = incoming.filter(message => message.readPolicy === "visible" && message.seq > source.readThroughSeq)
17
+ .reduce((last, message) => !last || message.seq > last.seq ? message : last, undefined);
18
+ if (latest)
19
+ next = recordReadIntent(next, { operationId: operationId(), threadId: source.id, messageId: latest.id, readPolicy: "visible" });
20
+ }
21
+ // Live/history messages can precede their read-index metadata. Persist their
22
+ // observations so leaving the page or reloading cannot resurrect the badge.
23
+ const observed = new Set(next.observedReadIntents?.filter(intent => intent.threadId === conversationId).map(intent => intent.messageId));
24
+ for (const message of messages) {
25
+ if (message.threadId !== conversationId || message.mine || message.from === state.profileId || message.recalled || indexedIds.has(message.id) || observed.has(message.id))
26
+ continue;
27
+ const readPolicy = message.readPolicy ?? (message.kind === "voice" ? "explicit" : "visible");
28
+ if (message.kind === "voice" || readPolicy === "explicit")
29
+ continue;
30
+ next = recordReadIntent(next, { operationId: operationId(), threadId: conversationId, messageId: message.id, readPolicy });
31
+ observed.add(message.id);
32
+ }
33
+ return next;
34
+ }
@@ -0,0 +1,28 @@
1
+ import { unreadInConversation, type ReadIndexState } from "./readIndex.js";
2
+ export type ReadingCandidate = {
3
+ id: string;
4
+ threadId: string;
5
+ from?: string;
6
+ mine?: boolean;
7
+ recalled?: boolean;
8
+ unread?: boolean;
9
+ attentionEligible?: boolean;
10
+ readPolicy?: "visible" | "explicit";
11
+ };
12
+ export type MessageReadingState = {
13
+ seq?: number;
14
+ read: boolean;
15
+ readPolicy: "visible" | "explicit";
16
+ };
17
+ export type ConversationReadingView = {
18
+ messageReading: Readonly<Record<string, MessageReadingState>>;
19
+ unread: ReturnType<typeof unreadInConversation> | undefined;
20
+ badgeCount: number | undefined;
21
+ };
22
+ /** One projection for indexed authority and observations awaiting indexing. A
23
+ * directory badge is optional compatibility metadata, never proof of coverage. */
24
+ export declare function projectConversationReading(state: ReadIndexState | undefined, conversation: {
25
+ id: string;
26
+ kind: "dm" | "group";
27
+ badgeCount?: number;
28
+ }, candidates?: readonly ReadingCandidate[]): ConversationReadingView;
@@ -0,0 +1,47 @@
1
+ import { sourcesInConversation, unreadInConversation } from "./readIndex.js";
2
+ /** One projection for indexed authority and observations awaiting indexing. A
3
+ * directory badge is optional compatibility metadata, never proof of coverage. */
4
+ export function projectConversationReading(state, conversation, candidates = []) {
5
+ const unique = new Map(candidates.filter(message => message.threadId === conversation.id).map(message => [message.id, message]));
6
+ const hidden = new Map();
7
+ for (const message of unique.values())
8
+ if (message.recalled && message.from)
9
+ hidden.set(message.id, new Set([message.from]));
10
+ const sources = state ? sourcesInConversation(state, conversation.id) : [];
11
+ const sourceIds = new Set([conversation.id, ...sources.map(source => source.id)]);
12
+ const indexed = new Set(), messageReading = Object.create(null);
13
+ for (const source of sources)
14
+ for (const message of source.messages) {
15
+ indexed.add(message.id);
16
+ messageReading[message.id] = { seq: message.seq, readPolicy: message.readPolicy, read: message.recalled || hidden.get(message.id)?.has(message.from) === true || message.from === state.profileId || !message.recipients.includes(state.profileId) || (message.readPolicy === "explicit" ? source.explicitReadMessageIds.includes(message.id) : message.seq <= source.readThroughSeq) };
17
+ }
18
+ const observed = new Map();
19
+ for (const intent of state?.observedReadIntents ?? [])
20
+ if (sourceIds.has(intent.threadId)) {
21
+ if (!observed.has(intent.messageId))
22
+ observed.set(intent.messageId, new Set());
23
+ observed.get(intent.messageId).add(intent.readPolicy);
24
+ }
25
+ let pendingMessages = 0, pendingMentions = 0, observedBadges = 0;
26
+ for (const message of unique.values()) {
27
+ const incoming = !message.mine && message.from !== state?.profileId && !message.recalled;
28
+ const attention = conversation.kind === "dm" || message.attentionEligible === true;
29
+ const policy = message.readPolicy ?? "visible";
30
+ const acknowledged = observed.get(message.id)?.has(policy) === true;
31
+ if (!indexed.has(message.id) && acknowledged)
32
+ messageReading[message.id] = { read: true, readPolicy: policy };
33
+ if (!incoming)
34
+ continue;
35
+ if (acknowledged && attention)
36
+ observedBadges++;
37
+ if (indexed.has(message.id) || acknowledged || message.unread !== true)
38
+ continue;
39
+ pendingMessages++;
40
+ if (message.attentionEligible === true)
41
+ pendingMentions++;
42
+ }
43
+ const authoritative = sources.length && state ? unreadInConversation(state, conversation.id, hidden) : undefined;
44
+ const unread = authoritative ? { unreadMessageCount: authoritative.unreadMessageCount + pendingMessages, unreadMentionCount: authoritative.unreadMentionCount + pendingMentions, badgeCount: authoritative.badgeCount + (conversation.kind === "group" ? pendingMentions : pendingMessages) } : undefined;
45
+ const pendingBadge = conversation.kind === "group" ? pendingMentions : pendingMessages;
46
+ return { messageReading, unread, badgeCount: unread?.badgeCount ?? (conversation.badgeCount === undefined ? undefined : Math.max(Math.max(0, conversation.badgeCount - observedBadges), pendingBadge)) };
47
+ }
@@ -1,16 +1,18 @@
1
+ import { type TokenProvider } from "../credentials.js";
1
2
  import { ChatSession } from "./chatSession.js";
2
- import type { ChatReaderSocialMessage } from "./readerClient.js";
3
3
  import type { ChatClient, ChatClientOptions } from "./chatClient.js";
4
- export type CachedChatMessage = ChatReaderSocialMessage & {
5
- id: string;
6
- timestamp: number;
7
- };
4
+ import { type CachedChatMessage } from "./cachedMessage.js";
5
+ export type { CachedChatMessage } from "./cachedMessage.js";
8
6
  export type CreateChatOptions = {
9
7
  appId: string;
10
8
  accountId: string;
11
9
  profileId: string;
12
10
  teamId: string;
13
- token: string;
11
+ token?: string;
12
+ getToken?: TokenProvider;
13
+ /** Limit reconnect reconciliation for a dedicated conversation document. */
14
+ threadId?: string;
15
+ uploadEndpoint?: string;
14
16
  endpoint?: string;
15
17
  clientInstanceId?: string;
16
18
  fetch?: typeof fetch;
@@ -21,4 +23,6 @@ export type CreateChatOptions = {
21
23
  onReach?: ChatClientOptions["onReach"];
22
24
  };
23
25
  /** Standard browser entry: storage, paging, realtime and reading share one identity. */
24
- export declare function createChat(options: CreateChatOptions): ChatSession<CachedChatMessage>;
26
+ export declare function createChatSession(options: CreateChatOptions): ChatSession<CachedChatMessage>;
27
+ /** @deprecated Advanced compatibility factory. Use /chat createChat, or createChatSession for raw sessions. */
28
+ export declare const createChat: typeof createChatSession;
@@ -1,39 +1,62 @@
1
+ import { ChatCredentials } from "../credentials.js";
2
+ import { voiceUploader } from "./voiceUpload.js";
3
+ import { isOffline } from "./connectivity.js";
4
+ import { isReadingControl, messageRequiresPlayback, messageMentionsProfile } from "./readingContent.js";
1
5
  import { ChatSession } from "./chatSession.js";
2
6
  import { SocialReadClient } from "./socialReadClient.js";
3
- import { chatTeamId } from "./teamScope.js";
4
- import { dmThreadId, groupThreadId, isValidChatTeamId } from "./thread.js";
7
+ import { isValidChatTeamId } from "./thread.js";
8
+ import { decodeCachedChatMessage } from "./cachedMessage.js";
5
9
  /** Standard browser entry: storage, paging, realtime and reading share one identity. */
6
- export function createChat(options) {
7
- const { appId, accountId, profileId, teamId, token, onError } = options;
10
+ export function createChatSession(options) {
11
+ const { appId, accountId, profileId, teamId, onError } = options;
12
+ const token = options.token ?? "";
13
+ if (!token && !options.getToken)
14
+ throw new Error("INVALID_CHAT_CREDENTIALS");
8
15
  if (!isValidChatTeamId(teamId))
9
16
  throw new Error("INVALID_CHAT_TEAM");
10
17
  const endpoint = (options.endpoint ?? "https://hub.pmate.chat").replace(/\/+$/, "");
11
18
  const http = endpoint.replace(/^ws/, "http"), ws = endpoint.replace(/^http/, "ws");
12
- const reader = new SocialReadClient({ appId, profileId, token, hubHttpUrl: http, fetch: options.fetch });
19
+ const credentials = options.getToken ? new ChatCredentials(options.getToken, new URL(http).origin, options.fetch) : undefined;
20
+ const fetcher = credentials?.fetch ?? options.fetch;
21
+ const reader = new SocialReadClient({ appId, profileId, token, hubHttpUrl: http, fetch: fetcher });
13
22
  const page = async (threadId, signal, cursor = {}) => {
14
23
  const result = await reader.messagePage({ threadId, teamId, limit: 100, ...cursor }, signal);
15
24
  return { ...result, messages: result.messages.map(message => ({ ...message, id: message.messageId, timestamp: message.serverCreatedAt })) };
16
25
  };
17
26
  return new ChatSession({
18
27
  sharedConnection: options.connection,
19
- messages: { scope: { appId, accountId, profileId, teamId }, transport: { load: (thread, signal) => page(thread, signal), changes: (thread, after, signal) => page(thread, signal, { after }), older: (thread, before, signal) => page(thread, signal, { before }) }, onError },
20
- reading: { appId, profileId, sessionToken: token, hubHttpUrl: http, fetch: options.fetch, onError },
21
- connection: { appId, profileId, teamId, hubUrl: ws, clientInstanceId: options.clientInstanceId, fetchImpl: options.fetch,
28
+ onDispose: () => credentials?.close(),
29
+ uploadVoice: voiceUploader(profileId, options.uploadEndpoint, fetcher),
30
+ reconcileOutgoing: async (record, signal) => {
31
+ if (isOffline())
32
+ throw new Error("OUTBOX_RECONCILIATION_OFFLINE");
33
+ const result = await reader.messagePage({ threadId: record.threadId, teamId, messageId: record.message.hash }, signal);
34
+ const expected = record.message.body;
35
+ return result.messages.some(message => {
36
+ const body = message.body;
37
+ if (message.from !== profileId || message.to !== record.message.to || body?.kind !== expected.kind || body?.action !== expected.action)
38
+ throw new Error("OUTBOX_RECONCILIATION_MISMATCH");
39
+ return true;
40
+ });
41
+ },
42
+ loadGroup: (groupId, signal) => reader.group({ groupId, teamId }, signal),
43
+ activeThreadId: options.threadId,
44
+ messages: { scope: { appId, accountId, profileId, teamId }, transport: { authorizeScope: async (signal) => { await reader.contacts(teamId, signal); }, load: (thread, signal) => page(thread, signal), changes: (thread, after, signal) => page(thread, signal, { after }), older: (thread, before, signal) => page(thread, signal, { before }) }, onError },
45
+ reading: { appId, profileId, sessionToken: token, hubHttpUrl: http, fetch: fetcher, onError },
46
+ connection: { appId, profileId, teamId, hubUrl: ws, clientInstanceId: options.clientInstanceId, fetchImpl: fetcher,
47
+ getToken: credentials?.getToken,
22
48
  session: { token, identity: { accountId, app: appId, profileId } }, onNotification: options.onNotification, onReach: options.onReach },
23
- decode: message => decodeIncoming(message, { appId, profileId, teamId }), onError,
49
+ readingCandidate: message => {
50
+ if (isReadingControl(message.body, appId))
51
+ return undefined;
52
+ const body = message.body;
53
+ if (!body || !["message", "quote"].includes(String(body.kind)) && !(body.kind === "contact" && ["friendRequest", "requestFriend"].includes(String(body.action))))
54
+ return undefined;
55
+ return { id: message.id, threadId: message.threadId, from: message.from, mine: message.from === profileId, recalled: message.recalled, unread: message.from !== profileId,
56
+ readPolicy: message.readPolicy === "explicit" || message.legacyDisplay?.kind === "voice" || messageRequiresPlayback(body) ? "explicit" : "visible", attentionEligible: messageMentionsProfile(body, profileId) };
57
+ },
58
+ decode: message => decodeCachedChatMessage(message, { appId, profileId, teamId }), onError,
24
59
  });
25
60
  }
26
- function decodeIncoming(message, scope) {
27
- if (message.opcode !== 1 || !message.body || typeof message.body !== "object")
28
- return;
29
- const body = message.body;
30
- if (body.kind !== "message" || body.content?.appId !== scope.appId || chatTeamId(body) !== scope.teamId)
31
- return;
32
- if (body.threadKind !== "dm" && body.threadKind !== "group")
33
- return;
34
- if (body.threadKind === "dm" && message.from !== scope.profileId && message.to !== scope.profileId)
35
- return;
36
- const threadId = body.threadKind === "dm" ? dmThreadId(scope.appId, message.from, message.to, scope.teamId) : groupThreadId(scope.appId, message.to, scope.teamId);
37
- return { appId: scope.appId, teamId: scope.teamId, threadId, threadKind: body.threadKind, messageId: message.hash, id: message.hash,
38
- timestamp: message.t, serverCreatedAt: message.t, clientCreatedAt: message.t, from: message.from, to: message.to, body: message.body, recipients: message.recipients };
39
- }
61
+ /** @deprecated Advanced compatibility factory. Use /chat createChat, or createChatSession for raw sessions. */
62
+ export const createChat = createChatSession;
@@ -8,13 +8,77 @@ export type OutgoingMessage = {
8
8
  message: ChatRuntimeMessage;
9
9
  status: "sending" | "failed" | "sent";
10
10
  confirmed?: boolean;
11
+ pendingState?: 0 | 1;
12
+ accessVersion?: number;
13
+ voiceBlob?: Blob;
14
+ voiceData?: {
15
+ bytes: ArrayBuffer;
16
+ type: string;
17
+ };
18
+ voiceAttachment?: {
19
+ size: number;
20
+ type: string;
21
+ };
22
+ };
23
+ export type VoiceAttachmentLease = {
24
+ url: string;
25
+ release: () => void;
26
+ };
27
+ export type SendMessageInput = {
28
+ to: string;
29
+ body: ChatBody;
30
+ recipients?: string[];
31
+ voiceBlob?: Blob; /** SDK-owned reaction sequencing, committed with the exact wire. */
32
+ reactionRevisionFloor?: number;
33
+ };
34
+ export type SenderScopeAccess = {
35
+ version: number;
36
+ allowed: boolean;
37
+ };
38
+ export type DurableSenderOptions = {
39
+ indexedDB?: IDBFactory;
40
+ name?: string;
41
+ access?: () => Promise<SenderScopeAccess>;
42
+ uploadVoice?: (blob: Blob, signal: AbortSignal) => Promise<string>;
43
+ reconcile?: (record: Pick<OutgoingMessage, "threadId" | "message">, signal: AbortSignal) => Promise<boolean>;
11
44
  };
12
45
  /** Persists the exact wire message before sending; retries preserve its hash. */
13
46
  export declare class DurableChatSender {
14
47
  private client;
15
48
  private onError;
49
+ private options;
16
50
  private db;
17
51
  private outbox;
52
+ private controlRevisions;
53
+ private attachments;
54
+ private schemaReady;
55
+ private attachmentBytes;
56
+ private attachmentLeases;
57
+ private epochs;
58
+ private uploads;
59
+ private reconciliations;
60
+ private firstDelivery;
61
+ private ioQueue;
62
+ private ioTimer?;
63
+ private ioRunning?;
64
+ private ioCount;
65
+ private ioBytes;
66
+ private acceptanceQueue;
67
+ private acceptanceTimer?;
68
+ private acceptanceRunning?;
69
+ private acceptanceCount;
70
+ private acceptanceBytes;
71
+ private uploadTail;
72
+ private uploadWaiting;
73
+ private confirmationQueue;
74
+ private confirmationTimer?;
75
+ private confirmationRunning?;
76
+ private confirmationSize;
77
+ private version;
78
+ private withdrawn;
79
+ private revoking?;
80
+ private channel?;
81
+ private accepted;
18
82
  private scope;
19
83
  private identity;
20
84
  private values;
@@ -22,35 +86,54 @@ export declare class DurableChatSender {
22
86
  private listeners;
23
87
  private running;
24
88
  private closed;
89
+ private closing;
90
+ private empty;
25
91
  private offConnection;
26
92
  private ready;
27
- constructor(client: ChatClient, scope: MessageScope, onError: (error: unknown) => void, options?: {
28
- indexedDB?: IDBFactory;
29
- name?: string;
30
- });
93
+ constructor(client: ChatClient, scope: MessageScope, onError: (error: unknown) => void, options?: DurableSenderOptions);
94
+ /** Read only the selected durable rows, without restoring or delivering an outbox. */
95
+ static readSelected(scope: MessageScope, threadId: string, ids: readonly string[], version: number, options?: Pick<DurableSenderOptions, "indexedDB" | "name">): Promise<OutgoingMessage[]>;
96
+ private readAccess;
97
+ private restore;
98
+ private checkAccess;
99
+ private withEpoch;
100
+ private deleteRows;
101
+ /** Stop disclosure immediately; durable invalidation is coordinated separately. */
102
+ withdraw(): void;
103
+ revoke(targetVersion?: number): Promise<void>;
31
104
  initialize(): Promise<void>;
32
105
  subscribe(listener: () => void): () => void;
33
106
  snapshot(threadId?: string): readonly OutgoingMessage[];
34
107
  private emit;
35
108
  /** Resolves after the exact wire message is durable, independently of network delivery. */
36
- enqueue(threadId: string, input: {
37
- to: string;
38
- body: ChatBody;
39
- recipients?: string[];
40
- }): Promise<ChatRuntimeMessage>;
109
+ enqueue(threadId: string, input: SendMessageInput): Promise<ChatRuntimeMessage>;
41
110
  /** Compatibility API: resolves only after transport delivery. */
42
- send(threadId: string, input: {
43
- to: string;
44
- body: ChatBody;
45
- recipients?: string[];
46
- }): Promise<ChatRuntimeMessage>;
111
+ send(threadId: string, input: SendMessageInput): Promise<ChatRuntimeMessage>;
47
112
  private persistOutgoing;
113
+ private scheduleAcceptances;
114
+ private failAcceptances;
115
+ private drainAcceptances;
48
116
  retry(hash: string): Promise<ChatRuntimeMessage>;
49
117
  private transmit;
118
+ private reconcile;
119
+ /** Only explicit playback/transcription acquires a local payload, never snapshots. */
120
+ acquireVoice(hash: string, threadId: string): Promise<VoiceAttachmentLease | undefined>;
121
+ private reserveAttachment;
122
+ private readVoice;
123
+ private prepareVoice;
50
124
  private save;
125
+ private queueIO;
126
+ private scheduleIO;
127
+ private failIO;
128
+ private drainIO;
51
129
  /** Confirm only after the authoritative message has been durably merged.
52
130
  * A tombstone prevents another tab's late failure from recreating the outbox. */
53
131
  confirm(hashes: string[]): Promise<void>;
132
+ private scheduleConfirmations;
133
+ private drainConfirmations;
134
+ private failConfirmations;
135
+ private flushConfirmations;
136
+ private confirmNow;
54
137
  private resume;
55
138
  close(): Promise<void>;
56
139
  }