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

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.
@@ -1,3 +1,4 @@
1
+ import { type MessageScope } from "./messageStore.js";
1
2
  export type ReadCountsResult = {
2
3
  appId: string;
3
4
  threadId: string;
@@ -11,6 +12,7 @@ export type ReadCountsOptions = {
11
12
  clientInstanceId: string;
12
13
  hubHttpUrl: string;
13
14
  fetch?: typeof fetch;
15
+ scope?: MessageScope;
14
16
  onChange: (value: ReadCountsResult) => void;
15
17
  onError?: (error: unknown) => void;
16
18
  };
@@ -31,7 +33,15 @@ export declare class ReadCountsClient {
31
33
  private subscribed;
32
34
  private subscriptionDenied;
33
35
  private subscriptionPending?;
36
+ private cache?;
37
+ private lastResult?;
38
+ private publish;
39
+ private revoked;
40
+ private accessVersion;
41
+ private requestedRevision;
42
+ private force;
34
43
  constructor(options: ReadCountsOptions);
44
+ private denyCache;
35
45
  private post;
36
46
  private subscription;
37
47
  private renew;
@@ -1,3 +1,5 @@
1
+ import { IndexedDbResponseCache } from "./responseCache.js";
2
+ import { messageScopeKey } from "./messageStore.js";
1
3
  let subscriptionSequence = 0;
2
4
  function nextSubscriptionSequence() { subscriptionSequence = Math.max(Date.now() * 1000, subscriptionSequence + 1); return subscriptionSequence; }
3
5
  /** One active conversation; fixed event window, bounded batches, no count polling. */
@@ -17,8 +19,31 @@ export class ReadCountsClient {
17
19
  subscribed = false;
18
20
  subscriptionDenied = false;
19
21
  subscriptionPending;
22
+ cache;
23
+ lastResult;
24
+ publish(value) {
25
+ if (JSON.stringify(value) === JSON.stringify(this.lastResult))
26
+ return;
27
+ this.lastResult = value;
28
+ this.options.onChange(value);
29
+ }
30
+ revoked = false;
31
+ accessVersion = 0;
32
+ requestedRevision = -1;
33
+ force = false;
20
34
  constructor(options) {
21
35
  this.options = options;
36
+ if (options.scope) {
37
+ if (options.scope.appId !== options.appId || options.scope.profileId !== options.profileId)
38
+ throw new Error("READ_COUNTS_SCOPE_MISMATCH");
39
+ this.cache = new IndexedDbResponseCache(messageScopeKey(options.scope));
40
+ }
41
+ }
42
+ denyCache() {
43
+ this.revoked = true;
44
+ this.accessVersion++;
45
+ this.lastResult = undefined;
46
+ void this.cache?.invalidate().catch(error => this.options.onError?.(error));
22
47
  }
23
48
  async post(path, body, signal) {
24
49
  const response = await (this.options.fetch ?? fetch)(`${this.options.hubHttpUrl.replace(/\/$/, "")}${path}`, { method: "POST", headers: { authorization: `Bearer ${this.options.token}`, "content-type": "application/json" }, body: JSON.stringify(body), signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(8000)]) : AbortSignal.timeout(8000), redirect: "error" });
@@ -33,7 +58,7 @@ export class ReadCountsClient {
33
58
  return this.post("/api/social/reading/subscription", { appId: this.options.appId, profileId: this.options.profileId, threadId, clientInstanceId: this.options.clientInstanceId, sequence: nextSubscriptionSequence(), release });
34
59
  }
35
60
  renew() {
36
- if (!this.thread || this.closed || this.subscriptionDenied)
61
+ if (!this.thread || this.closed || this.subscriptionDenied || (typeof navigator !== "undefined" && navigator.onLine === false))
37
62
  return;
38
63
  const threadId = this.thread, generation = this.generation;
39
64
  if (this.subscriptionPending?.generation === generation)
@@ -56,6 +81,7 @@ export class ReadCountsClient {
56
81
  this.subscribed = false;
57
82
  if (/READ_COUNTS_HTTP_(401|403)/.test(String(error))) {
58
83
  this.subscriptionDenied = true;
84
+ this.denyCache();
59
85
  clearInterval(this.heartbeat);
60
86
  this.heartbeat = undefined;
61
87
  }
@@ -83,9 +109,12 @@ export class ReadCountsClient {
83
109
  clearTimeout(this.timer);
84
110
  clearInterval(this.heartbeat);
85
111
  this.timer = undefined;
112
+ this.lastResult = undefined;
86
113
  this.thread = threadId;
87
114
  this.ids = ids;
88
115
  this.revision = -1;
116
+ this.requestedRevision = -1;
117
+ this.force = false;
89
118
  this.dirty = false;
90
119
  this.retries = 0;
91
120
  this.subscribed = false;
@@ -104,11 +133,13 @@ export class ReadCountsClient {
104
133
  throw new Error("INVALID_READ_COUNTS_EVENT");
105
134
  if (threadId !== this.thread || revision <= this.revision || this.closed)
106
135
  return;
136
+ this.requestedRevision = Math.max(this.requestedRevision, revision);
107
137
  this.schedule(3000);
108
138
  }
109
139
  reconnect() {
110
140
  if (!this.thread || this.closed)
111
141
  return;
142
+ this.force = true;
112
143
  this.subscribed = false;
113
144
  this.subscriptionDenied = false;
114
145
  this.renew();
@@ -137,20 +168,44 @@ export class ReadCountsClient {
137
168
  let retryDelay;
138
169
  const controller = new AbortController();
139
170
  this.controller = controller;
171
+ const accessVersion = this.accessVersion, force = this.force;
172
+ this.force = false;
173
+ const key = JSON.stringify(["counts", threadId, [...ids].sort()]);
140
174
  try {
175
+ const stored = this.cache ? await this.cache.read(key).catch(error => { this.options.onError?.(error); return undefined; }) : undefined;
176
+ if (generation !== this.generation || this.closed || accessVersion !== this.accessVersion)
177
+ return;
178
+ const cached = this.revoked || (stored?.cached && stored.cached.value.revision < this.revision) ? undefined : stored?.cached;
179
+ if (cached) {
180
+ this.revision = cached.value.revision;
181
+ this.publish(cached.value);
182
+ const offline = typeof navigator !== "undefined" && navigator.onLine === false;
183
+ if (offline || (!force && this.requestedRevision <= this.revision && Date.now() - cached.time < 5000))
184
+ return;
185
+ }
141
186
  const value = await this.post("/api/social/reading/counts", { appId: this.options.appId, profileId: this.options.profileId, threadId, messageIds: ids }, controller.signal);
142
- if (generation !== this.generation || this.closed)
187
+ if (generation !== this.generation || this.closed || accessVersion !== this.accessVersion)
143
188
  return;
144
189
  if (value?.appId !== this.options.appId || value.threadId !== threadId || !Number.isSafeInteger(value.revision) || value.revision < 0 || !value.counts || typeof value.counts !== "object" || ids.some(id => !Number.isSafeInteger(value.counts[id]) || value.counts[id] < 0) || Object.keys(value.counts).some(id => !ids.includes(id)))
145
190
  throw new Error("INVALID_READ_COUNTS_RESULT");
191
+ if (value.revision < this.revision)
192
+ return;
146
193
  this.retries = 0;
194
+ this.revoked = false;
195
+ if (stored)
196
+ await this.cache?.write(key, value, stored.epoch).catch(error => { if (String(error).includes("RESPONSE_CACHE_INVALIDATED"))
197
+ throw error; this.options.onError?.(error); });
198
+ if (generation !== this.generation || this.closed || accessVersion !== this.accessVersion)
199
+ return;
147
200
  this.revision = value.revision;
148
- this.options.onChange(value);
201
+ this.publish(value);
149
202
  if (!this.subscribed)
150
203
  this.renew();
151
204
  }
152
205
  catch (error) {
153
206
  if (!controller.signal.aborted && generation === this.generation && !this.closed) {
207
+ if (/READ_COUNTS_HTTP_(401|403)/.test(String(error)))
208
+ this.denyCache();
154
209
  this.options.onError?.(error);
155
210
  if (!/READ_COUNTS_HTTP_4\d\d|INVALID_READ_COUNTS/.test(String(error)) && this.retries < 3) {
156
211
  retryDelay = 1000 * 2 ** this.retries++;
@@ -178,5 +233,6 @@ export class ReadCountsClient {
178
233
  if (old)
179
234
  void this.subscription(old, true).catch(() => { });
180
235
  this.thread = undefined;
236
+ void this.cache?.close().catch(error => this.options.onError?.(error));
181
237
  }
182
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.8",
3
+ "version": "0.12.0-next.9",
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",