@pmate/chat-sdk 0.12.0-next.8 → 0.12.0-next.81

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 (111) hide show
  1. package/README.md +170 -83
  2. package/lib/advanced.d.ts +10 -0
  3. package/lib/advanced.js +10 -0
  4. package/lib/chat.d.ts +5 -0
  5. package/lib/chat.js +11 -0
  6. package/lib/chatContract.d.ts +151 -0
  7. package/lib/chatContract.js +1 -0
  8. package/lib/chatError.d.ts +14 -0
  9. package/lib/chatError.js +16 -0
  10. package/lib/chatTypes.d.ts +127 -0
  11. package/lib/chatTypes.js +1 -0
  12. package/lib/commands.d.ts +20 -0
  13. package/lib/commands.js +12 -0
  14. package/lib/components/ChatInputDesktop.js +5 -4
  15. package/lib/components/ChatInputMobile.js +5 -4
  16. package/lib/components/CommandMenu.d.ts +15 -0
  17. package/lib/components/CommandMenu.js +10 -0
  18. package/lib/credentials.d.ts +26 -0
  19. package/lib/credentials.js +110 -0
  20. package/lib/fileMetadata.d.ts +9 -0
  21. package/lib/fileMetadata.js +18 -0
  22. package/lib/framework.d.ts +11 -0
  23. package/lib/framework.js +12 -0
  24. package/lib/history.d.ts +4 -0
  25. package/lib/history.js +8 -0
  26. package/lib/hooks/useChatSnapshot.d.ts +4 -0
  27. package/lib/hooks/useChatSnapshot.js +9 -0
  28. package/lib/hooks/useComposerSuggestions.d.ts +50 -0
  29. package/lib/hooks/useComposerSuggestions.js +70 -0
  30. package/lib/index.d.ts +5 -0
  31. package/lib/index.js +5 -0
  32. package/lib/internal/chatEngine.d.ts +130 -0
  33. package/lib/internal/chatEngine.js +284 -0
  34. package/lib/internal/chatRegistry.d.ts +4 -0
  35. package/lib/internal/chatRegistry.js +4 -0
  36. package/lib/internal/createPublicChat.d.ts +4 -0
  37. package/lib/internal/createPublicChat.js +52 -0
  38. package/lib/messageView.d.ts +6 -0
  39. package/lib/messageView.js +103 -0
  40. package/lib/runtime/batchedReadIndex.d.ts +16 -3
  41. package/lib/runtime/batchedReadIndex.js +50 -4
  42. package/lib/runtime/cachedMessage.d.ts +12 -0
  43. package/lib/runtime/cachedMessage.js +18 -0
  44. package/lib/runtime/chatClient.d.ts +7 -0
  45. package/lib/runtime/chatClient.js +14 -3
  46. package/lib/runtime/chatError.d.ts +5 -0
  47. package/lib/runtime/chatError.js +20 -0
  48. package/lib/runtime/chatSession.d.ts +128 -13
  49. package/lib/runtime/chatSession.js +759 -48
  50. package/lib/runtime/connectivity.d.ts +4 -0
  51. package/lib/runtime/connectivity.js +8 -0
  52. package/lib/runtime/conversationDirectory.d.ts +37 -0
  53. package/lib/runtime/conversationDirectory.js +116 -0
  54. package/lib/runtime/conversationRead.d.ts +13 -0
  55. package/lib/runtime/conversationRead.js +34 -0
  56. package/lib/runtime/conversationReading.d.ts +28 -0
  57. package/lib/runtime/conversationReading.js +47 -0
  58. package/lib/runtime/createChat.d.ts +11 -7
  59. package/lib/runtime/createChat.js +46 -23
  60. package/lib/runtime/durableSender.d.ts +97 -14
  61. package/lib/runtime/durableSender.js +786 -71
  62. package/lib/runtime/groupClient.d.ts +91 -0
  63. package/lib/runtime/groupClient.js +380 -0
  64. package/lib/runtime/groupDenial.d.ts +2 -0
  65. package/lib/runtime/groupDenial.js +4 -0
  66. package/lib/runtime/index.d.ts +7 -0
  67. package/lib/runtime/index.js +5 -0
  68. package/lib/runtime/indexedDbRecovery.d.ts +4 -0
  69. package/lib/runtime/indexedDbRecovery.js +27 -0
  70. package/lib/runtime/messageClient.d.ts +35 -8
  71. package/lib/runtime/messageClient.js +364 -66
  72. package/lib/runtime/messageControls.d.ts +19 -0
  73. package/lib/runtime/messageControls.js +44 -0
  74. package/lib/runtime/messageLaunch.d.ts +44 -0
  75. package/lib/runtime/messageLaunch.js +137 -0
  76. package/lib/runtime/messageQuotes.d.ts +3 -0
  77. package/lib/runtime/messageQuotes.js +17 -0
  78. package/lib/runtime/messageReaders.d.ts +5 -1
  79. package/lib/runtime/messageReaders.js +16 -5
  80. package/lib/runtime/messageStore.d.ts +43 -2
  81. package/lib/runtime/messageStore.js +248 -14
  82. package/lib/runtime/messageTimeline.d.ts +50 -0
  83. package/lib/runtime/messageTimeline.js +120 -0
  84. package/lib/runtime/outboxSchema.d.ts +5 -0
  85. package/lib/runtime/outboxSchema.js +59 -0
  86. package/lib/runtime/outgoingContent.d.ts +94 -0
  87. package/lib/runtime/outgoingContent.js +118 -0
  88. package/lib/runtime/readBatchJournal.d.ts +35 -0
  89. package/lib/runtime/readBatchJournal.js +210 -0
  90. package/lib/runtime/readCounts.d.ts +15 -1
  91. package/lib/runtime/readCounts.js +97 -9
  92. package/lib/runtime/readIndex.d.ts +68 -7
  93. package/lib/runtime/readIndex.js +460 -22
  94. package/lib/runtime/readSchema.d.ts +2 -0
  95. package/lib/runtime/readSchema.js +58 -0
  96. package/lib/runtime/readSync.d.ts +1 -0
  97. package/lib/runtime/readSync.js +26 -9
  98. package/lib/runtime/readingClient.d.ts +19 -0
  99. package/lib/runtime/readingClient.js +131 -19
  100. package/lib/runtime/readingContent.d.ts +5 -0
  101. package/lib/runtime/readingContent.js +23 -0
  102. package/lib/runtime/responseCache.d.ts +40 -5
  103. package/lib/runtime/responseCache.js +182 -44
  104. package/lib/runtime/socialReadClient.d.ts +9 -4
  105. package/lib/runtime/socialReadClient.js +12 -8
  106. package/lib/runtime/voiceUpload.d.ts +2 -0
  107. package/lib/runtime/voiceUpload.js +27 -0
  108. package/lib/snapshot.d.ts +36 -0
  109. package/lib/snapshot.js +52 -0
  110. package/lib/types/chatInput.d.ts +3 -0
  111. package/package.json +32 -2
@@ -0,0 +1,94 @@
1
+ import { type ChatFile } from "../fileMetadata.js";
2
+ import type { ChatMention } from "../mentions/model.js";
3
+ import type { MessageScope } from "./messageStore.js";
4
+ import type { SendMessageInput } from "./durableSender.js";
5
+ export type ChatConversation = {
6
+ id: string;
7
+ kind: "dm" | "group";
8
+ targetId: string;
9
+ teamId?: string;
10
+ memberIds?: readonly string[];
11
+ };
12
+ export type MessageQuote = {
13
+ messageId?: string;
14
+ senderName: string;
15
+ content: string;
16
+ };
17
+ type ContentChoices = {
18
+ text: {
19
+ text: string;
20
+ mentions?: ChatMention[];
21
+ quote?: MessageQuote;
22
+ };
23
+ file: {
24
+ file: ChatFile;
25
+ };
26
+ image: {
27
+ image: {
28
+ url: string;
29
+ name?: string;
30
+ };
31
+ };
32
+ voice: {
33
+ voice: ({
34
+ blob: Blob;
35
+ url?: never;
36
+ } | {
37
+ url: string;
38
+ blob?: never;
39
+ }) & {
40
+ durationMs: number;
41
+ transcript?: string;
42
+ };
43
+ };
44
+ recall: {
45
+ recall: {
46
+ messageId: string;
47
+ };
48
+ };
49
+ reaction: {
50
+ reaction: {
51
+ messageId: string;
52
+ emojiId: string;
53
+ active: boolean;
54
+ };
55
+ };
56
+ invitation: {
57
+ invitation: {
58
+ message?: string;
59
+ };
60
+ };
61
+ groupCreate: {
62
+ groupCreate: {
63
+ title: string;
64
+ members: readonly string[];
65
+ };
66
+ };
67
+ groupUpdate: {
68
+ groupUpdate: {
69
+ expectedRevision: string;
70
+ addMembers?: readonly string[];
71
+ removeMembers?: readonly string[];
72
+ title?: string;
73
+ avatar?: string;
74
+ };
75
+ };
76
+ groupDissolve: {
77
+ groupDissolve: {
78
+ expectedRevision: string;
79
+ };
80
+ };
81
+ };
82
+ export type OutgoingContent = {
83
+ [K in keyof ContentChoices]: ContentChoices[K] & {
84
+ [P in Exclude<keyof ContentChoices, K>]?: never;
85
+ };
86
+ }[keyof ContentChoices];
87
+ export declare function validMessageIdentifier(value: unknown): value is string;
88
+ export declare function validateConversation(scope: MessageScope, thread: ChatConversation): void;
89
+ export declare const ALL_MENTION_ID = "__pmate_mention_all__";
90
+ /** Expand group mentions into concrete members understood by all protocol readers. */
91
+ export declare function expandGroupMentions(text: string, mentions: ChatMention[] | undefined, memberIds: readonly string[], senderId: string): ChatMention[];
92
+ /** Build the existing wire protocol inside the owner, before durable acceptance. */
93
+ export declare function outgoingContent(scope: MessageScope, thread: ChatConversation, input: OutgoingContent): SendMessageInput;
94
+ export {};
@@ -0,0 +1,118 @@
1
+ import { parseChatFile } from "../fileMetadata.js";
2
+ import { dmThreadId, groupThreadId } from "./thread.js";
3
+ const contentKeys = ["text", "file", "image", "voice", "recall", "reaction", "invitation", "groupCreate", "groupUpdate", "groupDissolve"];
4
+ export function validMessageIdentifier(value) { return typeof value === "string" && value.length > 0 && new TextEncoder().encode(value).byteLength <= 256 && !/[\u0000-\u001f]/u.test(value); }
5
+ function members(value) {
6
+ if (!Array.isArray(value) || value.length > 256 || !value.every(validMessageIdentifier))
7
+ throw new Error("INVALID_GROUP_MEMBERS");
8
+ return [...new Set(value)].sort();
9
+ }
10
+ export function validateConversation(scope, thread) {
11
+ if (!thread || !["dm", "group"].includes(thread.kind) || !validMessageIdentifier(thread.targetId) || thread.teamId !== undefined && thread.teamId !== scope.teamId)
12
+ throw new Error("INVALID_MESSAGE_CONVERSATION");
13
+ const expected = thread.kind === "group" ? groupThreadId(scope.appId, thread.targetId, scope.teamId) : dmThreadId(scope.appId, scope.profileId, thread.targetId, scope.teamId);
14
+ if (thread.id !== expected)
15
+ throw new Error("MESSAGE_CONVERSATION_MISMATCH");
16
+ }
17
+ export const ALL_MENTION_ID = "__pmate_mention_all__";
18
+ /** Expand group mentions into concrete members understood by all protocol readers. */
19
+ export function expandGroupMentions(text, mentions, memberIds, senderId) {
20
+ const validMembers = [...new Set(memberIds)], members = validMembers.filter(id => id !== senderId), entries = [...(mentions ?? [])];
21
+ const pattern = /(?:^|[\s([{,。!?、:;])(@(?:all|所有人))(?=$|[\s,.;:!?,。!?、:;)\]}])/gu;
22
+ for (const match of text.matchAll(pattern)) {
23
+ const start = match.index + match[0].length - match[1].length;
24
+ if (!entries.some(entry => entry.start === start && entry.end === start + match[1].length))
25
+ entries.push({ id: ALL_MENTION_ID, name: match[1].slice(1), start, end: start + match[1].length });
26
+ }
27
+ const seen = new Set();
28
+ return entries.flatMap(entry => {
29
+ if (text.slice(entry.start, entry.end) !== `@${entry.name}`)
30
+ return [];
31
+ if (entry.id === ALL_MENTION_ID)
32
+ return ["all", "所有人"].includes(entry.name) ? members.map(id => ({ ...entry, id })) : [];
33
+ return validMembers.includes(entry.id) ? [entry] : [];
34
+ }).filter(entry => { const key = `${entry.id}:${entry.start}:${entry.end}`; if (seen.has(key))
35
+ return false; seen.add(key); return true; });
36
+ }
37
+ /** Build the existing wire protocol inside the owner, before durable acceptance. */
38
+ export function outgoingContent(scope, thread, input) {
39
+ validateConversation(scope, thread);
40
+ if (!input || typeof input !== "object" || contentKeys.filter(key => Object.hasOwn(input, key)).length !== 1)
41
+ throw new Error("INVALID_MESSAGE_CONTENT");
42
+ const identity = { appId: scope.appId, teamId: scope.teamId };
43
+ if (input.invitation) {
44
+ if (typeof input.invitation !== "object" || Array.isArray(input.invitation) || thread.kind !== "dm" || input.invitation.message !== undefined && typeof input.invitation.message !== "string")
45
+ throw new Error("INVALID_MESSAGE_INVITATION");
46
+ return { to: thread.targetId, body: { kind: "contact", threadKind: "dm", action: "friendRequest", payload: { ...identity, message: input.invitation.message ?? "" } } };
47
+ }
48
+ if (input.groupCreate) {
49
+ if (thread.kind !== "group" || typeof input.groupCreate.title !== "string")
50
+ throw new Error("INVALID_GROUP_CREATE");
51
+ const recipients = members(input.groupCreate.members);
52
+ if (new Set([scope.profileId, ...recipients]).size > 256)
53
+ throw new Error("INVALID_GROUP_MEMBERS");
54
+ return { to: thread.targetId, recipients, body: { kind: "group", threadKind: "group", action: "groupCreate", payload: { ...identity, groupId: thread.targetId, title: input.groupCreate.title, members: recipients } } };
55
+ }
56
+ if (input.groupUpdate) {
57
+ const update = input.groupUpdate, add = members(update.addMembers ?? []), remove = members(update.removeMembers ?? []);
58
+ if (thread.kind !== "group" || !validMessageIdentifier(update.expectedRevision) || Number(add.length > 0) + Number(remove.length > 0) + Number(update.title !== undefined) + Number(update.avatar !== undefined) !== 1)
59
+ throw new Error("INVALID_GROUP_UPDATE");
60
+ if (update.title !== undefined && (typeof update.title !== "string" || !update.title.trim() || [...update.title.trim()].length > 120))
61
+ throw new Error("INVALID_GROUP_TITLE");
62
+ if (update.avatar !== undefined && (typeof update.avatar !== "string" || !update.avatar.startsWith("https://") || update.avatar.length > 2048 || /\s/u.test(update.avatar)))
63
+ throw new Error("INVALID_GROUP_AVATAR");
64
+ return { to: thread.targetId, body: { kind: "group", threadKind: "group", action: "groupUpdate", payload: { ...identity, groupId: thread.targetId, expectedRevision: update.expectedRevision, addMembers: add, removeMembers: remove, ...(update.title === undefined ? {} : { title: update.title.trim() }), ...(update.avatar === undefined ? {} : { avatar: update.avatar }) } } };
65
+ }
66
+ if (input.groupDissolve) {
67
+ if (thread.kind !== "group" || !validMessageIdentifier(input.groupDissolve.expectedRevision))
68
+ throw new Error("INVALID_GROUP_DISSOLVE");
69
+ return { to: thread.targetId, body: { kind: "group", threadKind: "group", action: "groupDissolve", payload: { ...identity, groupId: thread.targetId, expectedRevision: input.groupDissolve.expectedRevision } } };
70
+ }
71
+ if (thread.kind === "group" && !Array.isArray(thread.memberIds))
72
+ throw new Error("MESSAGE_GROUP_MEMBERS_UNAVAILABLE");
73
+ const recipients = thread.kind === "group" ? [...new Set(thread.memberIds)] : undefined;
74
+ if (recipients && (!recipients.length || recipients.length > 256 || !recipients.every(validMessageIdentifier)))
75
+ throw new Error("MESSAGE_GROUP_MEMBERS_UNAVAILABLE");
76
+ const base = { ...identity, ...(thread.kind === "group" ? { groupId: thread.targetId } : {}) };
77
+ if (input.recall) {
78
+ if (!validMessageIdentifier(input.recall.messageId))
79
+ throw new Error("INVALID_MESSAGE_RECALL");
80
+ return { to: thread.targetId, ...(recipients ? { recipients } : {}), body: { kind: "message", threadKind: thread.kind, contentKind: "text", content: { ...identity, text: "[消息已撤回]", recall: { messageId: input.recall.messageId } } } };
81
+ }
82
+ if (input.reaction) {
83
+ const reaction = input.reaction;
84
+ if (!validMessageIdentifier(reaction.messageId) || !validMessageIdentifier(reaction.emojiId) || typeof reaction.active !== "boolean")
85
+ throw new Error("INVALID_MESSAGE_REACTION");
86
+ // The durable sender assigns the revision in the acceptance transaction.
87
+ return { to: thread.targetId, ...(recipients ? { recipients } : {}), body: { kind: "message", threadKind: thread.kind, contentKind: "text", content: { ...identity, text: "[表情回应]", reaction: { version: 1, messageId: reaction.messageId, emojiId: reaction.emojiId, active: reaction.active, revision: 1 } } } };
88
+ }
89
+ if (input.file) {
90
+ const file = parseChatFile(input.file);
91
+ if (!file)
92
+ throw new Error("INVALID_MESSAGE_FILE");
93
+ return { to: thread.targetId, ...(recipients ? { recipients } : {}), body: { kind: "message", threadKind: thread.kind, contentKind: "text", content: { ...base, text: `[文件] ${file.name}\n${file.url}`, file } } };
94
+ }
95
+ let content, contentKind = "text", voiceBlob;
96
+ if ("text" in input) {
97
+ if (typeof input.text !== "string")
98
+ throw new Error("INVALID_MESSAGE_TEXT");
99
+ const mentions = thread.kind === "group" ? expandGroupMentions(input.text, input.mentions, recipients, scope.profileId) : input.mentions;
100
+ content = { ...base, text: input.text, ...(input.quote ? { quote: input.quote } : {}), ...(mentions?.length ? { mentions } : {}) };
101
+ }
102
+ else if ("image" in input && input.image) {
103
+ if (typeof input.image.url !== "string" || !input.image.url)
104
+ throw new Error("INVALID_MESSAGE_IMAGE");
105
+ contentKind = "image";
106
+ content = { ...base, url: input.image.url, ...(input.image.name === undefined ? {} : { name: input.image.name }) };
107
+ }
108
+ else if ("voice" in input && input.voice) {
109
+ const voice = input.voice;
110
+ if (!Number.isFinite(voice.durationMs) || voice.durationMs <= 0 || !("blob" in voice ? voice.blob instanceof Blob : typeof voice.url === "string" && voice.url.length > 0))
111
+ throw new Error("INVALID_MESSAGE_VOICE");
112
+ voiceBlob = voice.blob;
113
+ content = { ...base, text: "[语音]", voice: { url: voiceBlob ? "" : voice.url, durationMs: voice.durationMs, ...(voice.transcript ? { transcript: voice.transcript } : {}) } };
114
+ }
115
+ else
116
+ throw new Error("INVALID_MESSAGE_CONTENT");
117
+ return { to: thread.targetId, body: { kind: "message", threadKind: thread.kind, contentKind, content }, ...(recipients ? { recipients } : {}), ...(voiceBlob ? { voiceBlob } : {}) };
118
+ }
@@ -0,0 +1,35 @@
1
+ import { Dexie } from "dexie";
2
+ export type ReadMutation = {
3
+ table: "readingThreads" | "readingMessages" | "readingPositions";
4
+ key: string[];
5
+ value?: unknown;
6
+ };
7
+ type Plan = {
8
+ profileKey: string[];
9
+ header: unknown;
10
+ versions?: unknown;
11
+ };
12
+ /** Cross-document serialization; the persisted journal is the crash boundary. */
13
+ export declare function lockReadOwner<T>(factory: IDBFactory, name: string, owner: string, run: () => Promise<T>, fallback?: (run: () => Promise<T>) => Promise<T>): Promise<T>;
14
+ /** Stage the full plan first, roll forward bounded batches, publish cursor last. */
15
+ export declare class ReadBatchJournal {
16
+ private db;
17
+ private closed;
18
+ private rows;
19
+ private knownOwners;
20
+ private leases;
21
+ private acquiring;
22
+ constructor(db: Dexie, closed: () => boolean);
23
+ lease<T>(owner: string, run: () => Promise<T>): Promise<T>;
24
+ releaseLeases(): Promise<void>;
25
+ checkLeases(renew?: boolean): Promise<void>;
26
+ private check;
27
+ private tables;
28
+ recover(owner: string): Promise<void>;
29
+ private cleanup;
30
+ private apply;
31
+ private validatePlan;
32
+ private finish;
33
+ commit(owner: string, operations: Iterable<ReadMutation>, plan: Plan): Promise<void>;
34
+ }
35
+ export {};
@@ -0,0 +1,210 @@
1
+ import { Dexie } from "dexie";
2
+ const LIMIT = 124, BYTES = 1024 * 1024, LEASE_MS = 5_000;
3
+ const size = (value) => JSON.stringify(value).length * 3;
4
+ const localLocks = new WeakMap();
5
+ /** Cross-document serialization; the persisted journal is the crash boundary. */
6
+ export async function lockReadOwner(factory, name, owner, run, fallback) {
7
+ const key = JSON.stringify(["pmate-reading", name, owner]);
8
+ if (typeof navigator !== "undefined" && navigator.locks)
9
+ return navigator.locks.request(key, run);
10
+ if (typeof window !== "undefined") {
11
+ if (!fallback)
12
+ throw new Error("READ_STORAGE_LOCK_UNAVAILABLE");
13
+ return fallback(run);
14
+ }
15
+ let locks = localLocks.get(factory);
16
+ if (!locks) {
17
+ locks = new Map();
18
+ localLocks.set(factory, locks);
19
+ }
20
+ const previous = locks.get(key) ?? Promise.resolve();
21
+ let release;
22
+ const current = new Promise(resolve => { release = resolve; });
23
+ locks.set(key, current);
24
+ await previous;
25
+ try {
26
+ return await run();
27
+ }
28
+ finally {
29
+ release();
30
+ if (locks.get(key) === current)
31
+ locks.delete(key);
32
+ }
33
+ }
34
+ /** Stage the full plan first, roll forward bounded batches, publish cursor last. */
35
+ export class ReadBatchJournal {
36
+ db;
37
+ closed;
38
+ rows;
39
+ knownOwners = new Set();
40
+ leases = new Map();
41
+ acquiring = new Set();
42
+ constructor(db, closed) {
43
+ this.db = db;
44
+ this.closed = closed;
45
+ this.rows = db.table("readBatches");
46
+ }
47
+ async lease(owner, run) {
48
+ const token = crypto.randomUUID();
49
+ for (;;) {
50
+ this.check();
51
+ const attempt = (async () => {
52
+ const acquired = await this.db.transaction("rw", this.rows, async () => {
53
+ const old = (await this.rows.get([owner, "lease"]))?.lease;
54
+ if (old && old.expires > Date.now())
55
+ return false;
56
+ await this.rows.put({ owner, key: "lease", lease: { token, expires: Date.now() + LEASE_MS } });
57
+ return true;
58
+ });
59
+ if (acquired)
60
+ this.leases.set(owner, token);
61
+ return acquired;
62
+ })();
63
+ this.acquiring.add(attempt);
64
+ const acquired = await attempt.finally(() => this.acquiring.delete(attempt));
65
+ if (acquired)
66
+ break;
67
+ await new Promise(resolve => setTimeout(resolve, 25));
68
+ }
69
+ try {
70
+ const result = await run();
71
+ this.check();
72
+ await this.db.transaction("r", this.rows, () => this.checkLeases());
73
+ return result;
74
+ }
75
+ finally {
76
+ this.leases.delete(owner);
77
+ if (!this.closed())
78
+ await this.db.transaction("rw", this.rows, async () => { if ((await this.rows.get([owner, "lease"]))?.lease?.token === token)
79
+ await this.rows.delete([owner, "lease"]); });
80
+ }
81
+ }
82
+ async releaseLeases() {
83
+ await Promise.allSettled([...this.acquiring]);
84
+ for (const [owner, token] of this.leases)
85
+ await this.db.transaction("rw", this.rows, async () => { if ((await this.rows.get([owner, "lease"]))?.lease?.token === token)
86
+ await this.rows.delete([owner, "lease"]); });
87
+ }
88
+ async checkLeases(renew = false) {
89
+ for (const [owner, token] of this.leases) {
90
+ if ((await this.rows.get([owner, "lease"]))?.lease?.token !== token)
91
+ throw new Error("READ_STORAGE_LOCK_LOST");
92
+ if (renew)
93
+ await this.rows.put({ owner, key: "lease", lease: { token, expires: Date.now() + LEASE_MS } });
94
+ }
95
+ }
96
+ check() { if (this.closed())
97
+ throw new Error("READ_STORAGE_CLOSED"); }
98
+ tables() { return [this.rows, ...["readingProfiles", "readingVersions", "readingThreads", "readingMessages", "readingPositions"].map(name => this.db.table(name))]; }
99
+ async recover(owner) {
100
+ this.check();
101
+ const active = (await this.rows.get([owner, "active"]))?.active;
102
+ if (!active) {
103
+ if (!this.knownOwners.has(owner)) {
104
+ await this.cleanup(owner);
105
+ this.knownOwners.add(owner);
106
+ }
107
+ return;
108
+ }
109
+ const plan = (await this.rows.get([owner, `${active.id}:plan`]))?.plan;
110
+ if (!plan || typeof active.id !== "string" || !Number.isSafeInteger(active.next) || !Number.isSafeInteger(active.total) || active.next < 0 || active.total < 1 || active.next > active.total)
111
+ throw new Error("READ_STORAGE_INCOMPLETE");
112
+ this.validatePlan(owner, plan);
113
+ while (active.next < active.total) {
114
+ this.check();
115
+ await this.db.transaction("rw", this.tables(), async () => {
116
+ await this.checkLeases();
117
+ const chunk = await this.rows.get([owner, `${active.id}:${active.next}`]);
118
+ if (!chunk?.operations || chunk.operations.length > LIMIT || chunk.operations.some(operation => !["readingThreads", "readingMessages", "readingPositions"].includes(operation.table) || operation.key[0] !== owner || operation.value !== undefined && operation.value.owner !== owner))
119
+ throw new Error("READ_STORAGE_INCOMPLETE");
120
+ await this.apply(chunk.operations);
121
+ await this.rows.put({ owner, key: "active", active: { ...active, next: active.next + 1 } });
122
+ await this.checkLeases(true);
123
+ });
124
+ active.next++;
125
+ await new Promise(resolve => setTimeout(resolve, 0));
126
+ }
127
+ this.check();
128
+ await this.db.transaction("rw", this.tables(), async () => { await this.checkLeases(); await this.finish(plan); await this.rows.delete([owner, "active"]); await this.checkLeases(true); });
129
+ await this.cleanup(owner);
130
+ this.knownOwners.add(owner);
131
+ }
132
+ async cleanup(owner) {
133
+ for (;;) {
134
+ this.check();
135
+ const keys = await this.rows.where("owner").equals(owner).limit(LIMIT).primaryKeys();
136
+ const removable = keys.filter(key => key[1] !== "lease");
137
+ if (removable.length)
138
+ await this.db.transaction("rw", this.rows, async () => { await this.checkLeases(); await this.rows.bulkDelete(removable); await this.checkLeases(true); });
139
+ const count = keys.length;
140
+ if (count < LIMIT)
141
+ return;
142
+ await new Promise(resolve => setTimeout(resolve, 0));
143
+ }
144
+ }
145
+ async apply(operations) {
146
+ for (const name of ["readingThreads", "readingMessages", "readingPositions"]) {
147
+ const selected = operations.filter(operation => operation.table === name);
148
+ const writes = selected.filter(operation => operation.value !== undefined).map(operation => operation.value);
149
+ const deletes = selected.filter(operation => operation.value === undefined).map(operation => operation.key);
150
+ if (writes.length)
151
+ await this.db.table(name).bulkPut(writes);
152
+ if (deletes.length)
153
+ await this.db.table(name).bulkDelete(deletes);
154
+ }
155
+ }
156
+ validatePlan(owner, plan) {
157
+ const header = plan.header;
158
+ if (!Array.isArray(plan.profileKey) || JSON.stringify(plan.profileKey) !== owner || !header || header.schemaVersion !== 1 || header.appId !== plan.profileKey[0] || header.profileId !== plan.profileKey[1])
159
+ throw new Error("READ_IDENTITY_MISMATCH");
160
+ }
161
+ async finish(plan) {
162
+ if (plan.versions !== undefined)
163
+ await this.db.table("readingVersions").put(plan.versions, plan.profileKey);
164
+ await this.db.table("readingProfiles").put(plan.header, plan.profileKey);
165
+ }
166
+ async commit(owner, operations, plan) {
167
+ this.check();
168
+ this.validatePlan(owner, plan);
169
+ this.knownOwners.delete(owner);
170
+ await this.cleanup(owner);
171
+ if (size(plan) > BYTES)
172
+ throw new Error("READ_STORAGE_RECORD_TOO_LARGE");
173
+ const id = crypto.randomUUID(), iterator = operations[Symbol.iterator]();
174
+ let next = iterator.next(), total = 0;
175
+ while (!next.done) {
176
+ const batch = [];
177
+ let bytes = 512;
178
+ while (!next.done && batch.length < LIMIT) {
179
+ const weight = size(next.value);
180
+ if (weight > BYTES - 512)
181
+ throw new Error("READ_STORAGE_RECORD_TOO_LARGE");
182
+ if (batch.length && bytes + weight > BYTES)
183
+ break;
184
+ bytes += weight;
185
+ batch.push(next.value);
186
+ next = iterator.next();
187
+ }
188
+ if (total === 0 && next.done) {
189
+ await this.db.transaction("rw", this.tables(), async () => { await this.checkLeases(); await this.apply(batch); await this.finish(plan); await this.checkLeases(true); });
190
+ this.knownOwners.add(owner);
191
+ return;
192
+ }
193
+ this.check();
194
+ await this.db.transaction("rw", this.rows, async () => { await this.checkLeases(); await this.rows.put({ owner, key: `${id}:${total++}`, operations: batch }); await this.checkLeases(true); });
195
+ await new Promise(resolve => setTimeout(resolve, 0));
196
+ }
197
+ if (!total) {
198
+ await this.db.transaction("rw", this.tables(), async () => { await this.checkLeases(); await this.finish(plan); await this.checkLeases(true); });
199
+ this.knownOwners.add(owner);
200
+ return;
201
+ }
202
+ await this.db.transaction("rw", this.rows, async () => {
203
+ await this.checkLeases();
204
+ await this.rows.put({ owner, key: `${id}:plan`, plan });
205
+ await this.rows.put({ owner, key: "active", active: { id, next: 0, total } });
206
+ await this.checkLeases(true);
207
+ });
208
+ await this.recover(owner);
209
+ }
210
+ }
@@ -1,3 +1,5 @@
1
+ import { type ResponseCacheAccess } from "./responseCache.js";
2
+ import { type MessageScope } from "./messageStore.js";
1
3
  export type ReadCountsResult = {
2
4
  appId: string;
3
5
  threadId: string;
@@ -11,6 +13,8 @@ export type ReadCountsOptions = {
11
13
  clientInstanceId: string;
12
14
  hubHttpUrl: string;
13
15
  fetch?: typeof fetch;
16
+ scope?: MessageScope;
17
+ access?: ResponseCacheAccess;
14
18
  onChange: (value: ReadCountsResult) => void;
15
19
  onError?: (error: unknown) => void;
16
20
  };
@@ -25,13 +29,22 @@ export declare class ReadCountsClient {
25
29
  private generation;
26
30
  private retries;
27
31
  private closed;
32
+ private closing?;
28
33
  private revision;
29
34
  private running;
30
35
  private dirty;
31
36
  private subscribed;
32
37
  private subscriptionDenied;
33
38
  private subscriptionPending?;
39
+ private cache?;
40
+ private lastResult?;
41
+ private publish;
42
+ private revoked;
43
+ private accessVersion;
44
+ private requestedRevision;
45
+ private force;
34
46
  constructor(options: ReadCountsOptions);
47
+ private denyCache;
35
48
  private post;
36
49
  private subscription;
37
50
  private renew;
@@ -40,5 +53,6 @@ export declare class ReadCountsClient {
40
53
  reconnect(): void;
41
54
  private schedule;
42
55
  private refresh;
43
- close(): void;
56
+ retireScope(version: number): Promise<void>;
57
+ close(): Promise<void>;
44
58
  }