@pmate/chat-sdk 0.12.0-next.1 → 0.12.0-next.2

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.
@@ -33,6 +33,7 @@ export declare class MessageClient<T extends StoredMessage> {
33
33
  private watches;
34
34
  private reads;
35
35
  private syncing;
36
+ private paging;
36
37
  private writes;
37
38
  private activeWrites;
38
39
  private writeTasks;
@@ -63,6 +64,7 @@ export declare class MessageClient<T extends StoredMessage> {
63
64
  messages: T[];
64
65
  hasEarlier: boolean;
65
66
  }>;
67
+ private fillWindow;
66
68
  revoke(thread: string): Promise<void>;
67
69
  close(): Promise<void>;
68
70
  }
@@ -7,6 +7,7 @@ export class MessageClient {
7
7
  watches = new Map();
8
8
  reads = new Map();
9
9
  syncing = new Map();
10
+ paging = new Map();
10
11
  writes = new Map();
11
12
  activeWrites = new Set();
12
13
  writeTasks;
@@ -55,6 +56,7 @@ export class MessageClient {
55
56
  await this.synchronize(thread);
56
57
  if (this.closed)
57
58
  throw new Error("MESSAGE_CLIENT_CLOSED");
59
+ await this.fillWindow(thread, limit);
58
60
  return this.reload(thread, limit);
59
61
  })().finally(() => { if (this.reads.get(key) === task)
60
62
  this.reads.delete(key); });
@@ -183,19 +185,33 @@ export class MessageClient {
183
185
  throw new Error("MESSAGE_ACCESS_REVOKED");
184
186
  if (!(await this.storage.read(thread, 1)).coverage?.complete)
185
187
  await this.synchronize(thread);
188
+ return this.fillWindow(thread, limit, before);
189
+ }
190
+ async fillWindow(thread, limit, before) {
186
191
  let page = await this.storage.read(thread, limit, before);
187
192
  while (page.messages.length < limit && page.coverage?.nextBefore && this.options.transport.older) {
188
- const token = page.coverage.nextBefore;
189
- const remote = await this.options.transport.older(thread, token, this.abort.signal);
190
- if (this.closed || this.revoked.has(thread))
191
- throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
192
- if (remote.resetRequired) {
193
- await this.storage.removeThread(thread);
194
- throw new Error("MESSAGE_SYNC_RESET_REQUIRED");
193
+ const pending = this.paging.get(thread);
194
+ if (pending) {
195
+ await pending;
196
+ page = await this.storage.read(thread, limit, before);
197
+ continue;
195
198
  }
196
- if (remote.nextBefore === token)
197
- throw new Error("MESSAGE_SYNC_NO_PROGRESS");
198
- await this.persist(thread, remote.messages, undefined, true, remote.nextBefore ?? null);
199
+ const token = page.coverage.nextBefore;
200
+ const task = (async () => {
201
+ const remote = await this.options.transport.older(thread, token, this.abort.signal);
202
+ if (this.closed || this.revoked.has(thread))
203
+ throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
204
+ if (remote.resetRequired) {
205
+ await this.storage.removeThread(thread);
206
+ throw new Error("MESSAGE_SYNC_RESET_REQUIRED");
207
+ }
208
+ if (remote.nextBefore === token)
209
+ throw new Error("MESSAGE_SYNC_NO_PROGRESS");
210
+ await this.persist(thread, remote.messages, undefined, true, remote.nextBefore ?? null);
211
+ })().finally(() => { if (this.paging.get(thread) === task)
212
+ this.paging.delete(thread); });
213
+ this.paging.set(thread, task);
214
+ await task;
199
215
  page = await this.storage.read(thread, limit, before);
200
216
  }
201
217
  return { messages: page.messages, hasEarlier: page.hasEarlier || Boolean(page.coverage?.nextBefore) };
@@ -39,21 +39,33 @@ export class MessageReadersClient {
39
39
  }
40
40
  async request(input) {
41
41
  const { token, fetch: fetcher, hubHttpUrl, appId, profileId } = this.options;
42
- const response = await (fetcher ?? fetch)(`${hubHttpUrl.replace(/^ws/, "http").replace(/\/+$/, "")}/api/social/reading/readers`, {
43
- method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ ...input, appId, profileId }),
44
- signal: AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(8000)]), redirect: "error", cache: "no-store",
45
- });
46
- if (!response.ok)
47
- throw new MessageReadersError(response.status, `MESSAGE_READERS_HTTP_${response.status}`);
48
- const envelope = await response.json(), data = envelope.data;
49
- if (envelope.success !== true || data?.appId !== appId || data.threadId !== input.threadId || data.messageId !== input.messageId
50
- || !Number.isSafeInteger(data.revision) || data.revision < 0 || !Number.isSafeInteger(data.total) || data.total < 0
51
- || !Array.isArray(data.profileIds) || data.profileIds.length > input.limit || data.profileIds.length > data.total
52
- || data.profileIds.some((id, i, ids) => typeof id !== "string" || !id || id.length > 512 || id === profileId || (i > 0 && id <= ids[i - 1]) || (input.afterProfileId && !data.resetRequired && id <= input.afterProfileId))
53
- || typeof data.resetRequired !== "boolean" || (data.nextAfter !== null && (typeof data.nextAfter !== "string" || data.nextAfter !== data.profileIds.at(-1)))
54
- || (!data.resetRequired && input.revision !== undefined && data.revision !== input.revision))
55
- throw new Error("INVALID_MESSAGE_READERS_RESPONSE");
56
- return data;
42
+ const controller = new AbortController();
43
+ const cancel = () => controller.abort(this.lifetime.signal.reason);
44
+ this.lifetime.signal.addEventListener("abort", cancel, { once: true });
45
+ const timeout = setTimeout(() => controller.abort(), 8000);
46
+ try {
47
+ const response = await (fetcher ?? fetch)(`${hubHttpUrl.replace(/^ws/, "http").replace(/\/+$/, "")}/api/social/reading/readers`, {
48
+ method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ ...input, appId, profileId }),
49
+ signal: controller.signal, redirect: "error", cache: "no-store",
50
+ });
51
+ if (!response.ok)
52
+ throw new MessageReadersError(response.status, `MESSAGE_READERS_HTTP_${response.status}`);
53
+ const envelope = await response.json(), data = envelope.data;
54
+ if (envelope.success !== true || data?.appId !== appId || data.threadId !== input.threadId || data.messageId !== input.messageId
55
+ || !Number.isSafeInteger(data.revision) || data.revision < 0 || !Number.isSafeInteger(data.total) || data.total < 0
56
+ || !Array.isArray(data.profileIds) || data.profileIds.length > input.limit || data.profileIds.length > data.total
57
+ || data.profileIds.some((id, i, ids) => typeof id !== "string" || !id || id.length > 512 || id === profileId || (i > 0 && id <= ids[i - 1]) || (input.afterProfileId && !data.resetRequired && id <= input.afterProfileId))
58
+ || typeof data.resetRequired !== "boolean" || (data.nextAfter !== null && (typeof data.nextAfter !== "string" || data.nextAfter !== data.profileIds.at(-1)))
59
+ || (!data.resetRequired && input.revision !== undefined && data.revision !== input.revision))
60
+ throw new Error("INVALID_MESSAGE_READERS_RESPONSE");
61
+ if (this.lifetime.signal.aborted)
62
+ throw new Error("MESSAGE_READERS_CLOSED");
63
+ return data;
64
+ }
65
+ finally {
66
+ clearTimeout(timeout);
67
+ this.lifetime.signal.removeEventListener("abort", cancel);
68
+ }
57
69
  }
58
70
  close() { this.lifetime.abort(); this.pending.clear(); }
59
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.1",
3
+ "version": "0.12.0-next.2",
4
4
  "description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",