@pmate/chat-sdk 0.12.0-next.6 → 0.12.0-next.61
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 +98 -3
- 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 +1 -0
- package/lib/runtime/chatClient.js +1 -1
- package/lib/runtime/chatSession.d.ts +109 -12
- package/lib/runtime/chatSession.js +624 -40
- package/lib/runtime/connectivity.d.ts +4 -0
- package/lib/runtime/connectivity.js +8 -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 +5 -5
- package/lib/runtime/createChat.js +30 -17
- 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 +6 -0
- package/lib/runtime/index.js +5 -0
- package/lib/runtime/messageClient.d.ts +30 -8
- package/lib/runtime/messageClient.js +332 -66
- package/lib/runtime/messageControls.d.ts +19 -0
- package/lib/runtime/messageControls.js +44 -0
- package/lib/runtime/messageLaunch.d.ts +42 -0
- package/lib/runtime/messageLaunch.js +133 -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 +38 -2
- package/lib/runtime/messageStore.js +215 -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 +85 -0
- package/lib/runtime/outgoingContent.js +106 -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 +96 -8
- 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 +25 -8
- 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/recordCache.d.ts +9 -0
- package/lib/runtime/recordCache.js +62 -5
- package/lib/runtime/responseCache.d.ts +57 -0
- package/lib/runtime/responseCache.js +230 -0
- package/lib/runtime/socialReadClient.d.ts +3 -1
- package/lib/runtime/socialReadClient.js +8 -4
- package/lib/runtime/voiceUpload.d.ts +2 -0
- package/lib/runtime/voiceUpload.js +27 -0
- package/package.json +6 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type ResponseCacheAccess } from "./responseCache.js";
|
|
2
|
+
import { type MessageScope } from "./messageStore.js";
|
|
3
|
+
import { type ChatConversation } from "./outgoingContent.js";
|
|
4
|
+
import type { SocialGroup } from "./socialReadClient.js";
|
|
5
|
+
export type ChatGroup = {
|
|
6
|
+
groupId: string;
|
|
7
|
+
teamId: string;
|
|
8
|
+
ownerId: string;
|
|
9
|
+
title: string;
|
|
10
|
+
avatar?: string;
|
|
11
|
+
memberIds: readonly string[];
|
|
12
|
+
revision: string;
|
|
13
|
+
versionNumber: number;
|
|
14
|
+
};
|
|
15
|
+
export type GroupSnapshot = {
|
|
16
|
+
data?: ChatGroup;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
error?: unknown;
|
|
19
|
+
};
|
|
20
|
+
export type GroupClientOptions = {
|
|
21
|
+
scope: MessageScope;
|
|
22
|
+
access: ResponseCacheAccess;
|
|
23
|
+
load: (groupId: string, signal: AbortSignal) => Promise<SocialGroup>;
|
|
24
|
+
onError: (error: unknown) => void;
|
|
25
|
+
onDenied?: (error: unknown) => Promise<void> | void;
|
|
26
|
+
indexedDB?: IDBFactory;
|
|
27
|
+
name?: string;
|
|
28
|
+
};
|
|
29
|
+
/** SDK-owned roster cache. Read sources, concurrency and reconciliation stay private. */
|
|
30
|
+
export declare class GroupClient {
|
|
31
|
+
private options;
|
|
32
|
+
private cache;
|
|
33
|
+
private entries;
|
|
34
|
+
private pins;
|
|
35
|
+
private listeners;
|
|
36
|
+
private localQueue;
|
|
37
|
+
private localScheduled;
|
|
38
|
+
private localRunning;
|
|
39
|
+
private tasks;
|
|
40
|
+
private authority?;
|
|
41
|
+
private version?;
|
|
42
|
+
private clock;
|
|
43
|
+
private active;
|
|
44
|
+
private queue;
|
|
45
|
+
private controllers;
|
|
46
|
+
private channel?;
|
|
47
|
+
private withdrawn;
|
|
48
|
+
private closed;
|
|
49
|
+
private closing?;
|
|
50
|
+
constructor(options: GroupClientOptions);
|
|
51
|
+
private assertOpen;
|
|
52
|
+
private checkAuthority;
|
|
53
|
+
private entry;
|
|
54
|
+
snapshot(threadId: string): GroupSnapshot;
|
|
55
|
+
subscribe(listener: (threadId: string) => void): () => void;
|
|
56
|
+
pin(threadId: string): () => void;
|
|
57
|
+
private publish;
|
|
58
|
+
private normalize;
|
|
59
|
+
private cached;
|
|
60
|
+
private local;
|
|
61
|
+
private flushLocal;
|
|
62
|
+
private reload;
|
|
63
|
+
get(thread: ChatConversation, options?: {
|
|
64
|
+
refresh?: boolean;
|
|
65
|
+
}): Promise<ChatGroup>;
|
|
66
|
+
private schedule;
|
|
67
|
+
wake(): void;
|
|
68
|
+
hint(thread: ChatConversation, versionNumber?: number): void;
|
|
69
|
+
private slot;
|
|
70
|
+
private drain;
|
|
71
|
+
private request;
|
|
72
|
+
after(thread: ChatConversation, minimumVersion: number): Promise<ChatGroup>;
|
|
73
|
+
withdraw(): void;
|
|
74
|
+
retire(version: number): Promise<void>;
|
|
75
|
+
close(): Promise<void>;
|
|
76
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { IndexedDbResponseCache } from "./responseCache.js";
|
|
2
|
+
import { WindowedTasks } from "./coalescer.js";
|
|
3
|
+
import { isOffline } from "./connectivity.js";
|
|
4
|
+
import { messageScopeKey } from "./messageStore.js";
|
|
5
|
+
import { validateConversation, validMessageIdentifier } from "./outgoingContent.js";
|
|
6
|
+
const empty = Object.freeze({ loading: false });
|
|
7
|
+
const token = (value) => `${value?.versionNumber}:${value?.revision}`;
|
|
8
|
+
/** SDK-owned roster cache. Read sources, concurrency and reconciliation stay private. */
|
|
9
|
+
export class GroupClient {
|
|
10
|
+
options;
|
|
11
|
+
cache;
|
|
12
|
+
entries = new Map();
|
|
13
|
+
pins = new Map();
|
|
14
|
+
listeners = new Set();
|
|
15
|
+
localQueue = [];
|
|
16
|
+
localScheduled = false;
|
|
17
|
+
localRunning = new Set();
|
|
18
|
+
tasks;
|
|
19
|
+
authority;
|
|
20
|
+
version;
|
|
21
|
+
clock = 0;
|
|
22
|
+
active = 0;
|
|
23
|
+
queue = [];
|
|
24
|
+
controllers = new Set();
|
|
25
|
+
channel;
|
|
26
|
+
withdrawn = false;
|
|
27
|
+
closed = false;
|
|
28
|
+
closing;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
const scope = JSON.stringify(["groups", messageScopeKey(options.scope)]);
|
|
32
|
+
this.options = { ...options, scope: { ...options.scope } };
|
|
33
|
+
this.cache = new IndexedDbResponseCache(scope, { indexedDB: options.indexedDB, name: options.name, access: () => this.checkAuthority(), entries: 64, batchRecords: 32, recordBytes: 131072, revision: token });
|
|
34
|
+
this.tasks = new WindowedTasks(200, error => { if (!this.closed)
|
|
35
|
+
options.onError(error); });
|
|
36
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
37
|
+
this.channel = new BroadcastChannel(`pmate-chat-groups:${scope}:${options.name ?? "default"}`);
|
|
38
|
+
this.channel.onmessage = event => {
|
|
39
|
+
if (event.data?.authority !== this.version || typeof event.data?.threadId !== "string")
|
|
40
|
+
return;
|
|
41
|
+
const entry = this.entries.get(event.data.threadId);
|
|
42
|
+
if (entry)
|
|
43
|
+
void this.reload(entry).catch(error => { if (!this.closed)
|
|
44
|
+
options.onError(error); });
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
assertOpen() { if (this.closed)
|
|
49
|
+
throw new Error("GROUP_CLIENT_CLOSED"); if (this.withdrawn)
|
|
50
|
+
throw new Error("GROUP_ACCESS_REVOKED"); }
|
|
51
|
+
checkAuthority() {
|
|
52
|
+
if (this.withdrawn)
|
|
53
|
+
throw new Error("GROUP_ACCESS_REVOKED");
|
|
54
|
+
if (this.authority)
|
|
55
|
+
return this.authority;
|
|
56
|
+
return this.authority = this.options.access().then(access => {
|
|
57
|
+
if (this.withdrawn)
|
|
58
|
+
throw new Error("GROUP_ACCESS_REVOKED");
|
|
59
|
+
if (!access.allowed) {
|
|
60
|
+
this.withdraw();
|
|
61
|
+
throw new Error("GROUP_ACCESS_REVOKED");
|
|
62
|
+
}
|
|
63
|
+
if (this.version !== undefined && this.version !== access.version) {
|
|
64
|
+
this.withdraw();
|
|
65
|
+
throw new Error("GROUP_SCOPE_CHANGED");
|
|
66
|
+
}
|
|
67
|
+
this.version = access.version;
|
|
68
|
+
return access;
|
|
69
|
+
}).finally(() => { this.authority = undefined; });
|
|
70
|
+
}
|
|
71
|
+
entry(thread) {
|
|
72
|
+
this.assertOpen();
|
|
73
|
+
validateConversation(this.options.scope, thread);
|
|
74
|
+
if (thread.kind !== "group")
|
|
75
|
+
throw new Error("INVALID_GROUP_CONVERSATION");
|
|
76
|
+
let entry = this.entries.get(thread.id);
|
|
77
|
+
if (!entry) {
|
|
78
|
+
if (this.entries.size >= 256) {
|
|
79
|
+
const old = [...this.entries.values()].filter(value => !value.local && !value.request && !this.pins.has(value.thread.id)).sort((a, b) => a.used - b.used)[0];
|
|
80
|
+
if (!old)
|
|
81
|
+
throw new Error("GROUP_CACHE_BACKPRESSURE");
|
|
82
|
+
this.entries.delete(old.thread.id);
|
|
83
|
+
}
|
|
84
|
+
entry = { thread: { id: thread.id, kind: "group", targetId: thread.targetId, teamId: thread.teamId }, snapshot: empty, epoch: 0, token: null, loaded: false, checked: false, generation: 0, wanted: 0, attempts: 0, used: 0 };
|
|
85
|
+
this.entries.set(thread.id, entry);
|
|
86
|
+
}
|
|
87
|
+
entry.used = ++this.clock;
|
|
88
|
+
return entry;
|
|
89
|
+
}
|
|
90
|
+
snapshot(threadId) { return this.closed || this.withdrawn ? empty : this.entries.get(threadId)?.snapshot ?? empty; }
|
|
91
|
+
subscribe(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }
|
|
92
|
+
pin(threadId) { this.pins.set(threadId, (this.pins.get(threadId) ?? 0) + 1); return () => { const count = (this.pins.get(threadId) ?? 1) - 1; if (count)
|
|
93
|
+
this.pins.set(threadId, count);
|
|
94
|
+
else
|
|
95
|
+
this.pins.delete(threadId); }; }
|
|
96
|
+
publish(entry, next) {
|
|
97
|
+
if (this.closed)
|
|
98
|
+
return;
|
|
99
|
+
const previous = entry.snapshot;
|
|
100
|
+
if (previous.data && next.data && JSON.stringify(previous.data) === JSON.stringify(next.data))
|
|
101
|
+
next = { ...next, data: previous.data };
|
|
102
|
+
if (previous.data === next.data && previous.loading === next.loading && previous.error === next.error)
|
|
103
|
+
return;
|
|
104
|
+
entry.snapshot = Object.freeze(next);
|
|
105
|
+
this.listeners.forEach(listener => listener(entry.thread.id));
|
|
106
|
+
}
|
|
107
|
+
normalize(value, entry) {
|
|
108
|
+
const scope = this.options.scope;
|
|
109
|
+
if (!value || value.appId !== scope.appId || value.groupId !== entry.thread.targetId || value.teamId !== scope.teamId || !validMessageIdentifier(value.ownerId) || !validMessageIdentifier(value.revision) || !Number.isSafeInteger(value.versionNumber) || value.versionNumber < 1 || !Array.isArray(value.members) || !value.members.length || value.members.length > 256 || !value.members.every(validMessageIdentifier) || !value.members.includes(value.ownerId) || !value.members.includes(scope.profileId) || typeof value.title !== "string" || value.avatar !== undefined && value.avatar !== null && typeof value.avatar !== "string")
|
|
110
|
+
throw new Error("INVALID_GROUP_METADATA");
|
|
111
|
+
return Object.freeze({ groupId: value.groupId, teamId: scope.teamId, ownerId: value.ownerId, title: value.title, ...(value.avatar ? { avatar: value.avatar } : {}), memberIds: Object.freeze([...new Set(value.members)].sort()), revision: value.revision, versionNumber: value.versionNumber });
|
|
112
|
+
}
|
|
113
|
+
cached(value, entry) {
|
|
114
|
+
return this.normalize({ appId: this.options.scope.appId, groupId: value?.groupId, teamId: value?.teamId, ownerId: value?.ownerId, title: value?.title, avatar: value?.avatar, members: value?.memberIds, revision: value?.revision, versionNumber: value?.versionNumber }, entry);
|
|
115
|
+
}
|
|
116
|
+
local(entry) {
|
|
117
|
+
if (entry.loaded)
|
|
118
|
+
return Promise.resolve();
|
|
119
|
+
if (entry.local)
|
|
120
|
+
return entry.local;
|
|
121
|
+
this.publish(entry, { ...entry.snapshot, loading: !entry.snapshot.data });
|
|
122
|
+
entry.local = new Promise((resolve, reject) => this.localQueue.push({ entry, resolve, reject })).finally(() => { entry.local = undefined; });
|
|
123
|
+
if (!this.localScheduled) {
|
|
124
|
+
this.localScheduled = true;
|
|
125
|
+
queueMicrotask(() => { this.localScheduled = false; void this.flushLocal(); });
|
|
126
|
+
}
|
|
127
|
+
return entry.local;
|
|
128
|
+
}
|
|
129
|
+
flushLocal() {
|
|
130
|
+
const task = (async () => {
|
|
131
|
+
while (this.localQueue.length) {
|
|
132
|
+
const batch = this.localQueue.splice(0, 128);
|
|
133
|
+
try {
|
|
134
|
+
this.assertOpen();
|
|
135
|
+
await this.checkAuthority();
|
|
136
|
+
const rows = await this.cache.readMany(batch.map(item => item.entry.thread.id));
|
|
137
|
+
await this.checkAuthority();
|
|
138
|
+
this.assertOpen();
|
|
139
|
+
for (const item of batch) {
|
|
140
|
+
try {
|
|
141
|
+
const row = rows.get(item.entry.thread.id), entry = item.entry;
|
|
142
|
+
let data = row.cached ? this.cached(row.cached.value, entry) : undefined;
|
|
143
|
+
const old = entry.snapshot.data;
|
|
144
|
+
if (old && data && old.versionNumber > data.versionNumber)
|
|
145
|
+
data = old;
|
|
146
|
+
if (old && data && old.versionNumber === data.versionNumber && JSON.stringify(old) !== JSON.stringify(data))
|
|
147
|
+
throw new Error("GROUP_REVISION_CONFLICT");
|
|
148
|
+
entry.epoch = row.epoch;
|
|
149
|
+
entry.token = row.cached ? token(row.cached.value) : null;
|
|
150
|
+
entry.loaded = true;
|
|
151
|
+
this.publish(entry, { data: data ?? old, loading: false });
|
|
152
|
+
item.resolve();
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.publish(item.entry, { ...item.entry.snapshot, loading: false, error });
|
|
156
|
+
item.reject(error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
for (const item of batch) {
|
|
162
|
+
this.publish(item.entry, { loading: false, error });
|
|
163
|
+
item.reject(error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})().finally(() => this.localRunning.delete(task));
|
|
168
|
+
this.localRunning.add(task);
|
|
169
|
+
return task;
|
|
170
|
+
}
|
|
171
|
+
async reload(entry) { entry.loaded = false; await this.local(entry); }
|
|
172
|
+
async get(thread, options = {}) {
|
|
173
|
+
const entry = this.entry(thread);
|
|
174
|
+
await this.local(entry);
|
|
175
|
+
await this.checkAuthority();
|
|
176
|
+
this.assertOpen();
|
|
177
|
+
if (entry.snapshot.data && !options.refresh) {
|
|
178
|
+
if (!entry.checked && !isOffline())
|
|
179
|
+
this.schedule(entry);
|
|
180
|
+
return entry.snapshot.data;
|
|
181
|
+
}
|
|
182
|
+
if (isOffline())
|
|
183
|
+
throw entry.snapshot.error ?? new Error("GROUP_METADATA_OFFLINE");
|
|
184
|
+
return this.request(entry);
|
|
185
|
+
}
|
|
186
|
+
schedule(entry) { if (!this.closed && !this.withdrawn && !isOffline())
|
|
187
|
+
this.tasks.schedule(entry.thread.id, async () => { await this.get(entry.thread, { refresh: true }); }); }
|
|
188
|
+
wake() { for (const entry of this.entries.values())
|
|
189
|
+
this.schedule(entry); }
|
|
190
|
+
hint(thread, versionNumber) {
|
|
191
|
+
const entry = this.entries.get(thread.id);
|
|
192
|
+
if (!entry || this.closed || this.withdrawn || versionNumber !== undefined && versionNumber <= (entry.snapshot.data?.versionNumber ?? 0))
|
|
193
|
+
return;
|
|
194
|
+
entry.generation++;
|
|
195
|
+
entry.attempts = 0;
|
|
196
|
+
entry.wanted = Math.max(entry.wanted, versionNumber ?? 0);
|
|
197
|
+
this.schedule(entry);
|
|
198
|
+
}
|
|
199
|
+
slot(run) {
|
|
200
|
+
if (this.queue.length >= 128)
|
|
201
|
+
return Promise.reject(new Error("GROUP_NETWORK_BACKPRESSURE"));
|
|
202
|
+
return new Promise((resolve, reject) => { this.queue.push({ run, resolve, reject }); this.drain(); });
|
|
203
|
+
}
|
|
204
|
+
drain() {
|
|
205
|
+
while (!this.closed && this.active < 4 && this.queue.length) {
|
|
206
|
+
const work = this.queue.shift();
|
|
207
|
+
this.active++;
|
|
208
|
+
void work.run().then(work.resolve, work.reject).finally(() => { this.active--; this.drain(); });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
request(entry) {
|
|
212
|
+
if (entry.request)
|
|
213
|
+
return entry.request;
|
|
214
|
+
const task = this.slot(async () => {
|
|
215
|
+
this.assertOpen();
|
|
216
|
+
await this.checkAuthority();
|
|
217
|
+
const base = entry.token, generation = entry.generation, controller = new AbortController();
|
|
218
|
+
this.controllers.add(controller);
|
|
219
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
220
|
+
this.publish(entry, { ...entry.snapshot, loading: !entry.snapshot.data, error: undefined });
|
|
221
|
+
try {
|
|
222
|
+
const value = this.normalize(await this.options.load(entry.thread.targetId, controller.signal), entry);
|
|
223
|
+
await this.checkAuthority();
|
|
224
|
+
this.assertOpen();
|
|
225
|
+
if (entry.snapshot.data && value.versionNumber <= entry.snapshot.data.versionNumber)
|
|
226
|
+
await this.reload(entry);
|
|
227
|
+
const old = entry.snapshot.data;
|
|
228
|
+
if (old && value.versionNumber < old.versionNumber) {
|
|
229
|
+
this.publish(entry, { data: old, loading: false });
|
|
230
|
+
return old;
|
|
231
|
+
}
|
|
232
|
+
if (old && value.versionNumber === old.versionNumber) {
|
|
233
|
+
if (value.revision !== old.revision || JSON.stringify(value) !== JSON.stringify(old))
|
|
234
|
+
throw new Error("GROUP_REVISION_CONFLICT");
|
|
235
|
+
entry.checked = true;
|
|
236
|
+
this.publish(entry, { data: old, loading: false });
|
|
237
|
+
return old;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await this.cache.write(entry.thread.id, value, entry.epoch, base);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (!String(error).includes("RESPONSE_CACHE_CHANGED"))
|
|
244
|
+
throw error;
|
|
245
|
+
await this.reload(entry);
|
|
246
|
+
this.schedule(entry);
|
|
247
|
+
if (entry.snapshot.data)
|
|
248
|
+
return entry.snapshot.data;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
await this.checkAuthority();
|
|
252
|
+
this.assertOpen();
|
|
253
|
+
entry.token = token(value);
|
|
254
|
+
entry.checked = true;
|
|
255
|
+
this.publish(entry, { data: value, loading: false });
|
|
256
|
+
this.channel?.postMessage({ threadId: entry.thread.id, authority: this.version });
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (!this.closed) {
|
|
261
|
+
const denied = [401, 403].includes(error?.status ?? 0);
|
|
262
|
+
this.publish(entry, { data: denied ? undefined : entry.snapshot.data, loading: false, error });
|
|
263
|
+
if (denied)
|
|
264
|
+
await this.options.onDenied?.(error);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
this.controllers.delete(controller);
|
|
271
|
+
if (!this.closed && (generation !== entry.generation || entry.wanted > (entry.snapshot.data?.versionNumber ?? 0)) && entry.attempts++ < 4)
|
|
272
|
+
this.schedule(entry);
|
|
273
|
+
}
|
|
274
|
+
}).finally(() => { if (entry.request === task)
|
|
275
|
+
entry.request = undefined; });
|
|
276
|
+
entry.request = task;
|
|
277
|
+
return task;
|
|
278
|
+
}
|
|
279
|
+
async after(thread, minimumVersion) {
|
|
280
|
+
if (!Number.isSafeInteger(minimumVersion) || minimumVersion < 1)
|
|
281
|
+
throw new Error("INVALID_GROUP_VERSION");
|
|
282
|
+
const entry = this.entry(thread);
|
|
283
|
+
for (const delay of [0, 200, 400, 800, 1600]) {
|
|
284
|
+
if (entry.snapshot.data && entry.snapshot.data.versionNumber >= minimumVersion)
|
|
285
|
+
return entry.snapshot.data;
|
|
286
|
+
if (delay)
|
|
287
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
288
|
+
this.assertOpen();
|
|
289
|
+
try {
|
|
290
|
+
const value = await this.get(thread, { refresh: true });
|
|
291
|
+
if (value.versionNumber >= minimumVersion)
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (this.closed || this.withdrawn || isOffline() || ![503].includes(error?.status ?? 0))
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
throw new Error("GROUP_SYNC_PENDING");
|
|
300
|
+
}
|
|
301
|
+
withdraw() { this.withdrawn = true; this.tasks.close(); this.controllers.forEach(controller => controller.abort()); for (const entry of this.entries.values())
|
|
302
|
+
this.publish(entry, { loading: false, error: new Error("GROUP_ACCESS_REVOKED") }); }
|
|
303
|
+
retire(version) { this.withdraw(); return this.cache.retire(version); }
|
|
304
|
+
close() {
|
|
305
|
+
if (this.closing)
|
|
306
|
+
return this.closing;
|
|
307
|
+
this.closed = true;
|
|
308
|
+
this.tasks.close();
|
|
309
|
+
this.channel?.close();
|
|
310
|
+
this.controllers.forEach(controller => controller.abort());
|
|
311
|
+
this.listeners.clear();
|
|
312
|
+
const error = new Error("GROUP_CLIENT_CLOSED");
|
|
313
|
+
this.queue.splice(0).forEach(work => work.reject(error));
|
|
314
|
+
this.localQueue.splice(0).forEach(work => work.reject(error));
|
|
315
|
+
return this.closing = (async () => { try {
|
|
316
|
+
await Promise.allSettled([...this.entries.values()].flatMap(entry => entry.request ? [entry.request] : []));
|
|
317
|
+
await Promise.allSettled([...this.localRunning]);
|
|
318
|
+
await this.cache.close();
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
this.entries.clear();
|
|
322
|
+
this.pins.clear();
|
|
323
|
+
} })();
|
|
324
|
+
}
|
|
325
|
+
}
|
package/lib/runtime/index.d.ts
CHANGED
|
@@ -29,3 +29,9 @@ export * from "./durableSender.js";
|
|
|
29
29
|
export * from "./chatSession.js";
|
|
30
30
|
export * from "./messageReaders.js";
|
|
31
31
|
export * from "./createChat.js";
|
|
32
|
+
export * from "./conversationRead.js";
|
|
33
|
+
export { projectConversationReading, type ReadingCandidate, type MessageReadingState, type ConversationReadingView } from "./conversationReading.js";
|
|
34
|
+
export { projectMessageTimeline, type TimelineItem, type TimelineReaction, type TimelineReactionEvent } from "./messageTimeline.js";
|
|
35
|
+
export * from "./messageLaunch.js";
|
|
36
|
+
export * from "./outgoingContent.js";
|
|
37
|
+
export type { ChatGroup } from "./groupClient.js";
|
package/lib/runtime/index.js
CHANGED
|
@@ -29,3 +29,8 @@ export * from "./durableSender.js";
|
|
|
29
29
|
export * from "./chatSession.js";
|
|
30
30
|
export * from "./messageReaders.js";
|
|
31
31
|
export * from "./createChat.js";
|
|
32
|
+
export * from "./conversationRead.js";
|
|
33
|
+
export { projectConversationReading } from "./conversationReading.js";
|
|
34
|
+
export { projectMessageTimeline } from "./messageTimeline.js";
|
|
35
|
+
export * from "./messageLaunch.js";
|
|
36
|
+
export * from "./outgoingContent.js";
|
|
@@ -36,9 +36,12 @@ export declare class MessageClient<T extends StoredMessage> {
|
|
|
36
36
|
private options;
|
|
37
37
|
private storage;
|
|
38
38
|
private watches;
|
|
39
|
+
private windowEdges;
|
|
40
|
+
private accessListeners;
|
|
39
41
|
private allListeners;
|
|
40
42
|
private reads;
|
|
41
43
|
private syncing;
|
|
44
|
+
private completedSyncs;
|
|
42
45
|
private paging;
|
|
43
46
|
private writes;
|
|
44
47
|
private activeWrites;
|
|
@@ -54,15 +57,34 @@ export declare class MessageClient<T extends StoredMessage> {
|
|
|
54
57
|
private queueBytes;
|
|
55
58
|
private pendingBytes;
|
|
56
59
|
private pendingRecords;
|
|
60
|
+
private legacyImports;
|
|
61
|
+
private revokingScope?;
|
|
62
|
+
private scopeReady?;
|
|
63
|
+
private scopeVersion;
|
|
64
|
+
private scopeAllowed;
|
|
65
|
+
private scopeDenied;
|
|
66
|
+
private enqueuing;
|
|
67
|
+
private ensureScope;
|
|
57
68
|
constructor(options: MessageClientOptions<T>);
|
|
69
|
+
/** Shared guard for sibling caches; it never authorizes a denied old instance. */
|
|
70
|
+
subscribeAccessRevoked(listener: () => void): () => void;
|
|
71
|
+
scopeAccess(): Promise<{
|
|
72
|
+
version: number;
|
|
73
|
+
allowed: boolean;
|
|
74
|
+
}>;
|
|
58
75
|
getSnapshot(thread: string): MessageSnapshot<T> | undefined;
|
|
76
|
+
/** Exact authoritative row only; migration-only rows never prove delivery. */
|
|
77
|
+
cachedMessage(thread: string, id: string): Promise<T>;
|
|
59
78
|
subscribe(thread: string, listener: () => void): () => void;
|
|
60
79
|
subscribeAll(listener: (threadId: string) => void): () => void;
|
|
61
80
|
/** Close the connection gap for saved windows with four bounded background lanes. */
|
|
62
|
-
restore(since: number): Promise<void>;
|
|
81
|
+
restore(since: number, threadId?: string): Promise<void>;
|
|
63
82
|
read(thread: string, limit?: number, refresh?: boolean): Promise<MessageSnapshot<T>>;
|
|
64
83
|
/** Application connection/wake events invalidate, rather than fetching directly. */
|
|
65
84
|
invalidate(thread: string): void;
|
|
85
|
+
private fallback;
|
|
86
|
+
/** Migration batches remain separate from authoritative message coverage. */
|
|
87
|
+
importLegacy(thread: string, messages: T[]): Promise<void>;
|
|
66
88
|
private reload;
|
|
67
89
|
/** Events are visible only after their atomic batch commits. */
|
|
68
90
|
receive(thread: string, messages: T[]): Promise<void>;
|
|
@@ -71,18 +93,18 @@ export declare class MessageClient<T extends StoredMessage> {
|
|
|
71
93
|
private release;
|
|
72
94
|
private flushWrites;
|
|
73
95
|
private runWrites;
|
|
96
|
+
private assertNetworkAvailable;
|
|
74
97
|
synchronize(thread: string): Promise<void>;
|
|
98
|
+
/** Keep an explicitly requested history range in the shared subscribed view. */
|
|
99
|
+
expandWindow(thread: string, page: MessageSnapshot<T>): Promise<void>;
|
|
75
100
|
earlier(thread: string, before: {
|
|
76
101
|
timestamp: number;
|
|
77
102
|
id: string;
|
|
78
|
-
}, limit?: number): Promise<
|
|
79
|
-
messages: T[];
|
|
80
|
-
hasEarlier: boolean;
|
|
81
|
-
resetVersion: number;
|
|
82
|
-
complete: boolean;
|
|
83
|
-
}>;
|
|
103
|
+
}, limit?: number, expand?: boolean): Promise<MessageSnapshot<T>>;
|
|
84
104
|
private fillWindow;
|
|
85
105
|
private denyView;
|
|
106
|
+
private denyScope;
|
|
107
|
+
revokeScope(): Promise<number>;
|
|
86
108
|
revoke(thread: string): Promise<void>;
|
|
87
|
-
close(): Promise<void>;
|
|
109
|
+
close(beforeDispose?: Promise<unknown>): Promise<void>;
|
|
88
110
|
}
|