@pmate/chat-sdk 0.12.0-next.6 → 0.12.0-next.61

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 (64) hide show
  1. package/README.md +98 -3
  2. package/lib/runtime/batchedReadIndex.d.ts +16 -3
  3. package/lib/runtime/batchedReadIndex.js +50 -4
  4. package/lib/runtime/cachedMessage.d.ts +12 -0
  5. package/lib/runtime/cachedMessage.js +18 -0
  6. package/lib/runtime/chatClient.d.ts +1 -0
  7. package/lib/runtime/chatClient.js +1 -1
  8. package/lib/runtime/chatSession.d.ts +109 -12
  9. package/lib/runtime/chatSession.js +624 -40
  10. package/lib/runtime/connectivity.d.ts +4 -0
  11. package/lib/runtime/connectivity.js +8 -0
  12. package/lib/runtime/conversationRead.d.ts +13 -0
  13. package/lib/runtime/conversationRead.js +34 -0
  14. package/lib/runtime/conversationReading.d.ts +28 -0
  15. package/lib/runtime/conversationReading.js +47 -0
  16. package/lib/runtime/createChat.d.ts +5 -5
  17. package/lib/runtime/createChat.js +30 -17
  18. package/lib/runtime/durableSender.d.ts +97 -14
  19. package/lib/runtime/durableSender.js +786 -71
  20. package/lib/runtime/groupClient.d.ts +76 -0
  21. package/lib/runtime/groupClient.js +325 -0
  22. package/lib/runtime/index.d.ts +6 -0
  23. package/lib/runtime/index.js +5 -0
  24. package/lib/runtime/messageClient.d.ts +30 -8
  25. package/lib/runtime/messageClient.js +332 -66
  26. package/lib/runtime/messageControls.d.ts +19 -0
  27. package/lib/runtime/messageControls.js +44 -0
  28. package/lib/runtime/messageLaunch.d.ts +42 -0
  29. package/lib/runtime/messageLaunch.js +133 -0
  30. package/lib/runtime/messageQuotes.d.ts +3 -0
  31. package/lib/runtime/messageQuotes.js +17 -0
  32. package/lib/runtime/messageReaders.d.ts +12 -1
  33. package/lib/runtime/messageReaders.js +67 -3
  34. package/lib/runtime/messageStore.d.ts +38 -2
  35. package/lib/runtime/messageStore.js +215 -11
  36. package/lib/runtime/messageTimeline.d.ts +50 -0
  37. package/lib/runtime/messageTimeline.js +120 -0
  38. package/lib/runtime/outboxSchema.d.ts +5 -0
  39. package/lib/runtime/outboxSchema.js +59 -0
  40. package/lib/runtime/outgoingContent.d.ts +85 -0
  41. package/lib/runtime/outgoingContent.js +106 -0
  42. package/lib/runtime/readBatchJournal.d.ts +35 -0
  43. package/lib/runtime/readBatchJournal.js +210 -0
  44. package/lib/runtime/readCounts.d.ts +15 -1
  45. package/lib/runtime/readCounts.js +96 -8
  46. package/lib/runtime/readIndex.d.ts +68 -7
  47. package/lib/runtime/readIndex.js +460 -22
  48. package/lib/runtime/readSchema.d.ts +2 -0
  49. package/lib/runtime/readSchema.js +58 -0
  50. package/lib/runtime/readSync.d.ts +1 -0
  51. package/lib/runtime/readSync.js +25 -8
  52. package/lib/runtime/readingClient.d.ts +19 -0
  53. package/lib/runtime/readingClient.js +131 -19
  54. package/lib/runtime/readingContent.d.ts +5 -0
  55. package/lib/runtime/readingContent.js +23 -0
  56. package/lib/runtime/recordCache.d.ts +9 -0
  57. package/lib/runtime/recordCache.js +62 -5
  58. package/lib/runtime/responseCache.d.ts +57 -0
  59. package/lib/runtime/responseCache.js +230 -0
  60. package/lib/runtime/socialReadClient.d.ts +3 -1
  61. package/lib/runtime/socialReadClient.js +8 -4
  62. package/lib/runtime/voiceUpload.d.ts +2 -0
  63. package/lib/runtime/voiceUpload.js +27 -0
  64. package/package.json +6 -1
package/README.md CHANGED
@@ -39,7 +39,7 @@ A dropped socket (including a network change or a suspended laptop) previously r
39
39
 
40
40
  `client.watchConnection(handler)` immediately reports the current state and returns an unsubscribe function. States are `connecting`, `reconnecting`, `authenticating`, `connected`, `authentication_required`, and `closed`. Only `connected` means the Hub acknowledged authentication. A rejected authentication stops retries; supply a fresh login session using a new client. `close()` cancels retries, heartbeat timers and browser wake listeners.
41
41
 
42
- Messages sent without an acknowledgement are rejected on disconnect and are **not replayed**: delivery is uncertain, so automatic retries could duplicate them. Callers should preserve drafts, display a reconnecting state, and refresh message history after recovery. No durable offline outbox is provided. An initial `connect()` wait times out after 30 seconds, but recovery continues until explicitly closed; retain the client and observe its connection state.
42
+ The low-level `ChatClient.sendChat` rejects unacknowledged sends on disconnect and does not provide a durable outbox. Browser applications should use `createChat().sendMessage` below, which persists the exact wire in IndexedDB and owns delivery state and retries. An initial `connect()` wait times out after 30 seconds, but recovery continues until explicitly closed; retain the client and observe its connection state.
43
43
 
44
44
  ## Versioned Markdown and chart content
45
45
 
@@ -142,7 +142,9 @@ The browser entry point is `createChat` from `@pmate/chat-sdk/runtime`:
142
142
  ```ts
143
143
  const chat = createChat({ appId, accountId, profileId, teamId, token, onError })
144
144
  await chat.start()
145
- const page = await chat.getMessages(threadId, { limit: 50 })
145
+ const page = await chat.getMessages(threadId)
146
+ // SDK owns the page size and history boundary; hosts need not construct a cursor.
147
+ const earlier = await chat.getMessages(threadId, { earlier: true })
146
148
  // After an authoritative mutation (e.g. history assignment), await reconciliation.
147
149
  // Concurrent refreshes share transport; errors do not masquerade as fresh cache.
148
150
  const reconciled = await chat.getMessages(threadId, { refresh: true })
@@ -153,12 +155,31 @@ const offSummary = chat.subscribe(() => renderSummary(chat.getSnapshot()))
153
155
  await chat.observeMessages(threadId, visibleIds, "visible")
154
156
  // Report only the actual audio `playing` event.
155
157
  await chat.observeMessages(threadId, [voiceId], "played")
156
- await chat.sendMessage(threadId, { to, body, recipients })
158
+ const conversation = { id: threadId, kind: "dm" as const, targetId: peerId, teamId }
159
+ await chat.sendMessage(conversation, { text: "Hello" })
160
+ await chat.sendMessage(conversation, { image: { url: uploadedUrl, name: "photo.png" } })
161
+ await chat.sendMessage(conversation, { voice: { blob: recording, durationMs: 1200 } })
157
162
  const readers = await chat.getReaders(threadId, messageId)
158
163
  off(); offSummary()
164
+ // Ordinary navigation/shutdown preserves offline data.
159
165
  await chat.close()
160
166
  ```
161
167
 
168
+ ```ts
169
+ // When team access is withdrawn, use this instead of ordinary close(). It hides
170
+ // snapshots immediately, retires durable caches/outbox and closes this session.
171
+ // Other teams' reading operations remain intact. Create a new session to recover.
172
+ await chat.revokeAccess()
173
+ ```
174
+
175
+ Conversation descriptors identify the target and optional group `memberIds`; the SDK
176
+ checks the descriptor against its identity and team before acceptance. It builds
177
+ text, image, quote and voice envelopes and expands group `@all`/`@所有人` to concrete
178
+ members. Content callers do not provide `appId`, protocol `body` or `recipients`.
179
+ The existing `sendMessage(threadId, { to, body, recipients })` form remains available
180
+ for protocol integrations. Blob voice uploads stay in the durable sender; offline
181
+ uploads wait for retry instead of starting HTTP.
182
+
162
183
  The facade owns all clients below. Message reads transparently restore IndexedDB
163
184
  windows and fetch missing ranges through the authenticated Hub. Its normalized
164
185
  messages retain the protocol body; product rendering remains with the host.
@@ -201,6 +222,11 @@ legacy cleanup happens after durable migration. Normal reads use memory and writ
201
222
  are merged into bounded IndexedDB transactions. Cache errors must be surfaced by
202
223
  the host, and pending operations are not reported as saved before their promise
203
224
  resolves.
225
+ Control-event batches are bounded by both 128 records and 1 MiB per transaction;
226
+ the queue includes in-flight writes and is limited to 4096 records / 8 MiB.
227
+ `transactionBytes` and `queueBytes` can lower these limits. Oversized operations
228
+ fail before changing the memory snapshot. Repeating an in-flight write waits for
229
+ that physical write, including its failure, rather than acknowledging it early.
204
230
 
205
231
  `DurableChatSender` saves the exact message created by `ChatClient.prepareChat`
206
232
  before transmitting it. `retry(hash)` uses `sendPrepared` with the original hash
@@ -213,3 +239,72 @@ Custom primitive integrations must initialize them with the same authenticated
213
239
  scope, dispose them on identity changes, and wire their transport to an authorized
214
240
  server. Standard browser integrations should use the facade instead. The Merry migration and production validation
215
241
  are tracked in the unified message layer proposal.
242
+
243
+ When the browser reports offline, scheduled message restoration and reading
244
+ refreshes wait for the normal reconnect/wake path. Cached reads remain available;
245
+ unknown receipt counts stay unknown and an uncached reader detail page rejects
246
+ without attempting transport. A network failure arriving after the browser went
247
+ offline does not turn background synchronization into an operation error.
248
+ Authorization failures still revoke the corresponding cached data.
249
+
250
+ Reading entry points check the message scope even before `initialize()` finishes.
251
+ Concurrent observations share the permission lookup and batch their local writes.
252
+ Visibility/playback operations reject another known team's conversation; a
253
+ permission change in another team does not discard this team's observations.
254
+ Ordinary shutdown waits for accepted local reading writes, while access withdrawal
255
+ rejects late results. Retired asynchronous reading entries reject through promises;
256
+ refresh/subscription teardown remains safe.
257
+
258
+
259
+ ### Reading storage recovery
260
+
261
+ Large reading snapshots use a persisted journal. The full plan becomes durable
262
+ before canonical rows change; bounded transactions apply it, and the sync cursor
263
+ commits last. A reader recovers an active plan before returning a snapshot. Failed
264
+ staging leaves the previous snapshot authoritative; failure after activation keeps
265
+ the old cursor until recovery completes. Pending reads retain their operation IDs.
266
+
267
+ Writes use at most 128 records per transaction, including journal/lease records,
268
+ and conservatively budget one MiB per batch. Message order is stored per row so a
269
+ source no longer needs a growing array of every message ID. Ordinary observations
270
+ continue to update only positions and pending operations. Locks serialize each
271
+ owner across documents; a fenced IndexedDB lease supports contexts without Web Locks.
272
+
273
+ The private reading database format is now 60. Existing stores are renamed by
274
+ IndexedDB metadata operations, preserving keys and records. Older SDKs cannot
275
+ write through the former namespace and need a page reload to use the new format.
276
+ The message-body cache is separate and unchanged. The public reading state remains
277
+ schema version 1; applications do not select storage formats or recovery steps.
278
+
279
+
280
+ Pending voice snapshots contain `voiceAttachment` metadata, without loading audio bytes or creating Blob URLs at startup. For playback or transcription, acquire media through `await chat.getVoiceAttachment(threadId, messageId)`, use the returned `url`, and call `release()` when the consumer stops. The same method returns a remote voice reference for delivered messages. Attachment reads are charged before allocation, and close/revocation releases local URLs. A pending voice remains unread until the player actually starts; acquiring media does not mark it read.
281
+
282
+ The outbox format upgrade renames the old canonical store without copying binary data in the schema transaction, then atomically separates inline payloads in bounded, resumable transactions. An already-open older application must reload; unsupported senders reject the new format. Original wire/hash, failed status and retry semantics are retained.
283
+
284
+
285
+ Control messages use the same send API:
286
+
287
+ ```ts
288
+ await chat.sendMessage(conversation, {recall: {messageId}})
289
+ await chat.sendMessage(conversation, {reaction: {messageId, emojiId, active: true}})
290
+ await chat.sendMessage(conversation, {invitation: {message: "Hello"}})
291
+ await chat.sendMessage(group, {groupCreate: {title, members}})
292
+ await chat.sendMessage(group, {groupUpdate: {expectedRevision, addMembers}})
293
+ ```
294
+
295
+ These calls retain durable-acceptance semantics. Recall and reaction validate the target against the current scoped timeline or canonical local cache; the App does not supply protocol bodies or reaction versions. Reaction counters and exact wires commit in one IndexedDB transaction, including across documents. Validation ordering does not wait for each preceding commit, so a same-key burst still reaches the bounded acceptance batches. Pass `{waitFor:"delivered"}` as the third argument when an action requires server confirmation; the default still resolves after durable acceptance.
296
+
297
+
298
+ Group metadata uses the same identity, cache and subscriptions:
299
+
300
+ ```ts
301
+ const group = await chat.getGroup(conversation)
302
+ const off = chat.subscribe(conversation.id, () => {
303
+ renderGroup(chat.getSnapshot(conversation.id).group)
304
+ }, ["group"])
305
+ await chat.createGroup(conversation, {title, members})
306
+ await chat.updateGroup(conversation, {title: "New title"})
307
+ off()
308
+ ```
309
+
310
+ Reads restore trusted IndexedDB metadata before background revalidation. Creation and updates wait for confirmed delivery and a causal server version, including newer concurrent revisions; Apps do not poll for an exact message hash. Group events and browser recovery share a 200ms window. At most four metadata requests run concurrently, with 128 queued requests, a five-second request timeout, and at most five read-after-write attempts. Per-scope persistent metadata is capped at 64 groups / 1MiB, with 128KiB records, 32-record write batches, conditional cross-document writes and bounded pruning. Cached metadata does not bypass scope withdrawal or server membership checks. Binary media remains on demand.
@@ -1,6 +1,8 @@
1
- import { type ReadIndexPersistence, type ReadIndexState } from "./readIndex.js";
1
+ import { type ReadIndexPersistence, type ReadIndexState, type ReadIndexTransform, type ReadObservationScope } from "./readIndex.js";
2
2
  type Change = {
3
- transform: (state: ReadIndexState) => ReadIndexState;
3
+ weight: number;
4
+ observation?: ReadObservationScope;
5
+ transform: ReadIndexTransform;
4
6
  resolve: (state: ReadIndexState) => void;
5
7
  reject: (error: unknown) => void;
6
8
  };
@@ -13,9 +15,20 @@ export declare class BatchedReadIndex implements ReadIndexPersistence {
13
15
  private queueLimit;
14
16
  private queues;
15
17
  private closed;
18
+ private sourceCounts;
16
19
  constructor(storage: ReadIndexPersistence, windowMs?: number, batchSize?: number, queueLimit?: number);
17
20
  load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
18
- update(appId: string, profileId: string, transform: Change["transform"]): Promise<ReadIndexState>;
21
+ loadVersioned(appId: string, profileId: string): Promise<{
22
+ state: ReadIndexState | undefined;
23
+ versions: import("./readIndex.js").ReadIndexVersions;
24
+ } | {
25
+ state: ReadIndexState | undefined;
26
+ versions: {};
27
+ }>;
28
+ loadVersions(appId: string, profileId: string): Promise<{}>;
29
+ invalidateTeam(appId: string, profileId: string, teamId: string, version: number, knownThreadIds?: readonly string[]): Promise<ReadIndexState>;
30
+ private weight;
31
+ update(appId: string, profileId: string, transform: Change["transform"], observation?: ReadObservationScope): Promise<ReadIndexState>;
19
32
  private drain;
20
33
  flush(appId: string, profileId: string): Promise<void>;
21
34
  close(): Promise<void>;
@@ -8,6 +8,7 @@ export class BatchedReadIndex {
8
8
  queueLimit;
9
9
  queues = new Map();
10
10
  closed = false;
11
+ sourceCounts = new WeakMap();
11
12
  constructor(storage, windowMs = 16, batchSize = 128, queueLimit = 4096) {
12
13
  this.storage = storage;
13
14
  this.windowMs = windowMs;
@@ -22,7 +23,42 @@ export class BatchedReadIndex {
22
23
  throw new Error("READ_STORAGE_CLOSED");
23
24
  return this.storage.load(appId, profileId);
24
25
  }
25
- update(appId, profileId, transform) {
26
+ async loadVersioned(appId, profileId) {
27
+ await this.flush(appId, profileId);
28
+ if (this.closed)
29
+ throw new Error("READ_STORAGE_CLOSED");
30
+ return this.storage.loadVersioned ? this.storage.loadVersioned(appId, profileId) : { state: await this.storage.load(appId, profileId), versions: {} };
31
+ }
32
+ async loadVersions(appId, profileId) {
33
+ if (this.closed)
34
+ throw new Error("READ_STORAGE_CLOSED");
35
+ return this.storage.loadVersions ? this.storage.loadVersions(appId, profileId) : {};
36
+ }
37
+ async invalidateTeam(appId, profileId, teamId, version, knownThreadIds) {
38
+ if (this.closed)
39
+ throw new Error("READ_STORAGE_CLOSED");
40
+ if (!this.storage.invalidateTeam)
41
+ throw new Error("READ_SCOPE_INVALIDATION_UNAVAILABLE");
42
+ // Do not flush a blocked old HTTP transform first. The durable epoch is the
43
+ // barrier; any queued transform sees it inside its eventual transaction.
44
+ return this.storage.invalidateTeam(appId, profileId, teamId, version, knownThreadIds);
45
+ }
46
+ weight(observation) {
47
+ if (!observation)
48
+ return 1;
49
+ let counts = this.sourceCounts.get(observation.base);
50
+ if (!counts) {
51
+ counts = new Map();
52
+ for (const thread of observation.base.threads) {
53
+ counts.set(thread.id, (counts.get(thread.id) ?? 0) + 1);
54
+ if (thread.conversationId && thread.conversationId !== thread.id)
55
+ counts.set(thread.conversationId, (counts.get(thread.conversationId) ?? 0) + 1);
56
+ }
57
+ this.sourceCounts.set(observation.base, counts);
58
+ }
59
+ return observation.targets.reduce((sum, target) => sum + (counts.get(target.threadId) ?? 1), 0);
60
+ }
61
+ update(appId, profileId, transform, observation) {
26
62
  if (this.closed)
27
63
  return Promise.reject(new Error("READ_STORAGE_CLOSED"));
28
64
  const key = JSON.stringify([appId, profileId]);
@@ -33,7 +69,7 @@ export class BatchedReadIndex {
33
69
  }
34
70
  if (queue.pending.length >= this.queueLimit)
35
71
  return Promise.reject(new Error("READ_STORAGE_BACKPRESSURE"));
36
- const promise = new Promise((resolve, reject) => queue.pending.push({ transform, resolve, reject }));
72
+ const promise = new Promise((resolve, reject) => queue.pending.push({ transform, observation, weight: this.weight(observation), resolve, reject }));
37
73
  if (!queue.running && queue.timer === undefined)
38
74
  queue.timer = setTimeout(() => { queue.timer = undefined; void this.drain(key, queue); }, this.windowMs);
39
75
  return promise;
@@ -43,12 +79,22 @@ export class BatchedReadIndex {
43
79
  return queue.running;
44
80
  clearTimeout(queue.timer);
45
81
  queue.timer = undefined;
46
- const batch = queue.pending.splice(0, this.batchSize);
82
+ const batch = [];
83
+ let weight = 0;
84
+ while (queue.pending.length && batch.length < this.batchSize) {
85
+ const nextWeight = queue.pending[0].weight;
86
+ if (batch.length && weight + nextWeight > 128)
87
+ break;
88
+ weight += nextWeight;
89
+ batch.push(queue.pending.shift());
90
+ }
47
91
  if (!batch.length)
48
92
  return;
49
93
  const run = (async () => {
50
94
  try {
51
- const next = await this.storage.update(queue.appId, queue.profileId, state => batch.reduce((current, change) => change.transform(current), state));
95
+ const first = batch[0].observation;
96
+ const observation = first && batch.every(change => change.observation?.base === first.base) ? { base: first.base, targets: batch.flatMap(change => change.observation.targets) } : undefined;
97
+ const next = await this.storage.update(queue.appId, queue.profileId, (state, versions) => batch.reduce((current, change) => change.transform(current, versions), state), observation);
52
98
  batch.forEach(change => change.resolve(next));
53
99
  }
54
100
  catch (error) {
@@ -0,0 +1,12 @@
1
+ import type { ChatReaderSocialMessage } from "./readerClient.js";
2
+ import type { ChatRuntimeMessage } from "./protocol.js";
3
+ export type CachedChatMessage = ChatReaderSocialMessage & {
4
+ id: string;
5
+ timestamp: number;
6
+ readPolicy?: "visible" | "explicit";
7
+ };
8
+ export declare function decodeCachedChatMessage(message: ChatRuntimeMessage, scope: {
9
+ appId: string;
10
+ profileId: string;
11
+ teamId: string;
12
+ }): CachedChatMessage | undefined;
@@ -0,0 +1,18 @@
1
+ import { chatTeamId } from "./teamScope.js";
2
+ import { dmThreadId, groupThreadId } from "./thread.js";
3
+ export function decodeCachedChatMessage(message, scope) {
4
+ if (message.opcode !== 1 || !message.body || typeof message.body !== "object")
5
+ return;
6
+ const body = message.body;
7
+ const invitation = body.kind === "contact" && ["friendRequest", "requestFriend"].includes(String(body.action));
8
+ if ((!invitation && body.kind !== "message") || (invitation ? body.payload?.appId : body.content?.appId) !== scope.appId || chatTeamId(body) !== scope.teamId)
9
+ return;
10
+ const threadKind = invitation ? "dm" : body.threadKind;
11
+ if (threadKind !== "dm" && threadKind !== "group")
12
+ return;
13
+ if (threadKind === "dm" && message.from !== scope.profileId && message.to !== scope.profileId)
14
+ return;
15
+ const threadId = threadKind === "dm" ? dmThreadId(scope.appId, message.from, message.to, scope.teamId) : groupThreadId(scope.appId, message.to, scope.teamId);
16
+ return { appId: scope.appId, teamId: scope.teamId, threadId, threadKind, messageId: message.hash, id: message.hash,
17
+ timestamp: message.t, serverCreatedAt: message.t, clientCreatedAt: message.t, from: message.from, to: message.to, body: message.body, recipients: message.recipients };
18
+ }
@@ -62,6 +62,7 @@ export declare class ChatClient {
62
62
  profileId: string | undefined;
63
63
  accountId: string | undefined;
64
64
  teamId: string | undefined;
65
+ clientInstanceId: string;
65
66
  };
66
67
  watchConnection(handler: (state: ChatConnectionState) => void): () => void;
67
68
  private setConnection;
@@ -123,7 +123,7 @@ export class ChatClient {
123
123
  /** Non-secret identity used to validate a shared browser transport. */
124
124
  identityScope() {
125
125
  const identity = this.session?.identity ?? this.session?.session?.identity;
126
- return { appId: this.options.appId, profileId: this.profileIdValue ?? this.options.profileId, accountId: identity?.accountId, teamId: this.options.teamId };
126
+ return { appId: this.options.appId, profileId: this.profileIdValue ?? this.options.profileId, accountId: identity?.accountId, teamId: this.options.teamId, clientInstanceId: this.instanceId };
127
127
  }
128
128
  watchConnection(handler) {
129
129
  this.connectionHandlers.add(handler);
@@ -1,36 +1,55 @@
1
+ import { type ChatGroup } from "./groupClient.js";
2
+ import type { SocialGroup } from "./socialReadClient.js";
3
+ import { type ChatConversation, type OutgoingContent } from "./outgoingContent.js";
4
+ import { type TimelineItem } from "./messageTimeline.js";
5
+ import { type ReadingCandidate, type MessageReadingState } from "./conversationReading.js";
6
+ import type { ConversationReadMessage } from "./conversationRead.js";
7
+ import { type ReadCountsResult } from "./readCounts.js";
8
+ import type { OnlineReachEvent } from "./reach.js";
1
9
  import { type MessageReadersQuery } from "./messageReaders.js";
2
10
  import { type MessageClientOptions, type MessageSnapshot } from "./messageClient.js";
3
- import { DurableChatSender } from "./durableSender.js";
4
- import { type ReadingClientOptions } from "./readingClient.js";
11
+ import { DurableChatSender, type SendMessageInput, type DurableSenderOptions, type VoiceAttachmentLease } from "./durableSender.js";
12
+ import { ReadingClient, type ReadingClientOptions } from "./readingClient.js";
5
13
  import { ChatClient, type ChatClientOptions, type ChatConnectionState } from "./chatClient.js";
6
14
  import { unreadInConversation } from "./readIndex.js";
7
15
  import type { StoredMessage } from "./messageStore.js";
8
- import type { ChatBody, ChatRuntimeMessage } from "./protocol.js";
16
+ import type { ChatRuntimeMessage } from "./protocol.js";
9
17
  export type ChatSessionOptions<T extends StoredMessage> = {
10
18
  messages: MessageClientOptions<T>;
11
19
  connection: ChatClientOptions;
12
20
  sharedConnection?: ChatClient;
21
+ activeThreadId?: string;
22
+ uploadVoice?: DurableSenderOptions["uploadVoice"];
23
+ reconcileOutgoing?: DurableSenderOptions["reconcile"];
24
+ loadGroup?: (groupId: string, signal: AbortSignal) => Promise<SocialGroup>;
13
25
  reading: ReadingClientOptions;
14
26
  decode: (message: ChatRuntimeMessage) => T | undefined;
27
+ readingCandidate?: (message: T) => ReadingCandidate | undefined;
15
28
  onError: (error: unknown) => void;
16
29
  };
17
30
  export type ChatThreadSnapshot<T> = {
18
31
  messages: readonly T[];
32
+ timeline: readonly TimelineItem<T>[];
19
33
  outgoing: ReturnType<DurableChatSender["snapshot"]>;
20
34
  hasEarlier: boolean;
21
35
  resetVersion: number;
22
36
  complete: boolean;
23
- unread: ReturnType<typeof unreadInConversation>;
37
+ group?: ChatGroup;
38
+ readCounts?: ReadCountsResult;
39
+ unread: ReturnType<typeof unreadInConversation> | undefined;
40
+ messageReading: Readonly<Record<string, MessageReadingState>>;
24
41
  };
25
42
  export type ChatSummarySnapshot<T> = {
26
43
  threads: readonly {
27
44
  threadId: string;
28
45
  lastMessage?: T;
46
+ lastItem?: TimelineItem<T>;
47
+ group?: ChatGroup;
29
48
  unread: ChatThreadSnapshot<T>["unread"];
30
49
  outgoingCount: number;
31
50
  }[];
32
- unreadMessageCount: number;
33
- badgeCount: number;
51
+ unreadMessageCount: number | undefined;
52
+ badgeCount: number | undefined;
34
53
  connection: ChatConnectionState;
35
54
  };
36
55
  /** Owns all browser message clients. Hosts subscribe to stable thread snapshots. */
@@ -40,20 +59,63 @@ export declare class ChatSession<T extends StoredMessage> {
40
59
  private connection;
41
60
  private reading;
42
61
  private readers;
62
+ private groups?;
63
+ private counts?;
64
+ private countsReceiver?;
65
+ private countsThread?;
66
+ private countSnapshots;
67
+ private countClosings;
68
+ private countTasks;
69
+ private controlTails;
70
+ private acceptedControls;
71
+ private controlCount;
72
+ private controlBytes;
43
73
  private sender;
44
74
  private listeners;
45
75
  private snapshots;
76
+ private timelineDecoded;
77
+ private decodeTimeline;
78
+ private historyEdges;
79
+ private earlierReads;
46
80
  private off;
47
81
  private globalListeners;
48
82
  private globalSnapshot?;
83
+ private closedThread;
84
+ private closedSummary;
49
85
  private connectionState;
50
86
  private catchUp;
87
+ private readingAllowed;
88
+ private readingListeners;
89
+ private acceptedReading;
90
+ private readingAccess?;
51
91
  private readingStarted;
92
+ private readingStarting?;
93
+ private withdrawn;
94
+ private revoking?;
52
95
  private closed;
53
96
  private starting?;
97
+ private initializing?;
54
98
  private closing?;
55
99
  constructor(options: ChatSessionOptions<T>);
100
+ private scheduleCatchUp;
56
101
  start(): Promise<void>;
102
+ /** Initializes local state and background reading sync without opening a
103
+ * second transport when the host already owns calls/presence. */
104
+ initialize(): Promise<void>;
105
+ private startReading;
106
+ /** One-time migration only: never establishes trusted history coverage. */
107
+ importLegacyMessages(threadId: string, messages: T[]): Promise<void>;
108
+ getReadingSnapshot(): import("./readIndex.js").ReadIndexState | undefined;
109
+ subscribeReading(listener: () => void): () => void;
110
+ private setReadingAllowed;
111
+ private readingAuthority;
112
+ private authorizeReading;
113
+ private readingOperation;
114
+ refreshReading(): void;
115
+ synchronizeReading(cursor?: string, profileId?: string): Promise<import("./readIndex.js").ReadIndexState>;
116
+ /** Explicit text-history acknowledgement; unheard audio stays unread. */
117
+ markRead(threadId: string, messages?: readonly ConversationReadMessage[]): Promise<import("./readIndex.js").ReadIndexState>;
118
+ updateReading(transform: Parameters<ReadingClient["update"]>[0]): Promise<import("./readIndex.js").ReadIndexState>;
57
119
  private assertOpen;
58
120
  getMessages(threadId: string, options?: {
59
121
  limit?: number;
@@ -61,25 +123,60 @@ export declare class ChatSession<T extends StoredMessage> {
61
123
  timestamp: number;
62
124
  id: string;
63
125
  };
126
+ earlier?: boolean;
64
127
  all?: boolean;
65
128
  refresh?: boolean;
66
129
  }): Promise<MessageSnapshot<T>>;
130
+ private confirmMessages;
131
+ private rememberHistoryEdge;
132
+ private readEarlier;
67
133
  private readAll;
68
- sendMessage(threadId: string, input: {
69
- to: string;
70
- body: ChatBody;
71
- recipients?: string[];
134
+ /** Reads trusted local group metadata immediately; SDK refreshes it in the background. */
135
+ getGroup(thread: ChatConversation, options?: {
136
+ refresh?: boolean;
137
+ }): Promise<ChatGroup>;
138
+ createGroup(thread: ChatConversation, content: {
139
+ title: string;
140
+ members: readonly string[];
141
+ }): Promise<ChatGroup>;
142
+ updateGroup(thread: ChatConversation, changes: {
143
+ expectedRevision?: string;
144
+ addMembers?: readonly string[];
145
+ removeMembers?: readonly string[];
146
+ title?: string;
147
+ avatar?: string;
148
+ }): Promise<ChatGroup>;
149
+ sendMessage(thread: ChatConversation, input: OutgoingContent, options?: {
150
+ waitFor?: "accepted" | "delivered";
151
+ }): Promise<ChatRuntimeMessage>;
152
+ sendMessage(threadId: string, input: SendMessageInput, options?: {
153
+ waitFor?: "accepted" | "delivered";
72
154
  }): Promise<ChatRuntimeMessage>;
155
+ private sendControl;
73
156
  getReaders(threadId: string, messageId: string, page?: Omit<MessageReadersQuery, "threadId" | "messageId">, signal?: AbortSignal): Promise<import("./messageReaders.js").MessageReadersPage>;
157
+ /** Resolve voice media uniformly; pending local bytes are loaded only on demand. */
158
+ getVoiceAttachment(threadId: string, messageId: string): Promise<VoiceAttachmentLease>;
74
159
  retryMessage(messageId: string): Promise<ChatRuntimeMessage>;
75
- observeMessages(threadId: string, ids: string[], event: "visible" | "played"): Promise<import("./readIndex.js").ReadIndexState>;
160
+ observeMessages(threadId: string, ids: string[], event: "visible" | "played"): ReturnType<ReadingClient["recordVisible"]>;
161
+ observeMessages(threadId: string | undefined, ids: string[], event: "receipts", clientInstanceId?: string): Promise<void>;
162
+ /** Bridge transports can delegate social events without maintaining clients. */
163
+ handleReach(event: OnlineReachEvent): Promise<"handled" | undefined>;
164
+ private wakeCounts;
165
+ private publishCountSnapshot;
166
+ private publishCounts;
167
+ private watchCounts;
76
168
  getSnapshot(): ChatSummarySnapshot<T>;
77
169
  getSnapshot(threadId: string): ChatThreadSnapshot<T>;
170
+ private buildThreadSnapshot;
78
171
  subscribe(listener: () => void): () => void;
79
- subscribe(threadId: string, listener: () => void): () => void;
172
+ subscribe(threadId: string, listener: () => void, fields?: readonly (keyof ChatThreadSnapshot<T>)[]): () => void;
80
173
  private publishAll;
81
174
  private buildGlobal;
82
175
  private publishGlobal;
83
176
  private publish;
177
+ /** Permanently retire this identity/team session. UI withdrawal is immediate;
178
+ * completion means all durable stores have crossed the same authority epoch. */
179
+ revokeAccess(): Promise<void>;
84
180
  close(): Promise<void>;
181
+ private dispose;
85
182
  }