@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.
- package/lib/runtime/readCounts.d.ts +10 -0
- package/lib/runtime/readCounts.js +59 -3
- package/package.json +1 -1
|
@@ -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.
|
|
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