@pmate/chat-sdk 0.12.0-next.35 → 0.12.0-next.37

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.
@@ -297,7 +297,10 @@ export class ChatSession {
297
297
  }
298
298
  if (restart)
299
299
  continue;
300
- return { messages: pages.reverse().flat(), hasEarlier: false, complete: true, resetVersion: version };
300
+ const result = { messages: pages.reverse().flat(), hasEarlier: false, complete: true, resetVersion: version };
301
+ await this.messages.expandWindow(threadId, result);
302
+ this.rememberHistoryEdge(threadId, result);
303
+ return result;
301
304
  }
302
305
  catch (error) {
303
306
  this.assertOpen();
@@ -453,9 +456,9 @@ export class ChatSession {
453
456
  const messages = previous && previous.messages.length === raw.length && previous.messages.every((message, index) => message === raw[index]) ? previous.messages : raw;
454
457
  const outgoing = this.sender.snapshot(threadId);
455
458
  const sameSource = previous && previous.messages === messages && previous.outgoing.length === outgoing.length && previous.outgoing.every((record, index) => record === outgoing[index]);
456
- const semanticReading = previous?.messageReading ?? {};
457
- const semantics = sameSource ? previous.timeline : projectMessageTimeline({ appId: this.options.messages.scope.appId, profileId: this.options.messages.scope.profileId, threadId, messages, outgoing, decode: this.decodeTimeline, reading: semanticReading, previous: previous?.timeline });
458
459
  const candidates = new Map(messages.flatMap(message => { const candidate = this.options.readingCandidate?.(message); return candidate ? [[candidate.id, candidate]] : []; }));
460
+ const semanticReading = previous?.messageReading ?? {};
461
+ const semantics = sameSource ? previous.timeline : projectMessageTimeline({ appId: this.options.messages.scope.appId, profileId: this.options.messages.scope.profileId, threadId, messages, outgoing, candidates, decode: this.decodeTimeline, reading: semanticReading, previous: previous?.timeline });
459
462
  for (const item of semantics)
460
463
  if (item.recalled)
461
464
  candidates.set(item.id, { ...candidates.get(item.id), id: item.id, threadId, from: item.from, mine: item.mine, recalled: true });
@@ -464,7 +467,7 @@ export class ChatSession {
464
467
  const view = projectConversationReading(state, { id: threadId, kind }, [...candidates.values()]);
465
468
  const unread = previous && JSON.stringify(previous.unread) === JSON.stringify(view.unread) ? previous.unread : view.unread;
466
469
  const messageReading = previous && JSON.stringify(previous.messageReading) === JSON.stringify(view.messageReading) ? previous.messageReading : view.messageReading;
467
- const timeline = messageReading === semanticReading ? semantics : projectMessageTimeline({ appId: this.options.messages.scope.appId, profileId: this.options.messages.scope.profileId, threadId, messages, outgoing, decode: this.decodeTimeline, reading: messageReading, previous: previous?.timeline });
470
+ const timeline = messageReading === semanticReading ? semantics : projectMessageTimeline({ appId: this.options.messages.scope.appId, profileId: this.options.messages.scope.profileId, threadId, messages, outgoing, candidates, decode: this.decodeTimeline, reading: messageReading, previous: previous?.timeline });
468
471
  return { timeline, messages, hasEarlier: source?.hasEarlier ?? false, resetVersion: source?.resetVersion ?? 0, complete: source?.complete ?? false, outgoing, readCounts: this.countSnapshots.get(threadId), unread, messageReading };
469
472
  }
470
473
  subscribe(target, listener, fields) {
@@ -91,6 +91,8 @@ export declare class MessageClient<T extends StoredMessage> {
91
91
  private flushWrites;
92
92
  private runWrites;
93
93
  synchronize(thread: string): Promise<void>;
94
+ /** Keep an explicitly requested history range in the shared subscribed view. */
95
+ expandWindow(thread: string, page: MessageSnapshot<T>): Promise<void>;
94
96
  earlier(thread: string, before: {
95
97
  timestamp: number;
96
98
  id: string;
@@ -485,6 +485,18 @@ export class MessageClient {
485
485
  this.syncing.set(thread, task);
486
486
  return task;
487
487
  }
488
+ /** Keep an explicitly requested history range in the shared subscribed view. */
489
+ async expandWindow(thread, page) {
490
+ const first = page.messages[0];
491
+ if (!first || !page.complete)
492
+ return;
493
+ const old = this.windowEdges.get(thread);
494
+ if (!old || old.version !== page.resetVersion || first.timestamp < old.timestamp || first.timestamp === old.timestamp && first.id < old.id)
495
+ this.windowEdges.set(thread, { timestamp: first.timestamp, id: first.id, version: page.resetVersion });
496
+ const snapshot = await this.reload(thread);
497
+ if (snapshot.resetVersion !== page.resetVersion)
498
+ throw new Error("MESSAGE_PROJECTION_CHANGED");
499
+ }
488
500
  async earlier(thread, before, limit = 100, expand = false) {
489
501
  await this.ensureScope();
490
502
  if (this.scopeDenied)
@@ -500,12 +512,8 @@ export class MessageClient {
500
512
  await this.synchronize(thread);
501
513
  }
502
514
  const page = await this.fillWindow(thread, limit, before);
503
- if (expand && page.complete && page.messages.length) {
504
- const first = page.messages[0], old = this.windowEdges.get(thread);
505
- if (!old || old.version !== page.resetVersion || first.timestamp < old.timestamp || first.timestamp === old.timestamp && first.id < old.id)
506
- this.windowEdges.set(thread, { timestamp: first.timestamp, id: first.id, version: page.resetVersion });
507
- await this.reload(thread);
508
- }
515
+ if (expand)
516
+ await this.expandWindow(thread, page);
509
517
  return page;
510
518
  }
511
519
  catch (error) {
@@ -1,7 +1,7 @@
1
1
  import type { StoredMessage } from "./messageStore.js";
2
2
  import type { OutgoingMessage } from "./durableSender.js";
3
3
  import type { ChatRuntimeMessage } from "./protocol.js";
4
- import type { MessageReadingState } from "./conversationReading.js";
4
+ import type { MessageReadingState, ReadingCandidate } from "./conversationReading.js";
5
5
  export type TimelineReactionEvent = Reaction & {
6
6
  threadId: string;
7
7
  };
@@ -18,6 +18,8 @@ export type TimelineItem<T> = {
18
18
  mine: boolean;
19
19
  status: "sending" | "sent" | "failed" | "read";
20
20
  recalled: boolean;
21
+ unread?: boolean;
22
+ attentionEligible?: boolean;
21
23
  message: T | undefined;
22
24
  reading?: MessageReadingState;
23
25
  reactions: readonly TimelineReaction[];
@@ -42,6 +44,7 @@ export declare function projectMessageTimeline<T extends StoredMessage>(input: {
42
44
  outgoing: readonly OutgoingMessage[];
43
45
  decode: (message: ChatRuntimeMessage) => T | undefined;
44
46
  reading: Readonly<Record<string, MessageReadingState>>;
47
+ candidates?: ReadonlyMap<string, ReadingCandidate>;
45
48
  previous?: readonly TimelineItem<T>[];
46
49
  }): readonly TimelineItem<T>[];
47
50
  export {};
@@ -106,13 +106,14 @@ export function projectMessageTimeline(input) {
106
106
  continue;
107
107
  const mine = envelope.from === input.profileId, reading = input.reading[message.id];
108
108
  const withdrawn = Boolean(tombstones.has(message.id) || message.recalled || envelope.legacyDisplay?.recalled || envelope.from && recalled.get(message.id)?.has(envelope.from));
109
- const item = { id: message.id, threadId: message.threadId, timestamp: message.timestamp, from: envelope.from, mine, status: !mine && reading ? (reading.read ? "read" : status === "read" ? "sent" : status) : status, recalled: withdrawn,
109
+ const candidate = input.candidates?.get(message.id);
110
+ const item = { id: message.id, threadId: message.threadId, timestamp: message.timestamp, from: envelope.from, mine, status: !mine && reading ? (reading.read ? "read" : status === "read" ? "sent" : status) : status, recalled: withdrawn, ...(candidate ? { unread: !mine && !withdrawn && (reading ? !reading.read : candidate.unread), attentionEligible: candidate.attentionEligible } : {}),
110
111
  message: withdrawn ? undefined : message, reading, reactionEvents: withdrawn ? [] : groupedEvents.get(message.id) ?? [], reactions: withdrawn ? [] : [...(grouped.get(message.id) ?? [])].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([emojiId, users]) => { const userIds = [...users].sort(); return { emojiId, userIds, mine: userIds.includes(input.profileId) }; }) };
111
112
  const old = previous.get(item.id);
112
113
  const sameReading = old?.reading === item.reading || old?.reading?.seq === item.reading?.seq && old?.reading?.read === item.reading?.read && old?.reading?.readPolicy === item.reading?.readPolicy;
113
114
  const sameReactions = old?.reactions.length === item.reactions.length && old.reactions.every((reaction, index) => { const next = item.reactions[index]; return reaction.emojiId === next.emojiId && reaction.mine === next.mine && reaction.userIds.length === next.userIds.length && reaction.userIds.every((user, i) => user === next.userIds[i]); });
114
115
  const sameEvents = old?.reactionEvents.length === item.reactionEvents.length && old.reactionEvents.every((event, index) => { const next = item.reactionEvents[index]; return event.id === next.id && event.from === next.from && event.emojiId === next.emojiId && event.active === next.active && event.revision === next.revision && event.timestamp === next.timestamp && event.threadId === next.threadId && event.messageId === next.messageId; });
115
- result.push(old && sameEvents && old.message === item.message && old.timestamp === item.timestamp && old.from === item.from && old.mine === item.mine && old.status === item.status && old.recalled === item.recalled && sameReading && sameReactions ? old : item);
116
+ result.push(old && sameEvents && old.message === item.message && old.timestamp === item.timestamp && old.from === item.from && old.mine === item.mine && old.status === item.status && old.recalled === item.recalled && old.unread === item.unread && old.attentionEligible === item.attentionEligible && sameReading && sameReactions ? old : item);
116
117
  }
117
118
  result.sort((a, b) => a.timestamp - b.timestamp || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
118
119
  return input.previous?.length === result.length && result.every((item, index) => item === input.previous[index]) ? input.previous : result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.35",
3
+ "version": "0.12.0-next.37",
4
4
  "description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",