@pmate/chat-sdk 0.12.0-next.82 → 0.12.0-next.83
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/readSync.d.ts +1 -0
- package/lib/runtime/readSync.js +55 -6
- package/package.json +1 -1
package/lib/runtime/readSync.js
CHANGED
|
@@ -39,8 +39,12 @@ export class ReadSyncClient {
|
|
|
39
39
|
const response = await (this.options.fetch ?? globalThis.fetch)(`${this.options.hubHttpUrl.replace(/\/$/, "")}${path}`, {
|
|
40
40
|
...init, signal: timeout.signal, headers: { ...init.headers, authorization: `Bearer ${this.options.sessionToken}` },
|
|
41
41
|
});
|
|
42
|
-
if (!response.ok)
|
|
43
|
-
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
// Keep only a fixed domain code, never a provider response body.
|
|
44
|
+
const failure = await response.json().catch(() => undefined);
|
|
45
|
+
const code = failure?.message === "INVALID_READING_CURSOR" ? failure.message : undefined;
|
|
46
|
+
throw Object.assign(new Error(`READ_SYNC_HTTP_${response.status}`), { status: response.status, code });
|
|
47
|
+
}
|
|
44
48
|
const envelope = await response.json();
|
|
45
49
|
if (envelope.success !== true || envelope.data === undefined)
|
|
46
50
|
throw new Error("INVALID_READ_SYNC_RESPONSE");
|
|
@@ -51,7 +55,42 @@ export class ReadSyncClient {
|
|
|
51
55
|
this.controller.signal.removeEventListener("abort", abort);
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
|
-
async
|
|
58
|
+
async repairLegacyDiscussion(operation, versions) {
|
|
59
|
+
if (operation.threadRootId !== undefined || operation.readThroughSeq <= 0 || operation.explicitReadMessageIds.length)
|
|
60
|
+
return false;
|
|
61
|
+
const { appId, profileId, persistence } = this.options;
|
|
62
|
+
const query = new URLSearchParams({ appId, profileId, ...(this.options.metadataOnly ? { metadataOnly: "true" } : {}) });
|
|
63
|
+
const snapshot = await this.request(`/api/social/reading/snapshot?${query}`);
|
|
64
|
+
// Validate the complete response before changing any durable operation.
|
|
65
|
+
installReadSnapshot(createReadIndex(appId, profileId), snapshot);
|
|
66
|
+
const thread = snapshot.threads.find(thread => thread.id === operation.threadId);
|
|
67
|
+
const message = thread?.messages.find(message => message.id === operation.readThroughMessageId
|
|
68
|
+
&& message.seq === operation.readThroughSeq && message.readPolicy === "visible" && message.threadRootId != null);
|
|
69
|
+
if (!thread || !message)
|
|
70
|
+
return false;
|
|
71
|
+
let repaired = false;
|
|
72
|
+
await persistence.update(appId, profileId, (state, currentVersions) => {
|
|
73
|
+
assertReadIndexVersions(versions, currentVersions);
|
|
74
|
+
if (this.controller.signal.aborted)
|
|
75
|
+
throw new Error("READ_SYNC_STOPPED");
|
|
76
|
+
const pending = state.pending.map(item => {
|
|
77
|
+
if (item.operationId !== operation.operationId || JSON.stringify(item) !== JSON.stringify(operation))
|
|
78
|
+
return item;
|
|
79
|
+
repaired = true;
|
|
80
|
+
// New payload, new identity: an operation ID must never change meaning.
|
|
81
|
+
return { ...item, operationId: crypto.randomUUID(), threadRootId: message.threadRootId };
|
|
82
|
+
});
|
|
83
|
+
if (!repaired)
|
|
84
|
+
return state;
|
|
85
|
+
// Undo the rejected main-conversation optimistic position, then replay all
|
|
86
|
+
// remaining operations on the authoritative snapshot in their own scopes.
|
|
87
|
+
const threads = state.threads.map(item => item.id === thread.id && item.readThroughSeq === operation.readThroughSeq
|
|
88
|
+
? { ...item, readThroughSeq: thread.readThroughSeq } : item);
|
|
89
|
+
return installReadSnapshot({ ...state, threads, pending }, snapshot);
|
|
90
|
+
});
|
|
91
|
+
return repaired;
|
|
92
|
+
}
|
|
93
|
+
async run(repairedLegacy = false) {
|
|
55
94
|
const { appId, profileId, persistence } = this.options;
|
|
56
95
|
const loaded = persistence.loadVersioned ? await persistence.loadVersioned(appId, profileId) : { state: await persistence.load(appId, profileId), versions: {} };
|
|
57
96
|
const current = loaded.state ?? createReadIndex(appId, profileId);
|
|
@@ -72,9 +111,19 @@ export class ReadSyncClient {
|
|
|
72
111
|
for (const operation of outgoing) {
|
|
73
112
|
if (persistence.loadVersions)
|
|
74
113
|
assertReadIndexVersions(versions, await persistence.loadVersions(appId, profileId));
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
114
|
+
let receipt;
|
|
115
|
+
try {
|
|
116
|
+
receipt = await this.request("/api/social/reading", {
|
|
117
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...operation, appId, profileId }),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (!repairedLegacy && error?.status === 400
|
|
122
|
+
&& error.code === "INVALID_READING_CURSOR"
|
|
123
|
+
&& await this.repairLegacyDiscussion(operation, versions))
|
|
124
|
+
return this.run(true);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
78
127
|
if (receipt.operationId !== operation.operationId || receipt.accepted !== true
|
|
79
128
|
|| !Number.isSafeInteger(receipt.acceptedAt) || receipt.acceptedAt <= 0)
|
|
80
129
|
throw new Error("INVALID_READING_RECEIPT");
|
package/package.json
CHANGED