@pmate/chat-sdk 0.12.0-next.7 → 0.12.0-next.70
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 +152 -81
- package/lib/advanced.d.ts +10 -0
- package/lib/advanced.js +10 -0
- package/lib/chat.d.ts +5 -0
- package/lib/chat.js +11 -0
- package/lib/chatContract.d.ts +148 -0
- package/lib/chatContract.js +1 -0
- package/lib/chatError.d.ts +11 -0
- package/lib/chatError.js +13 -0
- package/lib/chatTypes.d.ts +121 -0
- package/lib/chatTypes.js +1 -0
- package/lib/credentials.d.ts +26 -0
- package/lib/credentials.js +110 -0
- package/lib/fileMetadata.d.ts +9 -0
- package/lib/fileMetadata.js +18 -0
- package/lib/framework.d.ts +11 -0
- package/lib/framework.js +12 -0
- package/lib/history.d.ts +4 -0
- package/lib/history.js +8 -0
- package/lib/hooks/useChatSnapshot.d.ts +4 -0
- package/lib/hooks/useChatSnapshot.js +9 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/internal/chatEngine.d.ts +131 -0
- package/lib/internal/chatEngine.js +280 -0
- package/lib/internal/chatRegistry.d.ts +4 -0
- package/lib/internal/chatRegistry.js +4 -0
- package/lib/internal/createPublicChat.d.ts +4 -0
- package/lib/internal/createPublicChat.js +52 -0
- package/lib/messageView.d.ts +6 -0
- package/lib/messageView.js +103 -0
- package/lib/runtime/batchedReadIndex.d.ts +16 -3
- package/lib/runtime/batchedReadIndex.js +50 -4
- package/lib/runtime/cachedMessage.d.ts +12 -0
- package/lib/runtime/cachedMessage.js +18 -0
- package/lib/runtime/chatClient.d.ts +2 -0
- package/lib/runtime/chatClient.js +8 -3
- package/lib/runtime/chatError.d.ts +3 -0
- package/lib/runtime/chatError.js +15 -0
- package/lib/runtime/chatSession.d.ts +127 -13
- package/lib/runtime/chatSession.js +727 -40
- package/lib/runtime/connectivity.d.ts +4 -0
- package/lib/runtime/connectivity.js +8 -0
- package/lib/runtime/conversationDirectory.d.ts +37 -0
- package/lib/runtime/conversationDirectory.js +116 -0
- package/lib/runtime/conversationRead.d.ts +13 -0
- package/lib/runtime/conversationRead.js +34 -0
- package/lib/runtime/conversationReading.d.ts +28 -0
- package/lib/runtime/conversationReading.js +47 -0
- package/lib/runtime/createChat.d.ts +11 -7
- package/lib/runtime/createChat.js +46 -23
- package/lib/runtime/durableSender.d.ts +97 -14
- package/lib/runtime/durableSender.js +786 -71
- package/lib/runtime/groupClient.d.ts +76 -0
- package/lib/runtime/groupClient.js +325 -0
- package/lib/runtime/index.d.ts +7 -0
- package/lib/runtime/index.js +5 -0
- package/lib/runtime/messageClient.d.ts +35 -8
- package/lib/runtime/messageClient.js +364 -66
- package/lib/runtime/messageControls.d.ts +19 -0
- package/lib/runtime/messageControls.js +44 -0
- package/lib/runtime/messageLaunch.d.ts +44 -0
- package/lib/runtime/messageLaunch.js +137 -0
- package/lib/runtime/messageQuotes.d.ts +3 -0
- package/lib/runtime/messageQuotes.js +17 -0
- package/lib/runtime/messageReaders.d.ts +12 -1
- package/lib/runtime/messageReaders.js +67 -3
- package/lib/runtime/messageStore.d.ts +40 -2
- package/lib/runtime/messageStore.js +227 -11
- package/lib/runtime/messageTimeline.d.ts +50 -0
- package/lib/runtime/messageTimeline.js +120 -0
- package/lib/runtime/outboxSchema.d.ts +5 -0
- package/lib/runtime/outboxSchema.js +59 -0
- package/lib/runtime/outgoingContent.d.ts +89 -0
- package/lib/runtime/outgoingContent.js +113 -0
- package/lib/runtime/readBatchJournal.d.ts +35 -0
- package/lib/runtime/readBatchJournal.js +210 -0
- package/lib/runtime/readCounts.d.ts +15 -1
- package/lib/runtime/readCounts.js +97 -9
- package/lib/runtime/readIndex.d.ts +68 -7
- package/lib/runtime/readIndex.js +460 -22
- package/lib/runtime/readSchema.d.ts +2 -0
- package/lib/runtime/readSchema.js +58 -0
- package/lib/runtime/readSync.d.ts +1 -0
- package/lib/runtime/readSync.js +26 -9
- package/lib/runtime/readingClient.d.ts +19 -0
- package/lib/runtime/readingClient.js +131 -19
- package/lib/runtime/readingContent.d.ts +5 -0
- package/lib/runtime/readingContent.js +23 -0
- package/lib/runtime/responseCache.d.ts +59 -0
- package/lib/runtime/responseCache.js +232 -0
- package/lib/runtime/socialReadClient.d.ts +4 -2
- package/lib/runtime/socialReadClient.js +12 -8
- package/lib/runtime/voiceUpload.d.ts +2 -0
- package/lib/runtime/voiceUpload.js +27 -0
- package/package.json +27 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { waitForCaller } from "../credentials.js";
|
|
2
|
+
import { ChatEngine } from "./chatEngine.js";
|
|
3
|
+
import { registerChat } from "./chatRegistry.js";
|
|
4
|
+
import { createChat as createSession } from "../runtime/createChat.js";
|
|
5
|
+
import { asChatError } from "../runtime/chatError.js";
|
|
6
|
+
import { ChatError } from "../chatError.js";
|
|
7
|
+
/** Build only the public capabilities; no aliases or engine fields on the returned object. */
|
|
8
|
+
export function createPublicChat(options) {
|
|
9
|
+
const configured = { ...options, onError: (error) => options.onError?.(asChatError(error)) };
|
|
10
|
+
let session;
|
|
11
|
+
try {
|
|
12
|
+
session = createSession(configured);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw asChatError(error);
|
|
16
|
+
}
|
|
17
|
+
const engine = new ChatEngine(session, configured);
|
|
18
|
+
const keys = (value, allowed) => { if (Object.keys(value).some(key => !allowed.includes(key)))
|
|
19
|
+
throw new ChatError("invalid-input", "Unsupported operation options"); };
|
|
20
|
+
const read = (run, options = {}, cleanup) => {
|
|
21
|
+
try {
|
|
22
|
+
keys(options, ["signal"]);
|
|
23
|
+
options.signal?.throwIfAborted();
|
|
24
|
+
return waitForCaller(run(), options.signal, cleanup);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return Promise.reject(error);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const chat = Object.freeze({
|
|
31
|
+
start: () => engine.start(), close: () => engine.close(),
|
|
32
|
+
resolveThreadId: (target, options = {}) => read(() => engine.resolveThreadId(target), options),
|
|
33
|
+
getThread: (id, options) => read(() => engine.getThread(id), options), loadEarlier: (id, options) => read(() => engine.loadEarlier(id), options), refreshThread: (id, options) => read(() => engine.refreshThread(id), options),
|
|
34
|
+
subscribeThread: (id, listener, options = {}) => { keys(options, ["fields"]); return engine.subscribeThread(id, listener, options); }, subscribeChat: (listener, options = {}) => { keys(options, ["fields"]); return engine.subscribe(listener, options); },
|
|
35
|
+
sendMessage: (id, content, options) => engine.sendMessage(id, content, options), retryMessage: id => engine.retryMessage(id),
|
|
36
|
+
observeMessages: (id, ids, event) => engine.observeMessages(id, ids, event), markRead: id => engine.markRead(id), watchReaders: (id, ids, receiver) => engine.watchReaders(id, ids, receiver),
|
|
37
|
+
getReaders: (id, messageId, options = {}) => {
|
|
38
|
+
try {
|
|
39
|
+
keys(options, ["afterProfileId", "revision", "signal"]);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return Promise.reject(error);
|
|
43
|
+
}
|
|
44
|
+
return read(() => engine.getReaders(id, messageId, { afterProfileId: options.afterProfileId, revision: options.revision }), { signal: options.signal });
|
|
45
|
+
},
|
|
46
|
+
getVoiceAttachment: (id, messageId, options = {}) => read(() => engine.getVoiceAttachment(id, messageId), options, lease => lease.release()),
|
|
47
|
+
getGroup: (id, options = {}) => read(() => engine.getGroup(id), options), refreshGroup: (id, options = {}) => read(() => engine.getGroup(id, { refresh: true }), options),
|
|
48
|
+
createGroup: input => engine.createGroup(input), updateGroup: (id, changes) => engine.updateGroup(id, changes),
|
|
49
|
+
});
|
|
50
|
+
registerChat(chat, engine);
|
|
51
|
+
return chat;
|
|
52
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TimelineItem } from "./runtime/messageTimeline.js";
|
|
2
|
+
import type { CachedChatMessage } from "./runtime/cachedMessage.js";
|
|
3
|
+
import type { ChatMessage } from "./chatTypes.js";
|
|
4
|
+
export type { ChatMessage } from "./chatTypes.js";
|
|
5
|
+
/** Decode a body once; reading and delivery projections reuse its normalized content. */
|
|
6
|
+
export declare function createMessagePresenter(): (item: TimelineItem<CachedChatMessage>) => ChatMessage;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { parseChatFile } from "./fileMetadata.js";
|
|
2
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
4
|
+
function media(value) {
|
|
5
|
+
if (typeof value !== "string" || !value)
|
|
6
|
+
return undefined;
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(value, "https://chat.invalid");
|
|
9
|
+
return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function quote(value) {
|
|
16
|
+
const record = object(value);
|
|
17
|
+
if (typeof record.senderName !== "string" || typeof record.content !== "string" || record.content.length > 6000)
|
|
18
|
+
return;
|
|
19
|
+
return { senderName: record.senderName.slice(0, 100), content: record.content, ...(typeof record.messageId === "string" ? { messageId: record.messageId } : {}) };
|
|
20
|
+
}
|
|
21
|
+
function legacyDuration(value) {
|
|
22
|
+
if (typeof value !== "string" || !/^\d+:\d{2}$/.test(value))
|
|
23
|
+
return 0;
|
|
24
|
+
const [minutes, seconds] = value.split(":").map(Number);
|
|
25
|
+
return (minutes * 60 + seconds) * 1000;
|
|
26
|
+
}
|
|
27
|
+
function content(message) {
|
|
28
|
+
const body = object(message.body), outer = object(body.content);
|
|
29
|
+
const envelope = body.kind === "quote" && outer.content !== undefined ? outer : body;
|
|
30
|
+
const payload = object(envelope.content);
|
|
31
|
+
const legacy = object(message.legacyDisplay);
|
|
32
|
+
const validLegacy = legacy.id === message.id && legacy.threadId === message.threadId && legacy.orgId === message.teamId && legacy.from === message.from && typeof legacy.content === "string";
|
|
33
|
+
const hint = validLegacy ? legacy : {};
|
|
34
|
+
const file = parseChatFile(payload.file === undefined ? hint.file : payload.file);
|
|
35
|
+
const contentKind = envelope.contentKind, voice = object(payload.voice);
|
|
36
|
+
const voiceHint = hint.kind === "voice";
|
|
37
|
+
const voiceUrl = media(voice.url) ?? (voiceHint ? media(hint.audioUrl) : undefined) ?? "";
|
|
38
|
+
const durationMs = typeof voice.durationMs === "number" && Number.isFinite(voice.durationMs) ? voice.durationMs : voiceHint ? legacyDuration(hint.duration) : 0;
|
|
39
|
+
const hasVoice = voiceHint || Object.keys(voice).length > 0 && (voiceUrl.length > 0 || durationMs > 0) || contentKind === "voice";
|
|
40
|
+
const imageUrl = media(payload.url) ?? (hint.kind === "image" ? media(hint.imageUrl) : undefined);
|
|
41
|
+
const messageQuote = quote(payload.quote) ?? quote(hint.quote);
|
|
42
|
+
const rawText = text(payload.text) || text(object(body.payload).message);
|
|
43
|
+
const result = { kind: "text", text: validLegacy ? text(hint.content) : rawText, senderName: text(hint.senderName) || undefined, senderAvatar: media(hint.senderAvatar), timeLabel: text(hint.createdAt) || undefined,
|
|
44
|
+
recipientCount: message.threadKind === "dm" ? 1 : validLegacy ? typeof hint.recipientCount === "number" ? hint.recipientCount : undefined : Array.isArray(message.recipients) ? new Set(message.recipients.filter(id => id !== message.from)).size : undefined };
|
|
45
|
+
if (validLegacy && ["emoji", "system", "attachment", "quote"].includes(String(hint.kind)))
|
|
46
|
+
result.kind = hint.kind;
|
|
47
|
+
if (body.kind === "contact") {
|
|
48
|
+
result.kind = ["friendRequest", "requestFriend"].includes(String(body.action)) ? "invitation" : "system";
|
|
49
|
+
if (result.kind === "system")
|
|
50
|
+
result.system = { type: "contact-updated" };
|
|
51
|
+
}
|
|
52
|
+
else if (body.kind === "group") {
|
|
53
|
+
result.kind = "system";
|
|
54
|
+
result.system = { type: body.action === "groupCreate" ? "group-created" : "group-updated", title: text(object(body.payload).title) || undefined };
|
|
55
|
+
}
|
|
56
|
+
else if (file) {
|
|
57
|
+
result.kind = "attachment";
|
|
58
|
+
result.file = file;
|
|
59
|
+
}
|
|
60
|
+
else if (hasVoice) {
|
|
61
|
+
result.kind = "voice";
|
|
62
|
+
result.voice = { url: voiceUrl, durationMs, ...(text(voice.transcript) || text(hint.transcript) ? { transcript: text(voice.transcript) || text(hint.transcript) } : {}) };
|
|
63
|
+
}
|
|
64
|
+
else if ((contentKind === "image" || hint.kind === "image") && imageUrl) {
|
|
65
|
+
result.kind = "image";
|
|
66
|
+
result.image = { url: imageUrl, name: text(payload.name) || text(hint.imageAlt) || undefined };
|
|
67
|
+
}
|
|
68
|
+
else if (contentKind === "emoji") {
|
|
69
|
+
result.kind = "emoji";
|
|
70
|
+
result.text = text(payload.code) || result.text;
|
|
71
|
+
}
|
|
72
|
+
if (messageQuote) {
|
|
73
|
+
result.quote = messageQuote;
|
|
74
|
+
if (result.kind === "text")
|
|
75
|
+
result.kind = "quote";
|
|
76
|
+
}
|
|
77
|
+
if (result.kind === "attachment")
|
|
78
|
+
result.attachment = { name: text(hint.attachmentName) || undefined, size: text(hint.attachmentSize) || undefined };
|
|
79
|
+
if (payload.format !== undefined)
|
|
80
|
+
result.richContent = payload;
|
|
81
|
+
else if (hint.richContent !== undefined)
|
|
82
|
+
result.richContent = hint.richContent;
|
|
83
|
+
if (Array.isArray(payload.mentions))
|
|
84
|
+
result.mentions = payload.mentions;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/** Decode a body once; reading and delivery projections reuse its normalized content. */
|
|
88
|
+
export function createMessagePresenter() {
|
|
89
|
+
const bodies = new WeakMap(), items = new WeakMap();
|
|
90
|
+
return (item) => {
|
|
91
|
+
const previous = items.get(item);
|
|
92
|
+
if (previous)
|
|
93
|
+
return previous;
|
|
94
|
+
let value = { kind: "system", text: "" };
|
|
95
|
+
if (!item.recalled && item.message) {
|
|
96
|
+
value = bodies.get(item.message) ?? content(item.message);
|
|
97
|
+
bodies.set(item.message, value);
|
|
98
|
+
}
|
|
99
|
+
const result = { id: item.id, threadId: item.threadId, timestamp: item.timestamp, from: item.from, mine: item.mine, status: item.status, recalled: item.recalled, unread: item.unread, attentionEligible: item.attentionEligible, reading: item.reading, reactions: item.reactions, ...value };
|
|
100
|
+
items.set(item, result);
|
|
101
|
+
return result;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
+
}
|
|
@@ -16,6 +16,7 @@ export type ChatClientOptions = {
|
|
|
16
16
|
onNotification?: OnlineNotificationHandler;
|
|
17
17
|
profileId?: string;
|
|
18
18
|
botKey?: PmateBotKeyFile | string | unknown;
|
|
19
|
+
getToken?: () => Promise<string>;
|
|
19
20
|
session?: BotLoginSession;
|
|
20
21
|
hubUrl?: string;
|
|
21
22
|
authBaseUrl?: string;
|
|
@@ -62,6 +63,7 @@ export declare class ChatClient {
|
|
|
62
63
|
profileId: string | undefined;
|
|
63
64
|
accountId: string | undefined;
|
|
64
65
|
teamId: string | undefined;
|
|
66
|
+
clientInstanceId: string;
|
|
65
67
|
};
|
|
66
68
|
watchConnection(handler: (state: ChatConnectionState) => void): () => void;
|
|
67
69
|
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);
|
|
@@ -161,7 +161,7 @@ export class ChatClient {
|
|
|
161
161
|
catch (error) {
|
|
162
162
|
if (generation !== this.generation)
|
|
163
163
|
return;
|
|
164
|
-
if (error instanceof ChatAckError) {
|
|
164
|
+
if (error instanceof ChatAckError || ["authentication-required", "access-denied"].includes(error?.code ?? "")) {
|
|
165
165
|
this.close();
|
|
166
166
|
this.setConnection("authentication_required");
|
|
167
167
|
}
|
|
@@ -295,7 +295,12 @@ export class ChatClient {
|
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
297
|
async sendAuth() {
|
|
298
|
-
const
|
|
298
|
+
const generation = this.generation;
|
|
299
|
+
const token = this.options.getToken ? await this.options.getToken() : this.session?.token;
|
|
300
|
+
if (generation !== this.generation)
|
|
301
|
+
throw new Error("ChatClient closed");
|
|
302
|
+
if (this.session && token)
|
|
303
|
+
this.session = { ...this.session, token };
|
|
299
304
|
if (!token) {
|
|
300
305
|
throw new Error("Missing PMate session token");
|
|
301
306
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ChatError } from "../chatError.js";
|
|
2
|
+
export { ChatError } from "../chatError.js";
|
|
3
|
+
export function asChatError(error) {
|
|
4
|
+
if (error instanceof ChatError)
|
|
5
|
+
return error;
|
|
6
|
+
const value = error;
|
|
7
|
+
const status = value?.status, message = value?.message ?? String(error);
|
|
8
|
+
const code = status === 401 ? "authentication-required" : status === 403 || /ACCESS_REVOKED|ACCESS_DENIED|SCOPE_CHANGED/.test(message) ? "access-denied"
|
|
9
|
+
: /CLOSED/.test(message) ? "closed"
|
|
10
|
+
: status === 409 || /REVISION_CONFLICT|PROJECTION_CHANGED/.test(message) ? "conflict"
|
|
11
|
+
: /BACKPRESSURE|TOO_LARGE/.test(message) ? "busy"
|
|
12
|
+
: status === 400 || /INVALID_|CONVERSATION_MISMATCH|CONVERSATION_UNKNOWN|NOT_OWNED/.test(message) ? "invalid-input"
|
|
13
|
+
: /QuotaExceeded|DatabaseClosed|UnknownError|AbortError/.test(value?.name ?? "") || /STORAGE_|DATABASE_|CACHE_WRITE/.test(message) ? "storage-unavailable" : "unavailable";
|
|
14
|
+
return new ChatError(code, message, { cause: error, status });
|
|
15
|
+
}
|
|
@@ -1,59 +1,124 @@
|
|
|
1
|
+
import { type ConversationTarget } from "./conversationDirectory.js";
|
|
2
|
+
import { type ChatGroup } from "./groupClient.js";
|
|
3
|
+
import type { SocialGroup } from "./socialReadClient.js";
|
|
4
|
+
import { type ChatConversation, type OutgoingContent } from "./outgoingContent.js";
|
|
5
|
+
import { type TimelineItem } from "./messageTimeline.js";
|
|
6
|
+
import { type ReadingCandidate, type MessageReadingState } from "./conversationReading.js";
|
|
7
|
+
import type { ConversationReadMessage } from "./conversationRead.js";
|
|
8
|
+
import { type ReadCountsResult } from "./readCounts.js";
|
|
9
|
+
import type { OnlineReachEvent } from "./reach.js";
|
|
1
10
|
import { type MessageReadersQuery } from "./messageReaders.js";
|
|
2
11
|
import { type MessageClientOptions, type MessageSnapshot } from "./messageClient.js";
|
|
3
|
-
import { DurableChatSender } from "./durableSender.js";
|
|
4
|
-
import { type ReadingClientOptions } from "./readingClient.js";
|
|
12
|
+
import { DurableChatSender, type SendMessageInput, type DurableSenderOptions, type VoiceAttachmentLease } from "./durableSender.js";
|
|
13
|
+
import { ReadingClient, type ReadingClientOptions } from "./readingClient.js";
|
|
5
14
|
import { ChatClient, type ChatClientOptions, type ChatConnectionState } from "./chatClient.js";
|
|
6
15
|
import { unreadInConversation } from "./readIndex.js";
|
|
7
16
|
import type { StoredMessage } from "./messageStore.js";
|
|
8
|
-
import type {
|
|
17
|
+
import type { ChatRuntimeMessage } from "./protocol.js";
|
|
9
18
|
export type ChatSessionOptions<T extends StoredMessage> = {
|
|
19
|
+
onDispose?: () => void;
|
|
10
20
|
messages: MessageClientOptions<T>;
|
|
11
21
|
connection: ChatClientOptions;
|
|
12
22
|
sharedConnection?: ChatClient;
|
|
23
|
+
activeThreadId?: string;
|
|
24
|
+
uploadVoice?: DurableSenderOptions["uploadVoice"];
|
|
25
|
+
reconcileOutgoing?: DurableSenderOptions["reconcile"];
|
|
26
|
+
loadGroup?: (groupId: string, signal: AbortSignal) => Promise<SocialGroup>;
|
|
13
27
|
reading: ReadingClientOptions;
|
|
14
28
|
decode: (message: ChatRuntimeMessage) => T | undefined;
|
|
29
|
+
readingCandidate?: (message: T) => ReadingCandidate | undefined;
|
|
15
30
|
onError: (error: unknown) => void;
|
|
16
31
|
};
|
|
17
32
|
export type ChatThreadSnapshot<T> = {
|
|
18
33
|
messages: readonly T[];
|
|
34
|
+
timeline: readonly TimelineItem<T>[];
|
|
19
35
|
outgoing: ReturnType<DurableChatSender["snapshot"]>;
|
|
20
36
|
hasEarlier: boolean;
|
|
21
37
|
resetVersion: number;
|
|
22
38
|
complete: boolean;
|
|
23
|
-
|
|
39
|
+
group?: ChatGroup;
|
|
40
|
+
readCounts?: ReadCountsResult;
|
|
41
|
+
unread: ReturnType<typeof unreadInConversation> | undefined;
|
|
42
|
+
messageReading: Readonly<Record<string, MessageReadingState>>;
|
|
24
43
|
};
|
|
25
44
|
export type ChatSummarySnapshot<T> = {
|
|
26
45
|
threads: readonly {
|
|
27
46
|
threadId: string;
|
|
28
47
|
lastMessage?: T;
|
|
48
|
+
lastItem?: TimelineItem<T>;
|
|
49
|
+
group?: ChatGroup;
|
|
29
50
|
unread: ChatThreadSnapshot<T>["unread"];
|
|
30
51
|
outgoingCount: number;
|
|
31
52
|
}[];
|
|
32
|
-
unreadMessageCount: number;
|
|
33
|
-
badgeCount: number;
|
|
53
|
+
unreadMessageCount: number | undefined;
|
|
54
|
+
badgeCount: number | undefined;
|
|
34
55
|
connection: ChatConnectionState;
|
|
35
56
|
};
|
|
36
57
|
/** Owns all browser message clients. Hosts subscribe to stable thread snapshots. */
|
|
37
58
|
export declare class ChatSession<T extends StoredMessage> {
|
|
38
59
|
private options;
|
|
60
|
+
private conversations;
|
|
39
61
|
private messages;
|
|
40
62
|
private connection;
|
|
41
63
|
private reading;
|
|
42
64
|
private readers;
|
|
65
|
+
private groups?;
|
|
66
|
+
private counts?;
|
|
67
|
+
private countsReceiver?;
|
|
68
|
+
private countsThread?;
|
|
69
|
+
private countSnapshots;
|
|
70
|
+
private countClosings;
|
|
71
|
+
private countTasks;
|
|
72
|
+
private controlTails;
|
|
73
|
+
private acceptedControls;
|
|
74
|
+
private controlCount;
|
|
75
|
+
private controlBytes;
|
|
43
76
|
private sender;
|
|
44
77
|
private listeners;
|
|
45
78
|
private snapshots;
|
|
79
|
+
private timelineDecoded;
|
|
80
|
+
private decodeTimeline;
|
|
81
|
+
private historyEdges;
|
|
82
|
+
private earlierReads;
|
|
46
83
|
private off;
|
|
47
84
|
private globalListeners;
|
|
48
85
|
private globalSnapshot?;
|
|
86
|
+
private closedThread;
|
|
87
|
+
private closedSummary;
|
|
49
88
|
private connectionState;
|
|
50
89
|
private catchUp;
|
|
90
|
+
private readingAllowed;
|
|
91
|
+
private readingListeners;
|
|
92
|
+
private acceptedReading;
|
|
93
|
+
private readingAccess?;
|
|
51
94
|
private readingStarted;
|
|
95
|
+
private readingStarting?;
|
|
96
|
+
private withdrawn;
|
|
97
|
+
private revoking?;
|
|
52
98
|
private closed;
|
|
53
99
|
private starting?;
|
|
100
|
+
private initializing?;
|
|
54
101
|
private closing?;
|
|
55
102
|
constructor(options: ChatSessionOptions<T>);
|
|
103
|
+
private scheduleCatchUp;
|
|
56
104
|
start(): Promise<void>;
|
|
105
|
+
/** Initializes local state and background reading sync without opening a
|
|
106
|
+
* second transport when the host already owns calls/presence. */
|
|
107
|
+
initialize(): Promise<void>;
|
|
108
|
+
private startReading;
|
|
109
|
+
/** One-time migration only: never establishes trusted history coverage. */
|
|
110
|
+
importLegacyMessages(threadId: string, messages: T[]): Promise<void>;
|
|
111
|
+
getReadingSnapshot(): import("./readIndex.js").ReadIndexState | undefined;
|
|
112
|
+
subscribeReading(listener: () => void): () => void;
|
|
113
|
+
private setReadingAllowed;
|
|
114
|
+
private readingAuthority;
|
|
115
|
+
private authorizeReading;
|
|
116
|
+
private readingOperation;
|
|
117
|
+
refreshReading(): void;
|
|
118
|
+
synchronizeReading(cursor?: string, profileId?: string): Promise<import("./readIndex.js").ReadIndexState>;
|
|
119
|
+
/** Explicit text-history acknowledgement; unheard audio stays unread. */
|
|
120
|
+
markRead(threadId: string, messages?: readonly ConversationReadMessage[]): Promise<import("./readIndex.js").ReadIndexState>;
|
|
121
|
+
updateReading(transform: Parameters<ReadingClient["update"]>[0]): Promise<import("./readIndex.js").ReadIndexState>;
|
|
57
122
|
private assertOpen;
|
|
58
123
|
getMessages(threadId: string, options?: {
|
|
59
124
|
limit?: number;
|
|
@@ -61,25 +126,74 @@ export declare class ChatSession<T extends StoredMessage> {
|
|
|
61
126
|
timestamp: number;
|
|
62
127
|
id: string;
|
|
63
128
|
};
|
|
129
|
+
earlier?: boolean;
|
|
64
130
|
all?: boolean;
|
|
65
131
|
refresh?: boolean;
|
|
66
132
|
}): Promise<MessageSnapshot<T>>;
|
|
133
|
+
private confirmMessages;
|
|
134
|
+
private rememberHistoryEdge;
|
|
135
|
+
private readEarlier;
|
|
67
136
|
private readAll;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
137
|
+
/** Reads trusted local group metadata immediately; SDK refreshes it in the background. */
|
|
138
|
+
getGroup(thread: ChatConversation | string, options?: {
|
|
139
|
+
refresh?: boolean;
|
|
140
|
+
}): Promise<ChatGroup>;
|
|
141
|
+
createGroup(thread: ChatConversation, content: {
|
|
142
|
+
title: string;
|
|
143
|
+
members: readonly string[];
|
|
144
|
+
}): Promise<ChatGroup>;
|
|
145
|
+
updateGroup(target: ChatConversation | string, changes: {
|
|
146
|
+
expectedRevision?: string;
|
|
147
|
+
addMembers?: readonly string[];
|
|
148
|
+
removeMembers?: readonly string[];
|
|
149
|
+
title?: string;
|
|
150
|
+
avatar?: string;
|
|
151
|
+
}): Promise<ChatGroup>;
|
|
152
|
+
sendMessage(threadId: string, input: OutgoingContent, options?: {
|
|
153
|
+
waitFor?: "accepted" | "delivered";
|
|
154
|
+
}): Promise<ChatRuntimeMessage>;
|
|
155
|
+
sendMessage(thread: ChatConversation, input: OutgoingContent, options?: {
|
|
156
|
+
waitFor?: "accepted" | "delivered";
|
|
157
|
+
}): Promise<ChatRuntimeMessage>;
|
|
158
|
+
sendMessage(threadId: string, input: SendMessageInput, options?: {
|
|
159
|
+
waitFor?: "accepted" | "delivered";
|
|
72
160
|
}): Promise<ChatRuntimeMessage>;
|
|
161
|
+
private sendWire;
|
|
162
|
+
getConversation(target: ConversationTarget): Promise<string>;
|
|
163
|
+
private sendContent;
|
|
164
|
+
private resolveConversation;
|
|
165
|
+
/** Validated target identity survives content redaction; no roster is exposed. */
|
|
166
|
+
getConversationTarget(threadId: string): ChatConversation | undefined;
|
|
167
|
+
private sendControl;
|
|
73
168
|
getReaders(threadId: string, messageId: string, page?: Omit<MessageReadersQuery, "threadId" | "messageId">, signal?: AbortSignal): Promise<import("./messageReaders.js").MessageReadersPage>;
|
|
169
|
+
/** Resolve voice media uniformly; pending local bytes are loaded only on demand. */
|
|
170
|
+
getVoiceAttachment(threadId: string, messageId: string): Promise<VoiceAttachmentLease>;
|
|
74
171
|
retryMessage(messageId: string): Promise<ChatRuntimeMessage>;
|
|
75
|
-
observeMessages(threadId: string, ids: string[], event: "visible" | "played"):
|
|
172
|
+
observeMessages(threadId: string, ids: string[], event: "visible" | "played"): ReturnType<ReadingClient["recordVisible"]>;
|
|
173
|
+
observeMessages(threadId: string | undefined, ids: string[], event: "receipts", clientInstanceId?: string): Promise<void>;
|
|
174
|
+
/** Bridge transports can delegate social events without maintaining clients. */
|
|
175
|
+
handleReach(event: OnlineReachEvent): Promise<"handled" | undefined>;
|
|
176
|
+
private wakeCounts;
|
|
177
|
+
private publishCountSnapshot;
|
|
178
|
+
private publishCounts;
|
|
179
|
+
private watchCounts;
|
|
76
180
|
getSnapshot(): ChatSummarySnapshot<T>;
|
|
77
181
|
getSnapshot(threadId: string): ChatThreadSnapshot<T>;
|
|
78
|
-
|
|
79
|
-
|
|
182
|
+
private buildThreadSnapshot;
|
|
183
|
+
/** Delivers the current stable snapshot immediately, then only meaningful changes. */
|
|
184
|
+
subscribe(listener: (snapshot: ChatSummarySnapshot<T>) => void): () => void;
|
|
185
|
+
subscribe(threadId: string, listener: (snapshot: ChatThreadSnapshot<T>) => void, fields?: readonly (keyof ChatThreadSnapshot<T>)[]): () => void;
|
|
186
|
+
/** Change-only bridge for useSyncExternalStore and other framework adapters. */
|
|
187
|
+
subscribeChanges(listener: () => void): () => void;
|
|
188
|
+
subscribeChanges(threadId: string, listener: () => void, fields?: readonly (keyof ChatThreadSnapshot<T>)[]): () => void;
|
|
80
189
|
private publishAll;
|
|
81
190
|
private buildGlobal;
|
|
82
191
|
private publishGlobal;
|
|
83
192
|
private publish;
|
|
193
|
+
/** Permanently retire this identity/team session. UI withdrawal is immediate;
|
|
194
|
+
* completion means all durable stores have crossed the same authority epoch. */
|
|
195
|
+
revokeAccess(): Promise<void>;
|
|
196
|
+
get active(): boolean;
|
|
84
197
|
close(): Promise<void>;
|
|
198
|
+
private dispose;
|
|
85
199
|
}
|