@pmate/chat-sdk 0.12.0-next.59 → 0.12.0-next.60
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 +28 -0
- package/lib/runtime/chatSession.d.ts +32 -2
- package/lib/runtime/chatSession.js +109 -11
- package/lib/runtime/createChat.js +1 -0
- package/lib/runtime/durableSender.d.ts +3 -1
- package/lib/runtime/durableSender.js +38 -9
- package/lib/runtime/groupClient.d.ts +76 -0
- package/lib/runtime/groupClient.js +325 -0
- package/lib/runtime/index.d.ts +1 -0
- package/lib/runtime/outboxSchema.js +1 -0
- package/lib/runtime/outgoingContent.d.ts +63 -25
- package/lib/runtime/outgoingContent.js +57 -9
- package/lib/runtime/responseCache.d.ts +19 -6
- package/lib/runtime/responseCache.js +86 -29
- package/lib/runtime/socialReadClient.d.ts +2 -1
- package/lib/runtime/socialReadClient.js +2 -2
- package/package.json +1 -1
|
@@ -30,6 +30,9 @@ export class IndexedDbResponseCache {
|
|
|
30
30
|
constructor(scope, options = {}) {
|
|
31
31
|
this.scope = scope;
|
|
32
32
|
this.options = options;
|
|
33
|
+
for (const [value, max] of [[options.recordBytes ?? 65536, 1048576], [options.entries ?? 256, 256], [options.batchRecords ?? 128, 128]])
|
|
34
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
35
|
+
throw new Error("INVALID_RESPONSE_CACHE_LIMIT");
|
|
33
36
|
this.db = new Dexie(options.name ?? "pmate-chat-response-windows", { indexedDB: options.indexedDB ?? globalThis.indexedDB, IDBKeyRange: globalThis.IDBKeyRange });
|
|
34
37
|
this.db.version(1).stores({ windows: "[scope+key],scope", epochs: "scope" });
|
|
35
38
|
this.rows = this.db.table("windows");
|
|
@@ -72,15 +75,30 @@ export class IndexedDbResponseCache {
|
|
|
72
75
|
return { epoch, cached: row?.epoch === epoch ? { value: row.value, time: row.time } : undefined };
|
|
73
76
|
});
|
|
74
77
|
}
|
|
75
|
-
|
|
78
|
+
/** Key-only callers can share one bounded metadata read transaction. */
|
|
79
|
+
async readMany(keys) {
|
|
80
|
+
if (this.closed)
|
|
81
|
+
throw new Error("RESPONSE_CACHE_CLOSED");
|
|
82
|
+
if (keys.length > 128)
|
|
83
|
+
throw new Error("RESPONSE_CACHE_SELECTION_TOO_LARGE");
|
|
84
|
+
await this.synchronizeAccess();
|
|
85
|
+
return this.db.transaction("r", this.rows, this.epochs, async () => {
|
|
86
|
+
const epoch = (await this.epochs.get(this.scope))?.epoch ?? 0, unique = [...new Set(keys)];
|
|
87
|
+
const rows = await this.rows.bulkGet(unique.map(key => [this.scope, key]));
|
|
88
|
+
return new Map(unique.map((key, index) => { const row = rows[index]; return [key, { epoch, cached: row?.epoch === epoch ? { value: row.value, time: row.time } : undefined }]; }));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
write(key, value, epoch, expectedRevision) {
|
|
76
92
|
if (this.closed)
|
|
77
93
|
return Promise.reject(new Error("RESPONSE_CACHE_CLOSED"));
|
|
94
|
+
if (expectedRevision !== undefined && !this.options.revision)
|
|
95
|
+
return Promise.reject(new Error("RESPONSE_CACHE_REVISION_UNAVAILABLE"));
|
|
78
96
|
const copy = structuredClone(value), bytes = new TextEncoder().encode(JSON.stringify(copy)).byteLength;
|
|
79
97
|
// Count in-flight data as well as queued data; requests cannot create an
|
|
80
98
|
// unbounded backlog while IndexedDB is blocked by another tab.
|
|
81
|
-
if (bytes > 65536 || this.pending.length >= 128 || this.pending.reduce((n, w) => n + w.row.bytes, 0) + bytes > 1048576)
|
|
99
|
+
if (bytes > (this.options.recordBytes ?? 65536) || this.pending.length >= 128 || this.pending.reduce((n, w) => n + w.row.bytes, 0) + bytes > 1048576)
|
|
82
100
|
return Promise.reject(new Error("RESPONSE_CACHE_BACKPRESSURE"));
|
|
83
|
-
const promise = new Promise((resolve, reject) => this.pending.push({ row: { scope: this.scope, key, epoch, time: Date.now(), bytes, value: copy }, resolve, reject }));
|
|
101
|
+
const promise = new Promise((resolve, reject) => this.pending.push({ row: { scope: this.scope, key, epoch, time: Date.now(), bytes, value: copy }, expectedRevision, resolve, reject }));
|
|
84
102
|
this.tasks.schedule("write", () => this.flush());
|
|
85
103
|
return promise;
|
|
86
104
|
}
|
|
@@ -122,35 +140,74 @@ export class IndexedDbResponseCache {
|
|
|
122
140
|
return this.flush();
|
|
123
141
|
return;
|
|
124
142
|
}
|
|
125
|
-
|
|
126
|
-
if (!batch.length)
|
|
143
|
+
if (!this.pending.length)
|
|
127
144
|
return;
|
|
128
145
|
const task = (async () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
await this.
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
let failed = false, failure;
|
|
147
|
+
// Each batch commits and resolves independently. A configured smaller
|
|
148
|
+
// batch still drains all accepted requests, including during close.
|
|
149
|
+
while (this.pending.length) {
|
|
150
|
+
const batch = this.pending.slice(0, this.options.batchRecords ?? 128);
|
|
151
|
+
try {
|
|
152
|
+
await this.synchronizeAccess();
|
|
153
|
+
const outcome = await this.db.transaction("rw", this.rows, this.epochs, async () => {
|
|
154
|
+
const epoch = (await this.epochs.get(this.scope))?.epoch ?? 0;
|
|
155
|
+
const valid = batch.filter(write => write.row.epoch === epoch);
|
|
156
|
+
const conditional = valid.filter(write => write.expectedRevision !== undefined);
|
|
157
|
+
const keys = [...new Set(conditional.map(write => write.row.key))];
|
|
158
|
+
const current = conditional.length ? await this.rows.bulkGet(keys.map(key => [this.scope, key])) : [];
|
|
159
|
+
const projected = new Map(keys.map((key, index) => [key, current[index]?.epoch === epoch ? current[index] : undefined]));
|
|
160
|
+
const accepted = new Set(), changed = new Set(), unique = new Map();
|
|
161
|
+
for (const write of valid) {
|
|
162
|
+
if (write.expectedRevision !== undefined) {
|
|
163
|
+
const previous = projected.get(write.row.key);
|
|
164
|
+
const revision = previous ? this.options.revision(previous.value) : null;
|
|
165
|
+
if (revision !== write.expectedRevision) {
|
|
166
|
+
// Another document may have already committed this exact
|
|
167
|
+
// revision; it is an idempotent success, never an overwrite.
|
|
168
|
+
if (revision === this.options.revision(write.row.value) && JSON.stringify(previous.value) === JSON.stringify(write.row.value))
|
|
169
|
+
accepted.add(write);
|
|
170
|
+
else
|
|
171
|
+
changed.add(write);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
unique.set(write.row.key, write.row);
|
|
176
|
+
projected.set(write.row.key, write.row);
|
|
177
|
+
accepted.add(write);
|
|
178
|
+
}
|
|
179
|
+
if (unique.size) {
|
|
180
|
+
await this.rows.bulkPut([...unique.values()]);
|
|
181
|
+
const stored = await this.rows.where("scope").equals(this.scope).toArray();
|
|
182
|
+
stored.sort((a, b) => b.time - a.time || a.key.localeCompare(b.key));
|
|
183
|
+
let bytes = 0;
|
|
184
|
+
const evicted = [];
|
|
185
|
+
stored.forEach((row, index) => { bytes += row.bytes; if (index >= (this.options.entries ?? 256) || bytes > 1048576)
|
|
186
|
+
evicted.push([this.scope, row.key]); });
|
|
187
|
+
if (evicted.length)
|
|
188
|
+
await this.rows.bulkDelete(evicted);
|
|
189
|
+
}
|
|
190
|
+
return { accepted, changed };
|
|
191
|
+
});
|
|
192
|
+
for (const write of batch) {
|
|
193
|
+
if (outcome.accepted.has(write))
|
|
194
|
+
write.resolve();
|
|
195
|
+
else
|
|
196
|
+
write.reject(new Error(outcome.changed.has(write) ? "RESPONSE_CACHE_CHANGED" : "RESPONSE_CACHE_INVALIDATED"));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
batch.forEach(write => write.reject(error));
|
|
201
|
+
if (!failed)
|
|
202
|
+
failure = error;
|
|
203
|
+
failed = true;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
this.pending.splice(0, batch.length);
|
|
207
|
+
}
|
|
153
208
|
}
|
|
209
|
+
if (failed)
|
|
210
|
+
throw failure;
|
|
154
211
|
})().finally(() => { if (this.running === task)
|
|
155
212
|
this.running = undefined; });
|
|
156
213
|
this.running = task;
|
|
@@ -22,6 +22,7 @@ export type SocialGroup = {
|
|
|
22
22
|
title: string;
|
|
23
23
|
avatar?: string | null;
|
|
24
24
|
revision: string;
|
|
25
|
+
versionNumber?: number;
|
|
25
26
|
};
|
|
26
27
|
export type SocialRelationship = {
|
|
27
28
|
appId: string;
|
|
@@ -62,7 +63,7 @@ export declare class SocialReadClient {
|
|
|
62
63
|
group(input: {
|
|
63
64
|
groupId: string;
|
|
64
65
|
teamId?: string;
|
|
65
|
-
}): Promise<SocialGroup>;
|
|
66
|
+
}, signal?: AbortSignal): Promise<SocialGroup>;
|
|
66
67
|
relationship(input: {
|
|
67
68
|
peerId: string;
|
|
68
69
|
teamId?: string;
|
|
@@ -43,9 +43,9 @@ export class SocialReadClient {
|
|
|
43
43
|
throw new SocialReadError(502, "INVALID_MESSAGE_LOOKUP_RESPONSE");
|
|
44
44
|
return value;
|
|
45
45
|
}
|
|
46
|
-
group(input) {
|
|
46
|
+
group(input, signal) {
|
|
47
47
|
this.requireTeam(input.teamId);
|
|
48
|
-
return this.request("group", { groupId: input.groupId, ...this.teamQuery(input.teamId) });
|
|
48
|
+
return this.request("group", { groupId: input.groupId, ...this.teamQuery(input.teamId) }, signal);
|
|
49
49
|
}
|
|
50
50
|
relationship(input) {
|
|
51
51
|
this.requireTeam(input.teamId);
|
package/package.json
CHANGED