@inline-chat/realtime-sdk 0.0.6 → 0.0.8

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.
@@ -65,7 +65,7 @@ export declare class ProtocolClient {
65
65
  private trySendPendingRpcRequest;
66
66
  }
67
67
  export declare class ProtocolClientError extends Error {
68
- constructor(code: "not-authorized" | "not-connected" | "rpc-error" | "stopped" | "timeout", details?: {
68
+ constructor(code: "not-authorized" | "not-connected" | "rpc-error" | "stopped" | "timeout" | "invalid-rpc-method", details?: {
69
69
  code?: number;
70
70
  message?: string;
71
71
  });
@@ -3,6 +3,11 @@ import { AsyncChannel } from "../utils/async-channel.js";
3
3
  import { PingPongService } from "./ping-pong.js";
4
4
  const emptyRpcInput = { oneofKind: undefined };
5
5
  const defaultRpcTimeoutMs = 30_000;
6
+ const assertValidRpcMethod = (method) => {
7
+ if (typeof method !== "number" || !Number.isInteger(method) || method <= 0) {
8
+ throw new ProtocolClientError("invalid-rpc-method", { message: `Invalid rpc method: ${String(method)}` });
9
+ }
10
+ };
6
11
  export class ProtocolClient {
7
12
  events = new AsyncChannel();
8
13
  transport;
@@ -52,6 +57,7 @@ export class ProtocolClient {
52
57
  }
53
58
  async sendRpc(method, input = emptyRpcInput) {
54
59
  this.ensureOpenForRpc();
60
+ assertValidRpcMethod(method);
55
61
  const message = this.wrapMessage({
56
62
  oneofKind: "rpcCall",
57
63
  rpcCall: { method, input },
@@ -60,6 +66,7 @@ export class ProtocolClient {
60
66
  return message.id;
61
67
  }
62
68
  async callRpc(method, input = emptyRpcInput, options) {
69
+ assertValidRpcMethod(method);
63
70
  const message = this.wrapMessage({
64
71
  oneofKind: "rpcCall",
65
72
  rpcCall: { method, input },
@@ -175,18 +175,28 @@ export class InlineSdkClient {
175
175
  }
176
176
  const peerId = this.inputPeerFromTarget(params, "sendMessage");
177
177
  const media = params.media != null ? toInputMedia(params.media) : undefined;
178
- const result = await this.invoke(Method.SEND_MESSAGE, {
179
- oneofKind: "sendMessage",
180
- sendMessage: {
181
- peerId,
182
- ...(hasText ? { message: params.text } : {}),
183
- ...(media != null ? { media } : {}),
184
- ...(params.replyToMsgId != null ? { replyToMsgId: asInlineId(params.replyToMsgId, "replyToMsgId") } : {}),
185
- ...(params.parseMarkdown != null ? { parseMarkdown: params.parseMarkdown } : {}),
186
- ...(params.entities != null ? { entities: params.entities } : {}),
187
- ...(params.sendMode === "silent" ? { sendMode: MessageSendMode.MODE_SILENT } : {}),
188
- },
189
- });
178
+ let result;
179
+ try {
180
+ result = await this.invoke(Method.SEND_MESSAGE, {
181
+ oneofKind: "sendMessage",
182
+ sendMessage: {
183
+ peerId,
184
+ ...(hasText ? { message: params.text } : {}),
185
+ ...(media != null ? { media } : {}),
186
+ ...(params.replyToMsgId != null ? { replyToMsgId: asInlineId(params.replyToMsgId, "replyToMsgId") } : {}),
187
+ ...(params.parseMarkdown != null ? { parseMarkdown: params.parseMarkdown } : {}),
188
+ ...(params.entities != null ? { entities: params.entities } : {}),
189
+ ...(params.sendMode === "silent" ? { sendMode: MessageSendMode.MODE_SILENT } : {}),
190
+ },
191
+ });
192
+ }
193
+ catch (error) {
194
+ const target = "chatId" in params ? `chat:${String(params.chatId)}` : `user:${String(params.userId)}`;
195
+ const mediaKind = params.media?.kind ?? "none";
196
+ const textLen = hasText ? params.text.length : 0;
197
+ const detail = extractErrorMessage(error);
198
+ throw new Error(`sendMessage: request failed (${detail}; target=${target}; media=${mediaKind}; textLen=${textLen}; replyTo=${params.replyToMsgId != null ? String(params.replyToMsgId) : "none"})`, { cause: error });
199
+ }
190
200
  const messageId = extractFirstMessageId(result.sendMessage.updates);
191
201
  return { messageId };
192
202
  }
@@ -196,9 +206,14 @@ export class InlineSdkClient {
196
206
  const fileName = normalizeUploadFileName(params.fileName, params.type);
197
207
  const fileContentType = resolveUploadContentType(params.type, params.contentType);
198
208
  form.set("file", toUploadMultipartFile(params.file, fileName, fileContentType), fileName);
209
+ const fileSize = getBinaryInputSize(params.file);
210
+ let thumbnailName;
211
+ let thumbnailContentType;
212
+ let thumbnailSize;
199
213
  if (params.thumbnail != null) {
200
- const thumbnailName = normalizeUploadFileName(params.thumbnailFileName, "photo");
201
- const thumbnailContentType = resolveUploadContentType("photo", params.thumbnailContentType);
214
+ thumbnailName = normalizeUploadFileName(params.thumbnailFileName, "photo");
215
+ thumbnailContentType = resolveUploadContentType("photo", params.thumbnailContentType);
216
+ thumbnailSize = getBinaryInputSize(params.thumbnail);
202
217
  form.set("thumbnail", toUploadMultipartFile(params.thumbnail, thumbnailName, thumbnailContentType), thumbnailName);
203
218
  }
204
219
  if (params.type === "video") {
@@ -209,29 +224,56 @@ export class InlineSdkClient {
209
224
  form.set("height", String(height));
210
225
  form.set("duration", String(duration));
211
226
  }
212
- const response = await this.fetchImpl(resolveUploadFileUrl(this.httpBaseUrl), {
213
- method: "POST",
214
- headers: {
215
- authorization: `Bearer ${this.options.token}`,
216
- },
217
- body: form,
227
+ const uploadUrl = resolveUploadFileUrl(this.httpBaseUrl);
228
+ const requestContext = describeUploadContext({
229
+ type: params.type,
230
+ fileName,
231
+ fileContentType,
232
+ fileSize,
233
+ ...(thumbnailName ? { thumbnailName } : {}),
234
+ ...(thumbnailContentType ? { thumbnailContentType } : {}),
235
+ ...(thumbnailSize != null ? { thumbnailSize } : {}),
236
+ ...(params.type === "video"
237
+ ? {
238
+ width: params.width ?? defaultVideoWidth,
239
+ height: params.height ?? defaultVideoHeight,
240
+ duration: params.duration ?? defaultVideoDuration,
241
+ }
242
+ : {}),
243
+ uploadUrl: uploadUrl.toString(),
218
244
  });
245
+ let response;
246
+ try {
247
+ response = await this.fetchImpl(uploadUrl, {
248
+ method: "POST",
249
+ headers: {
250
+ authorization: `Bearer ${this.options.token}`,
251
+ },
252
+ body: form,
253
+ });
254
+ }
255
+ catch (error) {
256
+ const detail = extractErrorMessage(error);
257
+ throw new Error(`uploadFile: network request failed (${detail}; ${requestContext})`, {
258
+ cause: error,
259
+ });
260
+ }
219
261
  const payload = await parseJsonResponse(response);
220
262
  if (!response.ok) {
221
263
  const detail = describeUploadFailure(payload);
222
- throw new Error(`uploadFile: request failed with status ${response.status}${detail ? ` (${detail})` : ""}`);
264
+ throw new Error(`uploadFile: request failed with status ${response.status}${detail ? ` (${detail})` : ""}; ${requestContext}`);
223
265
  }
224
266
  if (!isRecord(payload) || payload.ok !== true) {
225
267
  const detail = describeUploadFailure(payload);
226
- throw new Error(`uploadFile: API error${detail ? ` (${detail})` : ""}`);
268
+ throw new Error(`uploadFile: API error${detail ? ` (${detail})` : ""}; ${requestContext}`);
227
269
  }
228
270
  const result = payload.result;
229
271
  if (!isRecord(result)) {
230
- throw new Error("uploadFile: malformed success payload");
272
+ throw new Error(`uploadFile: malformed success payload; ${requestContext}`);
231
273
  }
232
274
  const fileUniqueId = typeof result.fileUniqueId === "string" ? result.fileUniqueId.trim() : "";
233
275
  if (!fileUniqueId) {
234
- throw new Error("uploadFile: response missing fileUniqueId");
276
+ throw new Error(`uploadFile: response missing fileUniqueId; ${requestContext}`);
235
277
  }
236
278
  const photoId = parseOptionalBigInt(result.photoId, "photoId");
237
279
  const videoId = parseOptionalBigInt(result.videoId, "videoId");
@@ -699,6 +741,40 @@ function sanitizeUploadFileName(raw) {
699
741
  const noQuery = leaf.split(/[?#]/, 1)[0] ?? leaf;
700
742
  return noQuery.trim();
701
743
  }
744
+ function getBinaryInputSize(input) {
745
+ if (input instanceof Blob)
746
+ return input.size;
747
+ if (input instanceof Uint8Array)
748
+ return input.byteLength;
749
+ return input.byteLength;
750
+ }
751
+ function describeUploadContext(params) {
752
+ const parts = [
753
+ `type=${params.type}`,
754
+ `fileName=${params.fileName}`,
755
+ `fileContentType=${params.fileContentType}`,
756
+ `fileSize=${params.fileSize}`,
757
+ `uploadUrl=${params.uploadUrl}`,
758
+ ];
759
+ if (params.thumbnailName)
760
+ parts.push(`thumbnailName=${params.thumbnailName}`);
761
+ if (params.thumbnailContentType)
762
+ parts.push(`thumbnailContentType=${params.thumbnailContentType}`);
763
+ if (params.thumbnailSize != null)
764
+ parts.push(`thumbnailSize=${params.thumbnailSize}`);
765
+ if (params.width != null)
766
+ parts.push(`width=${params.width}`);
767
+ if (params.height != null)
768
+ parts.push(`height=${params.height}`);
769
+ if (params.duration != null)
770
+ parts.push(`duration=${params.duration}`);
771
+ return parts.join(", ");
772
+ }
773
+ function extractErrorMessage(error) {
774
+ if (error instanceof Error)
775
+ return error.message;
776
+ return String(error);
777
+ }
702
778
  function normalizePositiveInt(value, field) {
703
779
  if (value == null)
704
780
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inline-chat/realtime-sdk",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
@@ -20,7 +20,7 @@
20
20
  "test": "vitest run --coverage"
21
21
  },
22
22
  "dependencies": {
23
- "@inline-chat/protocol": "^0.0.2",
23
+ "@inline-chat/protocol": "^0.0.3",
24
24
  "ws": "^8.18.3"
25
25
  },
26
26
  "devDependencies": {