@pmate/chat-sdk 0.12.0-next.2 → 0.12.0-next.21

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 CHANGED
@@ -137,6 +137,60 @@ sends and reading synchronization independently. The runtime now provides local
137
137
  IndexedDB storage and fixed-window scheduling primitives so those operations can
138
138
  share bounded I/O, durable confirmation and scoped subscriptions.
139
139
 
140
+ The browser entry point is `createChat` from `@pmate/chat-sdk/runtime`:
141
+
142
+ ```ts
143
+ const chat = createChat({ appId, accountId, profileId, teamId, token, onError })
144
+ await chat.start()
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 })
148
+ // After an authoritative mutation (e.g. history assignment), await reconciliation.
149
+ // Concurrent refreshes share transport; errors do not masquerade as fresh cache.
150
+ const reconciled = await chat.getMessages(threadId, { refresh: true })
151
+ // Explicit full-history queries reuse cached pages and internally rebase expired cursors.
152
+ const history = await chat.getMessages(threadId, { all: true })
153
+ const off = chat.subscribe(threadId, () => render(chat.getSnapshot(threadId)))
154
+ const offSummary = chat.subscribe(() => renderSummary(chat.getSnapshot()))
155
+ await chat.observeMessages(threadId, visibleIds, "visible")
156
+ // Report only the actual audio `playing` event.
157
+ await chat.observeMessages(threadId, [voiceId], "played")
158
+ await chat.sendMessage(threadId, { to, body, recipients })
159
+ const readers = await chat.getReaders(threadId, messageId)
160
+ off(); offSummary()
161
+ // Ordinary navigation/shutdown preserves offline data.
162
+ await chat.close()
163
+ ```
164
+
165
+ ```ts
166
+ // When team access is withdrawn, use this instead of ordinary close(). It hides
167
+ // snapshots immediately, retires durable caches/outbox and closes this session.
168
+ // Other teams' reading operations remain intact. Create a new session to recover.
169
+ await chat.revokeAccess()
170
+ ```
171
+
172
+ The facade owns all clients below. Message reads transparently restore IndexedDB
173
+ windows and fetch missing ranges through the authenticated Hub. Its normalized
174
+ messages retain the protocol body; product rendering remains with the host.
175
+ `sendMessage` resolves after local persistence, while `outgoing` in the thread
176
+ snapshot reports transport progress and failure. Retry uses the original message
177
+ hash. The subscribed recent window remains 100 records regardless of individual
178
+ query limits; history requests return their own pages and do not replace it.
179
+ Global snapshots contain thread summaries and totals, not complete message arrays.
180
+ Message pages report `complete`; unavailable older pages return cached records with
181
+ `complete: false`. Authorization failures throw and never fall back to denied
182
+ records. Thread snapshots expose `resetVersion`; hosts discard retained rows when
183
+ it changes. Reset epochs are durable, so late cross-tab writes cannot restore a
184
+ pre-reset payload.
185
+ Voice visibility never acknowledges playback, including observations that arrive
186
+ before metadata. Create a new instance and close the previous owner when the
187
+ account or active Team changes.
188
+
189
+ This facade is still under integration. Reader details currently require network
190
+ access, global summaries do not yet replace the host's contact/thread metadata,
191
+ and Merry's message-list/send migration and production validation are unfinished.
192
+ The lower-level primitives remain available for custom transports:
193
+
140
194
  `MessageClient` reads a trusted recent window locally, coalesces concurrent misses,
141
195
  subscribes per conversation, and accepts incremental event batches. Its transport
142
196
  supplies authoritative `load`, `changes` and `older` pages. `nextBefore` tracks
@@ -157,6 +211,11 @@ legacy cleanup happens after durable migration. Normal reads use memory and writ
157
211
  are merged into bounded IndexedDB transactions. Cache errors must be surfaced by
158
212
  the host, and pending operations are not reported as saved before their promise
159
213
  resolves.
214
+ Control-event batches are bounded by both 128 records and 1 MiB per transaction;
215
+ the queue includes in-flight writes and is limited to 4096 records / 8 MiB.
216
+ `transactionBytes` and `queueBytes` can lower these limits. Oversized operations
217
+ fail before changing the memory snapshot. Repeating an in-flight write waits for
218
+ that physical write, including its failure, rather than acknowledging it early.
160
219
 
161
220
  `DurableChatSender` saves the exact message created by `ChatClient.prepareChat`
162
221
  before transmitting it. `retry(hash)` uses `sendPrepared` with the original hash
@@ -165,7 +224,7 @@ when the connection returns. Call `confirm(hashes)` only after authoritative
165
224
  messages have been durably merged. Confirmation tombstones prevent another tab's
166
225
  late transport failure from recreating an outgoing message.
167
226
 
168
- These APIs are runtime primitives; applications must initialize them with the
169
- same authenticated scope, dispose them on identity changes, and wire their
170
- transport to an authorized server. The Merry migration and production validation
227
+ Custom primitive integrations must initialize them with the same authenticated
228
+ scope, dispose them on identity changes, and wire their transport to an authorized
229
+ server. Standard browser integrations should use the facade instead. The Merry migration and production validation
171
230
  are tracked in the unified message layer proposal.
@@ -1,6 +1,6 @@
1
- import { type ReadIndexPersistence, type ReadIndexState } from "./readIndex.js";
1
+ import { type ReadIndexPersistence, type ReadIndexState, type ReadIndexTransform } from "./readIndex.js";
2
2
  type Change = {
3
- transform: (state: ReadIndexState) => ReadIndexState;
3
+ transform: ReadIndexTransform;
4
4
  resolve: (state: ReadIndexState) => void;
5
5
  reject: (error: unknown) => void;
6
6
  };
@@ -15,6 +15,15 @@ export declare class BatchedReadIndex implements ReadIndexPersistence {
15
15
  private closed;
16
16
  constructor(storage: ReadIndexPersistence, windowMs?: number, batchSize?: number, queueLimit?: number);
17
17
  load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
18
+ loadVersioned(appId: string, profileId: string): Promise<{
19
+ state: ReadIndexState | undefined;
20
+ versions: import("./readIndex.js").ReadIndexVersions;
21
+ } | {
22
+ state: ReadIndexState | undefined;
23
+ versions: {};
24
+ }>;
25
+ loadVersions(appId: string, profileId: string): Promise<{}>;
26
+ invalidateTeam(appId: string, profileId: string, teamId: string, version: number, knownThreadIds?: readonly string[]): Promise<ReadIndexState>;
18
27
  update(appId: string, profileId: string, transform: Change["transform"]): Promise<ReadIndexState>;
19
28
  private drain;
20
29
  flush(appId: string, profileId: string): Promise<void>;
@@ -22,6 +22,26 @@ export class BatchedReadIndex {
22
22
  throw new Error("READ_STORAGE_CLOSED");
23
23
  return this.storage.load(appId, profileId);
24
24
  }
25
+ async loadVersioned(appId, profileId) {
26
+ await this.flush(appId, profileId);
27
+ if (this.closed)
28
+ throw new Error("READ_STORAGE_CLOSED");
29
+ return this.storage.loadVersioned ? this.storage.loadVersioned(appId, profileId) : { state: await this.storage.load(appId, profileId), versions: {} };
30
+ }
31
+ async loadVersions(appId, profileId) {
32
+ if (this.closed)
33
+ throw new Error("READ_STORAGE_CLOSED");
34
+ return this.storage.loadVersions ? this.storage.loadVersions(appId, profileId) : {};
35
+ }
36
+ async invalidateTeam(appId, profileId, teamId, version, knownThreadIds) {
37
+ if (this.closed)
38
+ throw new Error("READ_STORAGE_CLOSED");
39
+ if (!this.storage.invalidateTeam)
40
+ throw new Error("READ_SCOPE_INVALIDATION_UNAVAILABLE");
41
+ // Do not flush a blocked old HTTP transform first. The durable epoch is the
42
+ // barrier; any queued transform sees it inside its eventual transaction.
43
+ return this.storage.invalidateTeam(appId, profileId, teamId, version, knownThreadIds);
44
+ }
25
45
  update(appId, profileId, transform) {
26
46
  if (this.closed)
27
47
  return Promise.reject(new Error("READ_STORAGE_CLOSED"));
@@ -48,7 +68,7 @@ export class BatchedReadIndex {
48
68
  return;
49
69
  const run = (async () => {
50
70
  try {
51
- const next = await this.storage.update(queue.appId, queue.profileId, state => batch.reduce((current, change) => change.transform(current), state));
71
+ const next = await this.storage.update(queue.appId, queue.profileId, (state, versions) => batch.reduce((current, change) => change.transform(current, versions), state));
52
72
  batch.forEach(change => change.resolve(next));
53
73
  }
54
74
  catch (error) {
@@ -56,6 +56,14 @@ export declare class ChatClient {
56
56
  private initialize;
57
57
  close(): void;
58
58
  connectionState(): ChatConnectionState;
59
+ /** Non-secret identity used to validate a shared browser transport. */
60
+ identityScope(): {
61
+ appId: string;
62
+ profileId: string | undefined;
63
+ accountId: string | undefined;
64
+ teamId: string | undefined;
65
+ clientInstanceId: string;
66
+ };
59
67
  watchConnection(handler: (state: ChatConnectionState) => void): () => void;
60
68
  private setConnection;
61
69
  private rejectPending;
@@ -120,6 +120,11 @@ export class ChatClient {
120
120
  this.rejectPending(new Error("ChatClient closed"));
121
121
  }
122
122
  connectionState() { return this.connection; }
123
+ /** Non-secret identity used to validate a shared browser transport. */
124
+ identityScope() {
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, clientInstanceId: this.instanceId };
127
+ }
123
128
  watchConnection(handler) {
124
129
  this.connectionHandlers.add(handler);
125
130
  handler(this.connection);
@@ -423,10 +428,11 @@ export class ChatClient {
423
428
  return profileId;
424
429
  }
425
430
  profileId() {
426
- if (!this.profileIdValue) {
431
+ const profileId = this.profileIdValue ?? this.options.profileId;
432
+ if (!profileId) {
427
433
  throw new Error("ChatClient is missing profile id; call connect() first");
428
434
  }
429
- return this.profileIdValue;
435
+ return profileId;
430
436
  }
431
437
  async fetchProfiles(appId, accountId, token) {
432
438
  const fetcher = this.options.fetchImpl ?? fetch;
@@ -1,14 +1,19 @@
1
+ import type { ConversationReadMessage } from "./conversationRead.js";
2
+ import { type ReadCountsResult } from "./readCounts.js";
3
+ import type { OnlineReachEvent } from "./reach.js";
1
4
  import { type MessageReadersQuery } from "./messageReaders.js";
2
- import { type MessageClientOptions } from "./messageClient.js";
5
+ import { type MessageClientOptions, type MessageSnapshot } from "./messageClient.js";
3
6
  import { DurableChatSender } from "./durableSender.js";
4
- import { type ReadingClientOptions } from "./readingClient.js";
5
- import { type ChatClientOptions } from "./chatClient.js";
7
+ import { ReadingClient, type ReadingClientOptions } from "./readingClient.js";
8
+ import { ChatClient, type ChatClientOptions, type ChatConnectionState } from "./chatClient.js";
6
9
  import { unreadInConversation } from "./readIndex.js";
7
10
  import type { StoredMessage } from "./messageStore.js";
8
11
  import type { ChatBody, ChatRuntimeMessage } from "./protocol.js";
9
12
  export type ChatSessionOptions<T extends StoredMessage> = {
10
13
  messages: MessageClientOptions<T>;
11
14
  connection: ChatClientOptions;
15
+ sharedConnection?: ChatClient;
16
+ activeThreadId?: string;
12
17
  reading: ReadingClientOptions;
13
18
  decode: (message: ChatRuntimeMessage) => T | undefined;
14
19
  onError: (error: unknown) => void;
@@ -17,7 +22,21 @@ export type ChatThreadSnapshot<T> = {
17
22
  messages: readonly T[];
18
23
  outgoing: ReturnType<DurableChatSender["snapshot"]>;
19
24
  hasEarlier: boolean;
20
- unread: ReturnType<typeof unreadInConversation>;
25
+ resetVersion: number;
26
+ complete: boolean;
27
+ readCounts?: ReadCountsResult;
28
+ unread: ReturnType<typeof unreadInConversation> | undefined;
29
+ };
30
+ export type ChatSummarySnapshot<T> = {
31
+ threads: readonly {
32
+ threadId: string;
33
+ lastMessage?: T;
34
+ unread: ChatThreadSnapshot<T>["unread"];
35
+ outgoingCount: number;
36
+ }[];
37
+ unreadMessageCount: number | undefined;
38
+ badgeCount: number | undefined;
39
+ connection: ChatConnectionState;
21
40
  };
22
41
  /** Owns all browser message clients. Hosts subscribe to stable thread snapshots. */
23
42
  export declare class ChatSession<T extends StoredMessage> {
@@ -26,15 +45,47 @@ export declare class ChatSession<T extends StoredMessage> {
26
45
  private connection;
27
46
  private reading;
28
47
  private readers;
48
+ private counts?;
49
+ private countsReceiver?;
50
+ private countsThread?;
51
+ private countSnapshots;
52
+ private countClosings;
53
+ private countTasks;
29
54
  private sender;
30
55
  private listeners;
31
56
  private snapshots;
57
+ private historyEdges;
58
+ private earlierReads;
32
59
  private off;
60
+ private globalListeners;
61
+ private globalSnapshot?;
62
+ private closedThread;
63
+ private closedSummary;
64
+ private connectionState;
65
+ private catchUp;
66
+ private readingStarted;
67
+ private readingStarting?;
68
+ private withdrawn;
69
+ private revoking?;
33
70
  private closed;
34
71
  private starting?;
72
+ private initializing?;
35
73
  private closing?;
36
74
  constructor(options: ChatSessionOptions<T>);
37
75
  start(): Promise<void>;
76
+ /** Initializes local state and background reading sync without opening a
77
+ * second transport when the host already owns calls/presence. */
78
+ initialize(): Promise<void>;
79
+ private startReading;
80
+ /** One-time migration only: never establishes trusted history coverage. */
81
+ importLegacyMessages(threadId: string, messages: T[]): Promise<void>;
82
+ getReadingSnapshot(): import("./readIndex.js").ReadIndexState | undefined;
83
+ subscribeReading(listener: () => void): () => void;
84
+ refreshReading(): void;
85
+ synchronizeReading(cursor?: string, profileId?: string): Promise<import("./readIndex.js").ReadIndexState>;
86
+ /** Explicit text-history acknowledgement; unheard audio stays unread. */
87
+ markRead(threadId: string, messages?: readonly ConversationReadMessage[]): Promise<import("./readIndex.js").ReadIndexState>;
88
+ updateReading(transform: Parameters<ReadingClient["update"]>[0]): Promise<import("./readIndex.js").ReadIndexState>;
38
89
  private assertOpen;
39
90
  getMessages(threadId: string, options?: {
40
91
  limit?: number;
@@ -42,7 +93,13 @@ export declare class ChatSession<T extends StoredMessage> {
42
93
  timestamp: number;
43
94
  id: string;
44
95
  };
45
- }): Promise<import("./messageClient.js").MessageSnapshot<T>>;
96
+ earlier?: boolean;
97
+ all?: boolean;
98
+ refresh?: boolean;
99
+ }): Promise<MessageSnapshot<T>>;
100
+ private rememberHistoryEdge;
101
+ private readEarlier;
102
+ private readAll;
46
103
  sendMessage(threadId: string, input: {
47
104
  to: string;
48
105
  body: ChatBody;
@@ -50,10 +107,25 @@ export declare class ChatSession<T extends StoredMessage> {
50
107
  }): Promise<ChatRuntimeMessage>;
51
108
  getReaders(threadId: string, messageId: string, page?: Omit<MessageReadersQuery, "threadId" | "messageId">, signal?: AbortSignal): Promise<import("./messageReaders.js").MessageReadersPage>;
52
109
  retryMessage(messageId: string): Promise<ChatRuntimeMessage>;
53
- observeMessages(threadId: string, ids: string[], event: "visible" | "played"): Promise<import("./readIndex.js").ReadIndexState>;
110
+ observeMessages(threadId: string, ids: string[], event: "visible" | "played"): ReturnType<ReadingClient["recordVisible"]>;
111
+ observeMessages(threadId: string | undefined, ids: string[], event: "receipts", clientInstanceId?: string): Promise<void>;
112
+ /** Bridge transports can delegate social events without maintaining clients. */
113
+ handleReach(event: OnlineReachEvent): Promise<"handled" | undefined>;
114
+ private wakeCounts;
115
+ private publishCountSnapshot;
116
+ private publishCounts;
117
+ private watchCounts;
118
+ getSnapshot(): ChatSummarySnapshot<T>;
54
119
  getSnapshot(threadId: string): ChatThreadSnapshot<T>;
55
- subscribe(threadId: string, listener: () => void): () => void;
120
+ subscribe(listener: () => void): () => void;
121
+ subscribe(threadId: string, listener: () => void, fields?: readonly (keyof ChatThreadSnapshot<T>)[]): () => void;
56
122
  private publishAll;
123
+ private buildGlobal;
124
+ private publishGlobal;
57
125
  private publish;
126
+ /** Permanently retire this identity/team session. UI withdrawal is immediate;
127
+ * completion means all durable stores have crossed the same authority epoch. */
128
+ revokeAccess(): Promise<void>;
58
129
  close(): Promise<void>;
130
+ private dispose;
59
131
  }