@pmate/chat-sdk 0.12.0-next.79 → 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.
@@ -0,0 +1,36 @@
1
+ import { type SocialGroup } from "./runtime/socialReadClient.js";
2
+ import type { ChatMessage, UnreadState } from "./chatTypes.js";
3
+ export type ConversationSnapshotOptions = {
4
+ appId: string;
5
+ accountId: string;
6
+ profileId: string;
7
+ teamId: string;
8
+ threadId: string;
9
+ token: string;
10
+ hubHttpUrl?: string;
11
+ limit?: number;
12
+ fetch?: typeof fetch;
13
+ signal?: AbortSignal;
14
+ };
15
+ export type ConversationSnapshot = Readonly<{
16
+ appId: string;
17
+ accountId: string;
18
+ profileId: string;
19
+ teamId: string;
20
+ threadId: string;
21
+ generatedAt: number;
22
+ contact: Readonly<{
23
+ peerId: string;
24
+ type: "group" | "user";
25
+ }>;
26
+ group: SocialGroup | null;
27
+ messages: readonly ChatMessage[];
28
+ hasEarlier: boolean;
29
+ unread: UnreadState;
30
+ checkpoint: string;
31
+ readingCursor: string;
32
+ serverTiming: string | null;
33
+ }>;
34
+ /** One network first-screen read. This does not open a socket, write a body cache,
35
+ * install a partial reading index, or mark anything read. Live SDK state takes over later. */
36
+ export declare function fetchConversationSnapshot(options: ConversationSnapshotOptions): Promise<ConversationSnapshot>;
@@ -0,0 +1,50 @@
1
+ import { SocialReadError } from "./runtime/socialReadClient.js";
2
+ import { projectConversationReading } from "./runtime/conversationReading.js";
3
+ import { projectMessageTimeline } from "./runtime/messageTimeline.js";
4
+ import { createMessagePresenter } from "./messageView.js";
5
+ const invalid = () => new SocialReadError(502, "INVALID_THREAD_SNAPSHOT_RESPONSE");
6
+ /** One network first-screen read. This does not open a socket, write a body cache,
7
+ * install a partial reading index, or mark anything read. Live SDK state takes over later. */
8
+ export async function fetchConversationSnapshot(options) {
9
+ const limit = options.limit ?? 30;
10
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
11
+ throw new SocialReadError(400, "INVALID_THREAD_SNAPSHOT_LIMIT");
12
+ const query = new URLSearchParams({ appId: options.appId, profileId: options.profileId, teamId: options.teamId, threadId: options.threadId, limit: String(limit) });
13
+ const response = await (options.fetch ?? fetch)(`${(options.hubHttpUrl ?? "https://hub.pmate.chat").replace(/\/+$/, "")}/api/social/thread-snapshot?${query}`, {
14
+ headers: { authorization: `Bearer ${options.token}`, accept: "application/json" }, cache: "no-store", redirect: "error",
15
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(15000)]) : AbortSignal.timeout(15000),
16
+ });
17
+ const envelope = await response.json().catch(() => null);
18
+ if (!response.ok || envelope?.success !== true)
19
+ throw new SocialReadError(response.status, envelope?.message ?? "THREAD_SNAPSHOT_FAILED");
20
+ const value = envelope.data;
21
+ if (!value || value.schemaVersion !== 1 || ["appId", "accountId", "profileId", "teamId", "threadId"].some(key => value[key] !== options[key]) || !Number.isFinite(value.generatedAt))
22
+ throw invalid();
23
+ const cursor = (value) => typeof value === "string" && /^mp1:\d+:[0-9a-f]{64}$/.test(value);
24
+ const page = value.page, reading = value.reading;
25
+ if (!page || !Array.isArray(page.messages) || page.messages.length > limit || !cursor(page.checkpoint) || (page.nextBefore !== null && !cursor(page.nextBefore)) || typeof page.hasMoreChanges !== "boolean" || page.hasMoreChanges || page.resetRequired !== false)
26
+ throw invalid();
27
+ if (page.messages.some(row => row.appId !== options.appId || row.teamId !== options.teamId || row.threadId !== options.threadId || !row.messageId || !Number.isFinite(row.serverCreatedAt)))
28
+ throw invalid();
29
+ if (!value.contact || !["group", "user"].includes(value.contact.type) || !value.contact.peerId)
30
+ throw invalid();
31
+ if (value.contact.type === "group" && (!value.group || value.group.appId !== options.appId || value.group.teamId !== options.teamId || value.group.groupId !== value.contact.peerId || !["active", "dissolved"].includes(value.group.status ?? "")))
32
+ throw invalid();
33
+ if (!reading || reading.schemaVersion !== 1 || reading.appId !== options.appId || reading.profileId !== options.profileId || reading.scope !== "thread-window" || typeof reading.syncCursor !== "string" || !Array.isArray(reading.threads) || reading.threads.length > 1 || reading.threads.some(thread => thread.id !== options.threadId || thread.teamId !== options.teamId || !Array.isArray(thread.messages) || !Array.isArray(thread.explicitReadMessageIds) || !Number.isSafeInteger(thread.readThroughSeq)))
34
+ throw invalid();
35
+ const summary = reading.summary;
36
+ if (!summary || [summary.unreadMessageCount, summary.unreadMentionCount, summary.badgeCount].some(count => !Number.isSafeInteger(count) || count < 0))
37
+ throw invalid();
38
+ const ids = new Set(page.messages.map(row => row.messageId));
39
+ if (reading.threads.some(thread => thread.messages.some(fact => !ids.has(fact.id) || !Number.isSafeInteger(fact.seq) || !Array.isArray(fact.recipients) || !Array.isArray(fact.mentions) || !["visible", "explicit"].includes(fact.readPolicy))))
40
+ throw invalid();
41
+ const facts = new Map(reading.threads.flatMap(thread => thread.messages).map(fact => [fact.id, fact]));
42
+ const messages = page.messages.map(row => ({ ...row, id: row.messageId, timestamp: row.serverCreatedAt, ...(facts.get(row.messageId)?.recalled ? { recalled: true } : {}) }));
43
+ const kind = value.contact.type === "group" ? "group" : "dm";
44
+ const state = { ...reading, pending: [] };
45
+ const projected = projectConversationReading(state, { id: options.threadId, kind });
46
+ const present = createMessagePresenter();
47
+ const timeline = projectMessageTimeline({ appId: options.appId, profileId: options.profileId, threadId: options.threadId, messages, outgoing: [], decode: () => undefined, reading: projected.messageReading });
48
+ return { appId: value.appId, accountId: value.accountId, profileId: value.profileId, teamId: value.teamId, threadId: value.threadId, generatedAt: value.generatedAt, contact: value.contact, group: value.group,
49
+ messages: timeline.map(present), hasEarlier: page.nextBefore !== null, unread: summary, checkpoint: page.checkpoint, readingCursor: reading.syncCursor, serverTiming: response.headers.get("server-timing") };
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.79",
3
+ "version": "0.12.0-next.80",
4
4
  "description": "Local-first chat SDK, message subscriptions, reading state, and shared chat UI primitives",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -48,6 +48,11 @@
48
48
  "types": "./lib/history.d.ts",
49
49
  "import": "./lib/history.js",
50
50
  "default": "./lib/history.js"
51
+ },
52
+ "./snapshot": {
53
+ "types": "./lib/snapshot.d.ts",
54
+ "import": "./lib/snapshot.js",
55
+ "default": "./lib/snapshot.js"
51
56
  }
52
57
  },
53
58
  "publishConfig": {