@oxpulse/chat-sdk 0.1.0
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/CHANGELOG.md +19 -0
- package/LICENSE +661 -0
- package/README.md +142 -0
- package/dist/client.d.ts +98 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +639 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +79 -0
- package/dist/utils.js.map +1 -0
- package/package.json +61 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDKChatClient — standalone npm package implementation.
|
|
3
|
+
*
|
|
4
|
+
* This is a standalone copy of the HTTP client for use by third-party
|
|
5
|
+
* integrations that import @oxpulse/chat-sdk directly. The SvelteKit
|
|
6
|
+
* front-end uses web/src/lib/api/sdkChat.ts (same semantics, same wire
|
|
7
|
+
* protocol) — kept separate to avoid pulling in SvelteKit build tooling.
|
|
8
|
+
*
|
|
9
|
+
* W4 skeleton: full implementation mirrors web/src/lib/api/sdkChat.ts.
|
|
10
|
+
* Publishing to npmjs.org is deferred to W8 (embed widget wave).
|
|
11
|
+
*/
|
|
12
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
13
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
14
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
15
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
16
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
17
|
+
};
|
|
18
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
19
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
20
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
21
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
22
|
+
};
|
|
23
|
+
var _SDKChatClient_instances, _SDKChatClient_baseUrl, _SDKChatClient_jwt, _SDKChatClient_compression, _SDKChatClient_minCompressBytes, _SDKChatClient_dictHint, _SDKChatClient_ready, _SDKChatClient_encodeBody, _SDKChatClient_fetchSubscribeTicket;
|
|
24
|
+
import { ensureWireCodecReady, encodeHttpBody, decodeHttpBody, setDictLoader, setDictBaseUrl, asWireBytes, } from '@oxpulse/wire-codec';
|
|
25
|
+
import { SDKChatError } from './errors.js';
|
|
26
|
+
import { arrayBufferToBase64url, base64ToArrayBuffer, httpStatusToCode, backoffMs, dispatchTransient, } from './utils.js';
|
|
27
|
+
function dtoToRoom(dto) {
|
|
28
|
+
return {
|
|
29
|
+
appId: dto.app_id,
|
|
30
|
+
roomId: dto.room_id,
|
|
31
|
+
title: dto.title,
|
|
32
|
+
productRef: dto.product_ref,
|
|
33
|
+
createdBy: dto.created_by,
|
|
34
|
+
createdAt: dto.created_at,
|
|
35
|
+
archivedAt: dto.archived_at,
|
|
36
|
+
metadata: dto.metadata,
|
|
37
|
+
members: dto.members.map(dtoToMember),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function dtoToMember(dto) {
|
|
41
|
+
return {
|
|
42
|
+
appId: dto.app_id,
|
|
43
|
+
roomId: dto.room_id,
|
|
44
|
+
userId: dto.user_id,
|
|
45
|
+
role: dto.role,
|
|
46
|
+
joinedAt: dto.joined_at,
|
|
47
|
+
lastReadSeq: dto.last_read_seq,
|
|
48
|
+
active: dto.active,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// ─── Client ───────────────────────────────────────────────────────────────────
|
|
52
|
+
const DEFAULT_MIN_COMPRESS_BYTES = 256;
|
|
53
|
+
export class SDKChatClient {
|
|
54
|
+
constructor(opts) {
|
|
55
|
+
_SDKChatClient_instances.add(this);
|
|
56
|
+
_SDKChatClient_baseUrl.set(this, void 0);
|
|
57
|
+
_SDKChatClient_jwt.set(this, void 0);
|
|
58
|
+
_SDKChatClient_compression.set(this, void 0);
|
|
59
|
+
_SDKChatClient_minCompressBytes.set(this, void 0);
|
|
60
|
+
_SDKChatClient_dictHint.set(this, void 0);
|
|
61
|
+
_SDKChatClient_ready.set(this, null);
|
|
62
|
+
if (opts.jwt.startsWith('Bearer ')) {
|
|
63
|
+
throw new SDKChatError('invalid_args', 'jwt must not include the "Bearer " prefix');
|
|
64
|
+
}
|
|
65
|
+
__classPrivateFieldSet(this, _SDKChatClient_baseUrl, opts.baseUrl.replace(/\/$/, ''), "f");
|
|
66
|
+
__classPrivateFieldSet(this, _SDKChatClient_jwt, opts.jwt, "f");
|
|
67
|
+
__classPrivateFieldSet(this, _SDKChatClient_compression, opts.compression ?? 'none', "f");
|
|
68
|
+
__classPrivateFieldSet(this, _SDKChatClient_minCompressBytes, opts.minCompressBytes ?? DEFAULT_MIN_COMPRESS_BYTES, "f");
|
|
69
|
+
__classPrivateFieldSet(this, _SDKChatClient_dictHint, opts.dictHint ?? 'zstd-dict-ru-v1', "f");
|
|
70
|
+
if (__classPrivateFieldGet(this, _SDKChatClient_compression, "f") !== 'none') {
|
|
71
|
+
// Configure loader before ensureWireCodecReady so dicts are preloaded correctly.
|
|
72
|
+
if (opts.dictLoader !== undefined) {
|
|
73
|
+
setDictLoader(opts.dictLoader);
|
|
74
|
+
}
|
|
75
|
+
else if (opts.dictBaseUrl !== undefined) {
|
|
76
|
+
setDictBaseUrl(opts.dictBaseUrl);
|
|
77
|
+
}
|
|
78
|
+
// Kick off zstd init + dict preload lazily. Awaited before first send.
|
|
79
|
+
__classPrivateFieldSet(this, _SDKChatClient_ready, ensureWireCodecReady(), "f");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Encode a payload to the bytes that would be sent on the wire, without sending.
|
|
84
|
+
* Mirrors the encoding _encodeBody applies to POST /api/sdk/messages.
|
|
85
|
+
*/
|
|
86
|
+
async encodeEnvelope(payload) {
|
|
87
|
+
return __classPrivateFieldGet(this, _SDKChatClient_instances, "m", _SDKChatClient_encodeBody).call(this, payload);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Decode bytes received from the server back to the JSON payload.
|
|
91
|
+
* Server responses are always plain JSON; this also handles wire-codec
|
|
92
|
+
* compressed frames for completeness (round-trip with encodeEnvelope).
|
|
93
|
+
*
|
|
94
|
+
* Note: #compression governs OUTGOING encoding only. A server may send a
|
|
95
|
+
* compressed frame (0xC6/0xC7) regardless of the client's compression setting.
|
|
96
|
+
* decodeEnvelope checks the first byte directly and initializes zstd if needed.
|
|
97
|
+
*/
|
|
98
|
+
async decodeEnvelope(bytes) {
|
|
99
|
+
if (typeof bytes === 'string') {
|
|
100
|
+
return JSON.parse(bytes);
|
|
101
|
+
}
|
|
102
|
+
if (bytes.length === 0)
|
|
103
|
+
throw new SDKChatError('server_error', 'decodeEnvelope: empty input');
|
|
104
|
+
const first = bytes[0];
|
|
105
|
+
if (first === 0x7b || first === 0x5b) {
|
|
106
|
+
// Plain JSON bytes — no zstd needed.
|
|
107
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
108
|
+
}
|
|
109
|
+
// Compressed frame (0xC6/0xC7): ensure zstd is ready regardless of #compression setting.
|
|
110
|
+
// #compression controls outgoing encoding only; server may compress responses independently.
|
|
111
|
+
if (first === 0xc6 || first === 0xc7) {
|
|
112
|
+
if (__classPrivateFieldGet(this, _SDKChatClient_ready, "f") !== null) {
|
|
113
|
+
await __classPrivateFieldGet(this, _SDKChatClient_ready, "f");
|
|
114
|
+
__classPrivateFieldSet(this, _SDKChatClient_ready, null, "f");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// #compression is 'none' — zstd not pre-initialized; initialize on demand.
|
|
118
|
+
await ensureWireCodecReady();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return decodeHttpBody(asWireBytes(bytes));
|
|
122
|
+
}
|
|
123
|
+
async send(roomId, args) {
|
|
124
|
+
const msgId = args.msgId ?? crypto.randomUUID();
|
|
125
|
+
const payload = {
|
|
126
|
+
room_id: roomId,
|
|
127
|
+
msg_id: msgId,
|
|
128
|
+
sender_uid: args.senderUid,
|
|
129
|
+
sealed_b64: arrayBufferToBase64url(args.sealed),
|
|
130
|
+
};
|
|
131
|
+
if (args.threadRootMsgId !== undefined) {
|
|
132
|
+
payload['thread_root_msg_id'] = args.threadRootMsgId;
|
|
133
|
+
}
|
|
134
|
+
const body = await __classPrivateFieldGet(this, _SDKChatClient_instances, "m", _SDKChatClient_encodeBody).call(this, payload);
|
|
135
|
+
let resp;
|
|
136
|
+
try {
|
|
137
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/messages`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
'Content-Type': typeof body === 'string' ? 'application/json' : 'application/octet-stream',
|
|
141
|
+
Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}`,
|
|
142
|
+
},
|
|
143
|
+
body: body,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
throw new SDKChatError('network', String(err));
|
|
148
|
+
}
|
|
149
|
+
if (!resp.ok) {
|
|
150
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `send failed: HTTP ${resp.status}`, resp.status);
|
|
151
|
+
}
|
|
152
|
+
const json = (await resp.json());
|
|
153
|
+
return { seq: json.seq, msgId: json.msg_id };
|
|
154
|
+
}
|
|
155
|
+
async list(roomId, args = {}) {
|
|
156
|
+
const params = new URLSearchParams({
|
|
157
|
+
room_id: roomId,
|
|
158
|
+
after_seq: String(args.afterSeq ?? 0),
|
|
159
|
+
limit: String(args.limit ?? 50),
|
|
160
|
+
});
|
|
161
|
+
let resp;
|
|
162
|
+
try {
|
|
163
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/messages?${params}`, {
|
|
164
|
+
headers: { Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}` },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
throw new SDKChatError('network', String(err));
|
|
169
|
+
}
|
|
170
|
+
if (!resp.ok) {
|
|
171
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `list failed: HTTP ${resp.status}`, resp.status);
|
|
172
|
+
}
|
|
173
|
+
const json = (await resp.json());
|
|
174
|
+
return json.map((row) => ({
|
|
175
|
+
seq: row.seq,
|
|
176
|
+
msgId: row.msg_id,
|
|
177
|
+
senderUid: row.sender_uid,
|
|
178
|
+
sealed: base64ToArrayBuffer(row.sealed_b64),
|
|
179
|
+
createdAt: row.created_at,
|
|
180
|
+
threadRootMsgId: row.thread_root_msg_id ?? null,
|
|
181
|
+
productRef: row.product_ref ?? null,
|
|
182
|
+
productMeta: row.product_meta ?? null,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
subscribe(roomId, args) {
|
|
186
|
+
let destroyed = false;
|
|
187
|
+
let lastSeq = 0;
|
|
188
|
+
let es = null;
|
|
189
|
+
const attach = (ticket) => {
|
|
190
|
+
if (destroyed)
|
|
191
|
+
return;
|
|
192
|
+
const url = `${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/messages/subscribe?ticket=${encodeURIComponent(ticket)}&after_seq=${lastSeq}`;
|
|
193
|
+
es = new EventSource(url);
|
|
194
|
+
es.onmessage = (ev) => {
|
|
195
|
+
try {
|
|
196
|
+
const data = JSON.parse(ev.data);
|
|
197
|
+
// W6: dispatch transient events that arrive on the default channel.
|
|
198
|
+
if (data.type && data.type !== 'message') {
|
|
199
|
+
dispatchTransient(data.type, data, args);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
lastSeq = data.seq;
|
|
203
|
+
args.onMessage({
|
|
204
|
+
seq: data.seq,
|
|
205
|
+
msgId: data.msg_id,
|
|
206
|
+
senderUid: data.sender_uid,
|
|
207
|
+
sealed: base64ToArrayBuffer(data.sealed_b64),
|
|
208
|
+
createdAt: data.created_at,
|
|
209
|
+
threadRootMsgId: data.thread_root_msg_id ?? null,
|
|
210
|
+
productRef: data.product_ref ?? null,
|
|
211
|
+
productMeta: data.product_meta ?? null,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Malformed frame — ignore.
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
// W6: named SSE events for transient channel.
|
|
219
|
+
const transientTypes = ['typing', 'presence', 'read_receipt'];
|
|
220
|
+
for (const evType of transientTypes) {
|
|
221
|
+
es.addEventListener(evType, (ev) => {
|
|
222
|
+
const msgEv = ev;
|
|
223
|
+
try {
|
|
224
|
+
const data = JSON.parse(msgEv.data);
|
|
225
|
+
dispatchTransient(evType, data, args);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Malformed frame — ignore.
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
es.onerror = () => {
|
|
233
|
+
es?.close();
|
|
234
|
+
es = null;
|
|
235
|
+
if (destroyed)
|
|
236
|
+
return;
|
|
237
|
+
reconnect(0);
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
// dispatchTransient imported from utils.ts — single source of truth
|
|
241
|
+
// shared with web/src/lib/api/sdkChat.ts. See packages/chat-sdk/src/utils.ts.
|
|
242
|
+
let reconnectTimer = null;
|
|
243
|
+
const reportError = (err) => {
|
|
244
|
+
if (!args.onError)
|
|
245
|
+
return;
|
|
246
|
+
const sdkErr = err instanceof SDKChatError
|
|
247
|
+
? err
|
|
248
|
+
: new SDKChatError('network', `subscribe internal error: ${String(err)}`);
|
|
249
|
+
args.onError(sdkErr);
|
|
250
|
+
};
|
|
251
|
+
const reconnect = (attempt) => {
|
|
252
|
+
const delay = backoffMs(attempt);
|
|
253
|
+
reconnectTimer = setTimeout(async () => {
|
|
254
|
+
reconnectTimer = null;
|
|
255
|
+
if (destroyed)
|
|
256
|
+
return;
|
|
257
|
+
try {
|
|
258
|
+
const missed = await this.list(roomId, { afterSeq: lastSeq });
|
|
259
|
+
for (const row of missed) {
|
|
260
|
+
lastSeq = row.seq;
|
|
261
|
+
args.onMessage(row);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
// Surface replay failures to caller; will still retry on next reconnect.
|
|
266
|
+
reportError(err);
|
|
267
|
+
}
|
|
268
|
+
if (destroyed)
|
|
269
|
+
return;
|
|
270
|
+
let ticket;
|
|
271
|
+
try {
|
|
272
|
+
ticket = await __classPrivateFieldGet(this, _SDKChatClient_instances, "m", _SDKChatClient_fetchSubscribeTicket).call(this, roomId, lastSeq);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
// Retry with backoff on ticket failure; surface error.
|
|
276
|
+
reportError(err);
|
|
277
|
+
reconnect(attempt + 1);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
attach(ticket);
|
|
281
|
+
}, delay);
|
|
282
|
+
};
|
|
283
|
+
// Initial connection: fetch ticket then open EventSource.
|
|
284
|
+
(async () => {
|
|
285
|
+
if (destroyed)
|
|
286
|
+
return;
|
|
287
|
+
let ticket;
|
|
288
|
+
try {
|
|
289
|
+
ticket = await __classPrivateFieldGet(this, _SDKChatClient_instances, "m", _SDKChatClient_fetchSubscribeTicket).call(this, roomId, lastSeq);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
reportError(err);
|
|
293
|
+
reconnect(0);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
attach(ticket);
|
|
297
|
+
})();
|
|
298
|
+
return () => {
|
|
299
|
+
destroyed = true;
|
|
300
|
+
if (reconnectTimer !== null) {
|
|
301
|
+
clearTimeout(reconnectTimer);
|
|
302
|
+
reconnectTimer = null;
|
|
303
|
+
}
|
|
304
|
+
es?.close();
|
|
305
|
+
es = null;
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// ── W6: Typing / Presence / Read receipts ─────────────────────────────────
|
|
309
|
+
/**
|
|
310
|
+
* Broadcast a typing indicator for roomId.
|
|
311
|
+
* Wire-contract: POST /api/sdk/rooms/:room_id/typing
|
|
312
|
+
* Body: { ttl_secs? }
|
|
313
|
+
*/
|
|
314
|
+
async sendTyping(roomId, ttlSecs) {
|
|
315
|
+
const body = {};
|
|
316
|
+
if (ttlSecs !== undefined)
|
|
317
|
+
body['ttl_secs'] = ttlSecs;
|
|
318
|
+
let resp;
|
|
319
|
+
try {
|
|
320
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}/typing`, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: {
|
|
323
|
+
'Content-Type': 'application/json',
|
|
324
|
+
Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}`,
|
|
325
|
+
},
|
|
326
|
+
body: JSON.stringify(body),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
throw new SDKChatError('network', `sendTyping failed: ${String(err)}`);
|
|
331
|
+
}
|
|
332
|
+
if (!resp.ok) {
|
|
333
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `sendTyping failed: HTTP ${resp.status}`, resp.status);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Send a presence heartbeat for roomId.
|
|
338
|
+
* Wire-contract: POST /api/sdk/rooms/:room_id/presence
|
|
339
|
+
* Body: {}
|
|
340
|
+
*/
|
|
341
|
+
async sendPresence(roomId) {
|
|
342
|
+
let resp;
|
|
343
|
+
try {
|
|
344
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}/presence`, {
|
|
345
|
+
method: 'POST',
|
|
346
|
+
headers: {
|
|
347
|
+
'Content-Type': 'application/json',
|
|
348
|
+
Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}`,
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify({}),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
throw new SDKChatError('network', `sendPresence failed: ${String(err)}`);
|
|
355
|
+
}
|
|
356
|
+
if (!resp.ok) {
|
|
357
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `sendPresence failed: HTTP ${resp.status}`, resp.status);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Fetch presence snapshot for roomId.
|
|
362
|
+
* Wire-contract: GET /api/sdk/rooms/:room_id/presence
|
|
363
|
+
* Returns: Array<PresenceUser>
|
|
364
|
+
*/
|
|
365
|
+
async getPresence(roomId) {
|
|
366
|
+
let resp;
|
|
367
|
+
try {
|
|
368
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}/presence`, {
|
|
369
|
+
headers: { Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}` },
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
throw new SDKChatError('network', `getPresence failed: ${String(err)}`);
|
|
374
|
+
}
|
|
375
|
+
if (!resp.ok) {
|
|
376
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `getPresence failed: HTTP ${resp.status}`, resp.status);
|
|
377
|
+
}
|
|
378
|
+
const arr = (await resp.json());
|
|
379
|
+
// Wire-contract assertion: each entry must have user_id.
|
|
380
|
+
return arr.map((entry) => {
|
|
381
|
+
if (!Object.prototype.hasOwnProperty.call(entry, 'user_id')) {
|
|
382
|
+
throw new SDKChatError('server_error', 'getPresence: entry missing user_id');
|
|
383
|
+
}
|
|
384
|
+
return { userId: entry.user_id, lastSeenAt: entry.last_seen_at };
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Mark messages up to seq as read (monotonic — cannot regress).
|
|
389
|
+
* Wire-contract: POST /api/sdk/rooms/:room_id/read?seq=N
|
|
390
|
+
*/
|
|
391
|
+
async markRead(roomId, seq) {
|
|
392
|
+
let resp;
|
|
393
|
+
try {
|
|
394
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}/read?seq=${seq}`, {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers: { Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}` },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
throw new SDKChatError('network', `markRead failed: ${String(err)}`);
|
|
401
|
+
}
|
|
402
|
+
if (!resp.ok) {
|
|
403
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `markRead failed: HTTP ${resp.status}`, resp.status);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// ── W5: Room management ────────────────────────────────────────────────────
|
|
407
|
+
/**
|
|
408
|
+
* Create a room. Idempotent per roomId.
|
|
409
|
+
* Wire-contract: POST /api/sdk/rooms
|
|
410
|
+
*/
|
|
411
|
+
async createRoom(args = {}) {
|
|
412
|
+
const body = {};
|
|
413
|
+
if (args.roomId !== undefined)
|
|
414
|
+
body['room_id'] = args.roomId;
|
|
415
|
+
if (args.title !== undefined)
|
|
416
|
+
body['title'] = args.title;
|
|
417
|
+
if (args.productRef !== undefined)
|
|
418
|
+
body['product_ref'] = args.productRef;
|
|
419
|
+
if (args.metadata !== undefined)
|
|
420
|
+
body['metadata'] = args.metadata;
|
|
421
|
+
if (args.initialMembers !== undefined) {
|
|
422
|
+
body['initial_members'] = args.initialMembers.map((m) => ({
|
|
423
|
+
user_id: m.userId,
|
|
424
|
+
role: m.role,
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
let resp;
|
|
428
|
+
try {
|
|
429
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms`, {
|
|
430
|
+
method: 'POST',
|
|
431
|
+
headers: {
|
|
432
|
+
'Content-Type': 'application/json',
|
|
433
|
+
Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}`,
|
|
434
|
+
},
|
|
435
|
+
body: JSON.stringify(body),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
throw new SDKChatError('network', `createRoom failed: ${String(err)}`);
|
|
440
|
+
}
|
|
441
|
+
if (!resp.ok) {
|
|
442
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `createRoom failed: HTTP ${resp.status}`, resp.status);
|
|
443
|
+
}
|
|
444
|
+
const dto = (await resp.json());
|
|
445
|
+
// Wire-contract assertion: room_id must be present.
|
|
446
|
+
if (!Object.prototype.hasOwnProperty.call(dto, 'room_id')) {
|
|
447
|
+
throw new SDKChatError('server_error', 'createRoom: response missing room_id');
|
|
448
|
+
}
|
|
449
|
+
return dtoToRoom(dto);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Fetch room metadata + active members.
|
|
453
|
+
* Wire-contract: GET /api/sdk/rooms/:room_id
|
|
454
|
+
*/
|
|
455
|
+
async getRoom(roomId) {
|
|
456
|
+
let resp;
|
|
457
|
+
try {
|
|
458
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}`, {
|
|
459
|
+
headers: { Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}` },
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
throw new SDKChatError('network', `getRoom failed: ${String(err)}`);
|
|
464
|
+
}
|
|
465
|
+
if (!resp.ok) {
|
|
466
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `getRoom failed: HTTP ${resp.status}`, resp.status);
|
|
467
|
+
}
|
|
468
|
+
const dto = (await resp.json());
|
|
469
|
+
if (!Object.prototype.hasOwnProperty.call(dto, 'room_id')) {
|
|
470
|
+
throw new SDKChatError('server_error', 'getRoom: response missing room_id');
|
|
471
|
+
}
|
|
472
|
+
return dtoToRoom(dto);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Update room title / metadata. Owner-only.
|
|
476
|
+
* Wire-contract: PATCH /api/sdk/rooms/:room_id
|
|
477
|
+
*/
|
|
478
|
+
async updateRoom(roomId, args) {
|
|
479
|
+
let resp;
|
|
480
|
+
try {
|
|
481
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}`, {
|
|
482
|
+
method: 'PATCH',
|
|
483
|
+
headers: {
|
|
484
|
+
'Content-Type': 'application/json',
|
|
485
|
+
Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}`,
|
|
486
|
+
},
|
|
487
|
+
body: JSON.stringify(args),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
throw new SDKChatError('network', `updateRoom failed: ${String(err)}`);
|
|
492
|
+
}
|
|
493
|
+
if (!resp.ok) {
|
|
494
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `updateRoom failed: HTTP ${resp.status}`, resp.status);
|
|
495
|
+
}
|
|
496
|
+
const dto = (await resp.json());
|
|
497
|
+
if (!Object.prototype.hasOwnProperty.call(dto, 'room_id')) {
|
|
498
|
+
throw new SDKChatError('server_error', 'updateRoom: response missing room_id');
|
|
499
|
+
}
|
|
500
|
+
return dtoToRoom(dto);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* List active members of a room (fetches room and returns members).
|
|
504
|
+
*/
|
|
505
|
+
async listMembers(roomId) {
|
|
506
|
+
const room = await this.getRoom(roomId);
|
|
507
|
+
return room.members;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Add a user to a room.
|
|
511
|
+
* Wire-contract: POST /api/sdk/rooms/:room_id/members
|
|
512
|
+
* Body: { user_id, role? }
|
|
513
|
+
*/
|
|
514
|
+
async addMember(roomId, userId, role = 'member') {
|
|
515
|
+
const body = { user_id: userId, role };
|
|
516
|
+
let resp;
|
|
517
|
+
try {
|
|
518
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}/members`, {
|
|
519
|
+
method: 'POST',
|
|
520
|
+
headers: {
|
|
521
|
+
'Content-Type': 'application/json',
|
|
522
|
+
Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}`,
|
|
523
|
+
},
|
|
524
|
+
body: JSON.stringify(body),
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
throw new SDKChatError('network', `addMember failed: ${String(err)}`);
|
|
529
|
+
}
|
|
530
|
+
if (!resp.ok) {
|
|
531
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `addMember failed: HTTP ${resp.status}`, resp.status);
|
|
532
|
+
}
|
|
533
|
+
const dto = (await resp.json());
|
|
534
|
+
// Wire-contract assertion: user_id must be present.
|
|
535
|
+
if (!Object.prototype.hasOwnProperty.call(dto, 'user_id')) {
|
|
536
|
+
throw new SDKChatError('server_error', 'addMember: response missing user_id');
|
|
537
|
+
}
|
|
538
|
+
return dtoToMember(dto);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Remove a user from a room (soft-delete).
|
|
542
|
+
* Wire-contract: DELETE /api/sdk/rooms/:room_id/members/:user_id
|
|
543
|
+
*/
|
|
544
|
+
async removeMember(roomId, userId) {
|
|
545
|
+
let resp;
|
|
546
|
+
try {
|
|
547
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}/members/${encodeURIComponent(userId)}`, {
|
|
548
|
+
method: 'DELETE',
|
|
549
|
+
headers: { Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}` },
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
throw new SDKChatError('network', `removeMember failed: ${String(err)}`);
|
|
554
|
+
}
|
|
555
|
+
if (!resp.ok) {
|
|
556
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `removeMember failed: HTTP ${resp.status}`, resp.status);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// ── W7: Thread view ────────────────────────────────────────────────────────
|
|
560
|
+
/**
|
|
561
|
+
* Fetch all reply messages in a thread.
|
|
562
|
+
* Wire-contract: GET /api/sdk/rooms/:room_id/threads/:root_msg_id
|
|
563
|
+
* Returns: Array<MessageRow> sorted by seq ascending (server-side ORDER BY seq).
|
|
564
|
+
* Requires scope: chat:read:<room_id>.
|
|
565
|
+
*/
|
|
566
|
+
async getThread(roomId, rootMsgId) {
|
|
567
|
+
let resp;
|
|
568
|
+
try {
|
|
569
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/rooms/${encodeURIComponent(roomId)}/threads/${encodeURIComponent(rootMsgId)}`, {
|
|
570
|
+
headers: { Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}` },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
throw new SDKChatError('network', `getThread failed: ${String(err)}`);
|
|
575
|
+
}
|
|
576
|
+
if (!resp.ok) {
|
|
577
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `getThread failed: HTTP ${resp.status}`, resp.status);
|
|
578
|
+
}
|
|
579
|
+
const json = (await resp.json());
|
|
580
|
+
return json.map((row) => ({
|
|
581
|
+
seq: row.seq,
|
|
582
|
+
msgId: row.msg_id,
|
|
583
|
+
senderUid: row.sender_uid,
|
|
584
|
+
sealed: base64ToArrayBuffer(row.sealed_b64),
|
|
585
|
+
createdAt: row.created_at,
|
|
586
|
+
threadRootMsgId: row.thread_root_msg_id ?? null,
|
|
587
|
+
productRef: row.product_ref ?? null,
|
|
588
|
+
productMeta: row.product_meta ?? null,
|
|
589
|
+
}));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
_SDKChatClient_baseUrl = new WeakMap(), _SDKChatClient_jwt = new WeakMap(), _SDKChatClient_compression = new WeakMap(), _SDKChatClient_minCompressBytes = new WeakMap(), _SDKChatClient_dictHint = new WeakMap(), _SDKChatClient_ready = new WeakMap(), _SDKChatClient_instances = new WeakSet(), _SDKChatClient_encodeBody =
|
|
593
|
+
/** Encode payload bytes for POST /api/sdk/messages per the configured compression mode.
|
|
594
|
+
* Returns a string for plain JSON (Content-Type: application/json) or
|
|
595
|
+
* Uint8Array for compressed frames (Content-Type: application/octet-stream). */
|
|
596
|
+
async function _SDKChatClient_encodeBody(payload) {
|
|
597
|
+
if (__classPrivateFieldGet(this, _SDKChatClient_compression, "f") === 'none') {
|
|
598
|
+
return JSON.stringify(payload);
|
|
599
|
+
}
|
|
600
|
+
// Ensure zstd is ready before first compressed send.
|
|
601
|
+
if (__classPrivateFieldGet(this, _SDKChatClient_ready, "f") !== null) {
|
|
602
|
+
await __classPrivateFieldGet(this, _SDKChatClient_ready, "f");
|
|
603
|
+
__classPrivateFieldSet(this, _SDKChatClient_ready, null, "f");
|
|
604
|
+
}
|
|
605
|
+
const jsonStr = JSON.stringify(payload);
|
|
606
|
+
const enc = new TextEncoder();
|
|
607
|
+
const jsonBytes = enc.encode(jsonStr);
|
|
608
|
+
if (jsonBytes.length < __classPrivateFieldGet(this, _SDKChatClient_minCompressBytes, "f")) {
|
|
609
|
+
// Below threshold — fall back to plain JSON regardless of mode.
|
|
610
|
+
return jsonStr;
|
|
611
|
+
}
|
|
612
|
+
if (__classPrivateFieldGet(this, _SDKChatClient_compression, "f") === 'dict') {
|
|
613
|
+
// encodeHttpBody falls back to dictless 0xC6 automatically if dict not loaded.
|
|
614
|
+
return encodeHttpBody(jsonBytes, __classPrivateFieldGet(this, _SDKChatClient_dictHint, "f"));
|
|
615
|
+
}
|
|
616
|
+
// 'auto': dictless 0xC6
|
|
617
|
+
return encodeHttpBody(jsonBytes);
|
|
618
|
+
}, _SDKChatClient_fetchSubscribeTicket = async function _SDKChatClient_fetchSubscribeTicket(roomId, afterSeq) {
|
|
619
|
+
let resp;
|
|
620
|
+
try {
|
|
621
|
+
resp = await fetch(`${__classPrivateFieldGet(this, _SDKChatClient_baseUrl, "f")}/api/sdk/messages/subscribe-ticket`, {
|
|
622
|
+
method: 'POST',
|
|
623
|
+
headers: {
|
|
624
|
+
'Content-Type': 'application/json',
|
|
625
|
+
Authorization: `Bearer ${__classPrivateFieldGet(this, _SDKChatClient_jwt, "f")}`,
|
|
626
|
+
},
|
|
627
|
+
body: JSON.stringify({ room_id: roomId, after_seq: afterSeq }),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
throw new SDKChatError('network', `subscribe-ticket fetch failed: ${String(err)}`);
|
|
632
|
+
}
|
|
633
|
+
if (!resp.ok) {
|
|
634
|
+
throw new SDKChatError(httpStatusToCode(resp.status), `subscribe-ticket HTTP ${resp.status}`, resp.status);
|
|
635
|
+
}
|
|
636
|
+
const body = (await resp.json());
|
|
637
|
+
return body.ticket;
|
|
638
|
+
};
|
|
639
|
+
//# sourceMappingURL=client.js.map
|