@inline-chat/realtime-sdk 0.0.1 → 0.0.3

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { InlineSdkClient } from "./sdk/inline-sdk-client.js";
2
- export type { InlineSdkClientOptions, InlineSdkState, InlineSdkStateStore, InlineInboundEvent, RpcInputForMethod, RpcResultForMethod, } from "./sdk/types.js";
2
+ export type { InlineSdkClientOptions, InlineSdkSendMessageMedia, InlineSdkSendMessageParams, InlineSdkState, InlineSdkStateStore, InlineSdkUploadFileParams, InlineSdkUploadFileResult, InlineSdkUploadFileType, InlineInboundEvent, RpcInputForMethod, RpcResultForMethod, } from "./sdk/types.js";
3
3
  export type { InlineSdkLogger } from "./sdk/logger.js";
4
4
  export { JsonFileStateStore } from "./state/json-file-state-store.js";
5
5
  export { serializeStateV1, deserializeStateV1 } from "./state/serde.js";
@@ -9,6 +9,7 @@ export type ProtocolClientOptions = {
9
9
  transport: Transport;
10
10
  getConnectionInit: () => ConnectionInit | null;
11
11
  logger?: InlineSdkLogger;
12
+ defaultRpcTimeoutMs?: number | null;
12
13
  };
13
14
  export declare class ProtocolClient {
14
15
  readonly events: AsyncChannel<ClientEvent>;
@@ -17,7 +18,8 @@ export declare class ProtocolClient {
17
18
  state: ClientState;
18
19
  private readonly log;
19
20
  private readonly getConnectionInit;
20
- private rpcContinuations;
21
+ private readonly defaultRpcTimeoutMs;
22
+ private pendingRpcRequests;
21
23
  private seq;
22
24
  private lastTimestamp;
23
25
  private sequence;
@@ -35,7 +37,7 @@ export declare class ProtocolClient {
35
37
  }): Promise<void>;
36
38
  sendRpc(method: Method, input?: RpcCall["input"]): Promise<bigint>;
37
39
  callRpc(method: Method, input?: RpcCall["input"], options?: {
38
- timeoutMs?: number;
40
+ timeoutMs?: number | null;
39
41
  }): Promise<RpcResult["result"]>;
40
42
  private startListeners;
41
43
  private handleTransportMessage;
@@ -53,10 +55,14 @@ export declare class ProtocolClient {
53
55
  private generateId;
54
56
  private currentTimestamp;
55
57
  private completeRpcResult;
58
+ private ensureOpenForRpc;
56
59
  private completeRpcError;
57
- private failRpcContinuation;
58
- private getAndRemoveRpcContinuation;
59
- private cancelAllRpcContinuations;
60
+ private failPendingRpcRequest;
61
+ private getAndRemovePendingRpcRequest;
62
+ private cancelAllPendingRpcRequests;
63
+ private resolveRpcTimeoutMs;
64
+ private resendPendingRpcRequests;
65
+ private trySendPendingRpcRequest;
60
66
  }
61
67
  export declare class ProtocolClientError extends Error {
62
68
  constructor(code: "not-authorized" | "not-connected" | "rpc-error" | "stopped" | "timeout", details?: {
@@ -2,6 +2,7 @@ import { ClientMessage, ServerProtocolMessage } from "@inline-chat/protocol/core
2
2
  import { AsyncChannel } from "../utils/async-channel.js";
3
3
  import { PingPongService } from "./ping-pong.js";
4
4
  const emptyRpcInput = { oneofKind: undefined };
5
+ const defaultRpcTimeoutMs = 30_000;
5
6
  export class ProtocolClient {
6
7
  events = new AsyncChannel();
7
8
  transport;
@@ -9,7 +10,8 @@ export class ProtocolClient {
9
10
  state = "connecting";
10
11
  log;
11
12
  getConnectionInit;
12
- rpcContinuations = new Map();
13
+ defaultRpcTimeoutMs;
14
+ pendingRpcRequests = new Map();
13
15
  seq = 0;
14
16
  lastTimestamp = 0;
15
17
  sequence = 0;
@@ -22,6 +24,7 @@ export class ProtocolClient {
22
24
  this.transport = options.transport;
23
25
  this.log = options.logger ?? {};
24
26
  this.getConnectionInit = options.getConnectionInit;
27
+ this.defaultRpcTimeoutMs = normalizeRpcTimeoutMs(options.defaultRpcTimeoutMs, defaultRpcTimeoutMs);
25
28
  this.pingPong = new PingPongService({ logger: this.log });
26
29
  this.pingPong.configure(this);
27
30
  this.startListeners();
@@ -48,6 +51,7 @@ export class ProtocolClient {
48
51
  await this.transport.reconnect({ skipDelay: options?.skipDelay });
49
52
  }
50
53
  async sendRpc(method, input = emptyRpcInput) {
54
+ this.ensureOpenForRpc();
51
55
  const message = this.wrapMessage({
52
56
  oneofKind: "rpcCall",
53
57
  rpcCall: { method, input },
@@ -61,17 +65,20 @@ export class ProtocolClient {
61
65
  rpcCall: { method, input },
62
66
  });
63
67
  return await new Promise((resolve, reject) => {
64
- const continuation = { resolve, reject };
65
- this.rpcContinuations.set(message.id, continuation);
66
- void this.transport.send(message).catch((error) => {
67
- this.failRpcContinuation(message.id, error instanceof Error ? error : new Error("send-failed"));
68
- });
69
- const timeoutMs = options?.timeoutMs ?? 15_000;
70
- if (timeoutMs > 0) {
71
- continuation.timeout = setTimeout(() => {
72
- this.failRpcContinuation(message.id, new ProtocolClientError("timeout"));
73
- }, timeoutMs);
68
+ const pending = {
69
+ message,
70
+ resolve,
71
+ reject,
72
+ timeoutMs: this.resolveRpcTimeoutMs(options?.timeoutMs),
73
+ sending: false,
74
+ };
75
+ this.pendingRpcRequests.set(message.id, pending);
76
+ if (pending.timeoutMs !== null) {
77
+ pending.timeout = setTimeout(() => {
78
+ this.failPendingRpcRequest(message.id, new ProtocolClientError("timeout"));
79
+ }, pending.timeoutMs);
74
80
  }
81
+ this.trySendPendingRpcRequest(message.id);
75
82
  });
76
83
  }
77
84
  async startListeners() {
@@ -168,6 +175,7 @@ export class ProtocolClient {
168
175
  }
169
176
  this.connectionAttemptNo = 0;
170
177
  this.pingPong.start();
178
+ this.resendPendingRpcRequests();
171
179
  }
172
180
  async connecting() {
173
181
  this.state = "connecting";
@@ -176,7 +184,7 @@ export class ProtocolClient {
176
184
  async reset() {
177
185
  this.pingPong.stop();
178
186
  this.stopAuthenticationTimeout();
179
- this.cancelAllRpcContinuations(new ProtocolClientError("stopped"));
187
+ this.cancelAllPendingRpcRequests(new ProtocolClientError("stopped"));
180
188
  this.state = "connecting";
181
189
  }
182
190
  startAuthenticationTimeout() {
@@ -195,8 +203,8 @@ export class ProtocolClient {
195
203
  }
196
204
  handleClientFailure() {
197
205
  this.pingPong.stop();
198
- this.cancelAllRpcContinuations(new ProtocolClientError("not-connected"));
199
206
  this.stopAuthenticationTimeout();
207
+ this.state = "connecting";
200
208
  if (this.reconnectionTimer) {
201
209
  clearTimeout(this.reconnectionTimer);
202
210
  }
@@ -239,36 +247,80 @@ export class ProtocolClient {
239
247
  return Math.floor(Date.now() / 1000) - this.epochSeconds;
240
248
  }
241
249
  completeRpcResult(msgId, rpcResult) {
242
- const continuation = this.getAndRemoveRpcContinuation(msgId);
243
- continuation?.resolve(rpcResult);
250
+ const pending = this.getAndRemovePendingRpcRequest(msgId);
251
+ pending?.resolve(rpcResult);
252
+ }
253
+ ensureOpenForRpc() {
254
+ if (this.state !== "open") {
255
+ throw new ProtocolClientError("not-connected");
256
+ }
244
257
  }
245
258
  completeRpcError(msgId, rpcError) {
246
259
  const error = new ProtocolClientError("rpc-error", { code: rpcError.code, message: rpcError.message });
247
- const continuation = this.getAndRemoveRpcContinuation(msgId);
248
- continuation?.reject(error);
260
+ const pending = this.getAndRemovePendingRpcRequest(msgId);
261
+ pending?.reject(error);
249
262
  }
250
- failRpcContinuation(msgId, error) {
251
- const continuation = this.getAndRemoveRpcContinuation(msgId);
252
- continuation?.reject(error);
263
+ failPendingRpcRequest(msgId, error) {
264
+ const pending = this.getAndRemovePendingRpcRequest(msgId);
265
+ pending?.reject(error);
253
266
  }
254
- getAndRemoveRpcContinuation(msgId) {
255
- const continuation = this.rpcContinuations.get(msgId);
256
- if (!continuation)
267
+ getAndRemovePendingRpcRequest(msgId) {
268
+ const pending = this.pendingRpcRequests.get(msgId);
269
+ if (!pending)
257
270
  return null;
258
- if (continuation.timeout)
259
- clearTimeout(continuation.timeout);
260
- this.rpcContinuations.delete(msgId);
261
- return continuation;
262
- }
263
- cancelAllRpcContinuations(error) {
264
- for (const continuation of this.rpcContinuations.values()) {
265
- continuation.reject(error);
266
- if (continuation.timeout)
267
- clearTimeout(continuation.timeout);
271
+ if (pending.timeout)
272
+ clearTimeout(pending.timeout);
273
+ this.pendingRpcRequests.delete(msgId);
274
+ return pending;
275
+ }
276
+ cancelAllPendingRpcRequests(error) {
277
+ for (const pending of this.pendingRpcRequests.values()) {
278
+ pending.reject(error);
279
+ if (pending.timeout)
280
+ clearTimeout(pending.timeout);
281
+ }
282
+ this.pendingRpcRequests.clear();
283
+ }
284
+ resolveRpcTimeoutMs(timeoutMs) {
285
+ return normalizeRpcTimeoutMs(timeoutMs, this.defaultRpcTimeoutMs);
286
+ }
287
+ resendPendingRpcRequests() {
288
+ for (const msgId of this.pendingRpcRequests.keys()) {
289
+ this.trySendPendingRpcRequest(msgId);
268
290
  }
269
- this.rpcContinuations.clear();
291
+ }
292
+ trySendPendingRpcRequest(msgId) {
293
+ const pending = this.pendingRpcRequests.get(msgId);
294
+ if (!pending)
295
+ return;
296
+ if (this.state !== "open")
297
+ return;
298
+ if (pending.sending)
299
+ return;
300
+ pending.sending = true;
301
+ void this.transport
302
+ .send(pending.message)
303
+ .catch((error) => {
304
+ this.log.warn?.("Failed to send RPC request; waiting for reconnect", error);
305
+ this.handleClientFailure();
306
+ })
307
+ .finally(() => {
308
+ pending.sending = false;
309
+ });
270
310
  }
271
311
  }
312
+ const normalizeRpcTimeoutMs = (timeoutMs, fallback) => {
313
+ const resolved = timeoutMs === undefined ? fallback : timeoutMs;
314
+ if (resolved == null)
315
+ return null;
316
+ if (resolved === Number.POSITIVE_INFINITY)
317
+ return null;
318
+ if (!Number.isFinite(resolved))
319
+ return null;
320
+ if (resolved <= 0)
321
+ return null;
322
+ return Math.floor(resolved);
323
+ };
272
324
  export class ProtocolClientError extends Error {
273
325
  constructor(code, details) {
274
326
  super(details?.message ?? code);
@@ -1,16 +1,11 @@
1
- import { MessageEntities, Method, type Peer, type RpcCall, type RpcResult } from "@inline-chat/protocol/core";
1
+ import { Method, type Peer, type RpcCall, type RpcResult } from "@inline-chat/protocol/core";
2
2
  import { type InlineIdLike } from "../ids.js";
3
- import type { InlineSdkClientOptions, InlineInboundEvent, InlineSdkState, MappedMethod, RpcInputForMethod, RpcResultForMethod } from "./types.js";
4
- type SendMessageTarget = {
5
- chatId: InlineIdLike;
6
- userId?: never;
7
- } | {
8
- userId: InlineIdLike;
9
- chatId?: never;
10
- };
3
+ import type { InlineSdkClientOptions, InlineInboundEvent, InlineSdkSendMessageParams, InlineSdkState, InlineSdkUploadFileParams, InlineSdkUploadFileResult, MappedMethod, RpcInputForMethod, RpcResultForMethod } from "./types.js";
11
4
  export declare class InlineSdkClient {
12
5
  private readonly options;
13
6
  private readonly log;
7
+ private readonly httpBaseUrl;
8
+ private readonly fetchImpl;
14
9
  private readonly transport;
15
10
  private readonly protocol;
16
11
  private readonly eventStream;
@@ -38,27 +33,22 @@ export declare class InlineSdkClient {
38
33
  peer?: Peer;
39
34
  title: string;
40
35
  }>;
41
- sendMessage(params: SendMessageTarget & {
42
- text: string;
43
- replyToMsgId?: InlineIdLike;
44
- parseMarkdown?: boolean;
45
- sendMode?: "silent";
46
- entities?: MessageEntities;
47
- }): Promise<{
36
+ sendMessage(params: InlineSdkSendMessageParams): Promise<{
48
37
  messageId: bigint | null;
49
38
  }>;
39
+ uploadFile(params: InlineSdkUploadFileParams): Promise<InlineSdkUploadFileResult>;
50
40
  sendTyping(params: {
51
41
  chatId: InlineIdLike;
52
42
  typing: boolean;
53
43
  }): Promise<void>;
54
44
  invokeRaw(method: Method, input?: RpcCall["input"], options?: {
55
- timeoutMs?: number;
45
+ timeoutMs?: number | null;
56
46
  }): Promise<RpcResult["result"]>;
57
47
  invokeUncheckedRaw(method: Method, input?: RpcCall["input"], options?: {
58
- timeoutMs?: number;
48
+ timeoutMs?: number | null;
59
49
  }): Promise<RpcResult["result"]>;
60
50
  invoke<M extends MappedMethod>(method: M, input: RpcInputForMethod<M>, options?: {
61
- timeoutMs?: number;
51
+ timeoutMs?: number | null;
62
52
  }): Promise<RpcResultForMethod<M>>;
63
53
  private assertMethodInputMatch;
64
54
  private assertMethodResultMatch;
@@ -76,4 +66,3 @@ export declare class InlineSdkClient {
76
66
  private scheduleStateSave;
77
67
  private flushStateSave;
78
68
  }
79
- export {};
@@ -1,4 +1,4 @@
1
- import { GetChatInput, GetMeInput, GetUpdatesInput, GetUpdatesResult_ResultType, GetUpdatesStateInput, InputPeer, MessageEntities, MessageSendMode, Method, UpdateBucket, UpdateComposeAction_ComposeAction, } from "@inline-chat/protocol/core";
1
+ import { GetChatInput, GetMeInput, GetUpdatesInput, GetUpdatesResult_ResultType, GetUpdatesStateInput, InputPeer, MessageSendMode, Method, UpdateBucket, UpdateComposeAction_ComposeAction, } from "@inline-chat/protocol/core";
2
2
  import { asInlineId } from "../ids.js";
3
3
  import { AsyncChannel } from "../utils/async-channel.js";
4
4
  import { ProtocolClient } from "../realtime/protocol-client.js";
@@ -8,6 +8,10 @@ import { noopLogger } from "./logger.js";
8
8
  import { getSdkVersion } from "./sdk-version.js";
9
9
  const nowSeconds = () => BigInt(Math.floor(Date.now() / 1000));
10
10
  const sdkLayer = 1;
11
+ const defaultApiBaseUrl = "https://api.inline.chat";
12
+ const defaultVideoWidth = 1280;
13
+ const defaultVideoHeight = 720;
14
+ const defaultVideoDuration = 1;
11
15
  function extractFirstMessageId(updates) {
12
16
  for (const update of updates ?? []) {
13
17
  if (update.update.oneofKind === "newMessage") {
@@ -21,6 +25,8 @@ function extractFirstMessageId(updates) {
21
25
  export class InlineSdkClient {
22
26
  options;
23
27
  log;
28
+ httpBaseUrl;
29
+ fetchImpl;
24
30
  transport;
25
31
  protocol;
26
32
  eventStream = new AsyncChannel();
@@ -35,8 +41,9 @@ export class InlineSdkClient {
35
41
  constructor(options) {
36
42
  this.options = options;
37
43
  this.log = options.logger ?? noopLogger;
38
- const baseUrl = options.baseUrl ?? "https://api.inline.chat";
39
- const url = resolveRealtimeUrl(baseUrl);
44
+ this.httpBaseUrl = normalizeHttpBaseUrl(options.baseUrl ?? defaultApiBaseUrl);
45
+ this.fetchImpl = options.fetch ?? fetch;
46
+ const url = resolveRealtimeUrl(this.httpBaseUrl);
40
47
  this.transport = options.transport ?? new WebSocketTransport({ url, logger: options.logger });
41
48
  this.protocol = new ProtocolClient({
42
49
  transport: this.transport,
@@ -46,6 +53,7 @@ export class InlineSdkClient {
46
53
  clientVersion: getSdkVersion(),
47
54
  }),
48
55
  logger: options.logger,
56
+ defaultRpcTimeoutMs: options.rpcTimeoutMs,
49
57
  });
50
58
  void this.startListeners();
51
59
  }
@@ -143,12 +151,24 @@ export class InlineSdkClient {
143
151
  if (params.entities != null && params.parseMarkdown != null) {
144
152
  throw new Error("sendMessage: provide either `entities` or `parseMarkdown`, not both");
145
153
  }
154
+ const hasText = typeof params.text === "string" && params.text.length > 0;
155
+ if (!hasText && params.media == null) {
156
+ throw new Error("sendMessage: provide `text` and/or `media`");
157
+ }
158
+ if (params.parseMarkdown != null && !hasText) {
159
+ throw new Error("sendMessage: `parseMarkdown` requires non-empty `text`");
160
+ }
161
+ if (params.entities != null && !hasText) {
162
+ throw new Error("sendMessage: `entities` requires non-empty `text`");
163
+ }
146
164
  const peerId = this.inputPeerFromTarget(params, "sendMessage");
165
+ const media = params.media != null ? toInputMedia(params.media) : undefined;
147
166
  const result = await this.invoke(Method.SEND_MESSAGE, {
148
167
  oneofKind: "sendMessage",
149
168
  sendMessage: {
150
169
  peerId,
151
- message: params.text,
170
+ ...(hasText ? { message: params.text } : {}),
171
+ ...(media != null ? { media } : {}),
152
172
  ...(params.replyToMsgId != null ? { replyToMsgId: asInlineId(params.replyToMsgId, "replyToMsgId") } : {}),
153
173
  ...(params.parseMarkdown != null ? { parseMarkdown: params.parseMarkdown } : {}),
154
174
  ...(params.entities != null ? { entities: params.entities } : {}),
@@ -158,6 +178,59 @@ export class InlineSdkClient {
158
178
  const messageId = extractFirstMessageId(result.sendMessage.updates);
159
179
  return { messageId };
160
180
  }
181
+ async uploadFile(params) {
182
+ const form = new FormData();
183
+ form.set("type", params.type);
184
+ const fileName = normalizeUploadFileName(params.fileName, params.type);
185
+ const fileContentType = resolveUploadContentType(params.type, params.contentType);
186
+ form.set("file", toBlob(params.file, fileContentType), fileName);
187
+ if (params.thumbnail != null) {
188
+ const thumbnailName = normalizeUploadFileName(params.thumbnailFileName, "photo");
189
+ const thumbnailContentType = resolveUploadContentType("photo", params.thumbnailContentType);
190
+ form.set("thumbnail", toBlob(params.thumbnail, thumbnailContentType), thumbnailName);
191
+ }
192
+ if (params.type === "video") {
193
+ const width = normalizePositiveInt(params.width, "width") ?? defaultVideoWidth;
194
+ const height = normalizePositiveInt(params.height, "height") ?? defaultVideoHeight;
195
+ const duration = normalizePositiveInt(params.duration, "duration") ?? defaultVideoDuration;
196
+ form.set("width", String(width));
197
+ form.set("height", String(height));
198
+ form.set("duration", String(duration));
199
+ }
200
+ const response = await this.fetchImpl(new URL("uploadFile", `${this.httpBaseUrl}/`), {
201
+ method: "POST",
202
+ headers: {
203
+ authorization: `Bearer ${this.options.token}`,
204
+ },
205
+ body: form,
206
+ });
207
+ const payload = await parseJsonResponse(response);
208
+ if (!response.ok) {
209
+ const detail = describeUploadFailure(payload);
210
+ throw new Error(`uploadFile: request failed with status ${response.status}${detail ? ` (${detail})` : ""}`);
211
+ }
212
+ if (!isRecord(payload) || payload.ok !== true) {
213
+ const detail = describeUploadFailure(payload);
214
+ throw new Error(`uploadFile: API error${detail ? ` (${detail})` : ""}`);
215
+ }
216
+ const result = payload.result;
217
+ if (!isRecord(result)) {
218
+ throw new Error("uploadFile: malformed success payload");
219
+ }
220
+ const fileUniqueId = typeof result.fileUniqueId === "string" ? result.fileUniqueId.trim() : "";
221
+ if (!fileUniqueId) {
222
+ throw new Error("uploadFile: response missing fileUniqueId");
223
+ }
224
+ const photoId = parseOptionalBigInt(result.photoId, "photoId");
225
+ const videoId = parseOptionalBigInt(result.videoId, "videoId");
226
+ const documentId = parseOptionalBigInt(result.documentId, "documentId");
227
+ return {
228
+ fileUniqueId,
229
+ ...(photoId != null ? { photoId } : {}),
230
+ ...(videoId != null ? { videoId } : {}),
231
+ ...(documentId != null ? { documentId } : {}),
232
+ };
233
+ }
161
234
  async sendTyping(params) {
162
235
  const peerId = InputPeer.create({
163
236
  type: { oneofKind: "chat", chat: { chatId: asInlineId(params.chatId, "chatId") } },
@@ -530,6 +603,138 @@ export class InlineSdkClient {
530
603
  await this.saveInFlight;
531
604
  }
532
605
  }
606
+ function toInputMedia(media) {
607
+ switch (media.kind) {
608
+ case "photo":
609
+ return {
610
+ media: {
611
+ oneofKind: "photo",
612
+ photo: {
613
+ photoId: asInlineId(media.photoId, "photoId"),
614
+ },
615
+ },
616
+ };
617
+ case "video":
618
+ return {
619
+ media: {
620
+ oneofKind: "video",
621
+ video: {
622
+ videoId: asInlineId(media.videoId, "videoId"),
623
+ },
624
+ },
625
+ };
626
+ case "document":
627
+ return {
628
+ media: {
629
+ oneofKind: "document",
630
+ document: {
631
+ documentId: asInlineId(media.documentId, "documentId"),
632
+ },
633
+ },
634
+ };
635
+ }
636
+ }
637
+ function normalizeHttpBaseUrl(baseUrl) {
638
+ const url = new URL(baseUrl);
639
+ const path = url.pathname.replace(/\/+$/, "");
640
+ url.pathname = path || "/";
641
+ return url.toString().replace(/\/$/, "");
642
+ }
643
+ function normalizeUploadFileName(raw, type) {
644
+ const trimmed = raw?.trim();
645
+ if (trimmed)
646
+ return trimmed;
647
+ switch (type) {
648
+ case "photo":
649
+ return "photo.jpg";
650
+ case "video":
651
+ return "video.mp4";
652
+ case "document":
653
+ return "document.bin";
654
+ }
655
+ }
656
+ function resolveUploadContentType(type, explicit) {
657
+ const trimmed = explicit?.trim();
658
+ if (trimmed)
659
+ return trimmed;
660
+ switch (type) {
661
+ case "photo":
662
+ return "image/jpeg";
663
+ case "video":
664
+ return "video/mp4";
665
+ case "document":
666
+ return "application/octet-stream";
667
+ }
668
+ }
669
+ function toBlob(input, type) {
670
+ if (input instanceof Blob) {
671
+ return input.type === type ? input : new Blob([input], { type });
672
+ }
673
+ return new Blob([input], { type });
674
+ }
675
+ function normalizePositiveInt(value, field) {
676
+ if (value == null)
677
+ return undefined;
678
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
679
+ throw new Error(`uploadFile: ${field} must be a positive integer`);
680
+ }
681
+ return value;
682
+ }
683
+ async function parseJsonResponse(response) {
684
+ const contentType = response.headers.get("content-type") ?? "";
685
+ if (!contentType.includes("application/json")) {
686
+ const text = await response.text();
687
+ return text;
688
+ }
689
+ try {
690
+ return await response.json();
691
+ }
692
+ catch {
693
+ return null;
694
+ }
695
+ }
696
+ function isRecord(value) {
697
+ return typeof value === "object" && value !== null && !Array.isArray(value);
698
+ }
699
+ function describeUploadFailure(payload) {
700
+ if (typeof payload === "string") {
701
+ const trimmed = payload.trim();
702
+ return trimmed ? trimmed : "";
703
+ }
704
+ if (!isRecord(payload))
705
+ return "";
706
+ const description = typeof payload.description === "string" ? payload.description.trim() : "";
707
+ if (description)
708
+ return description;
709
+ const error = typeof payload.error === "string" ? payload.error.trim() : "";
710
+ if (error)
711
+ return error;
712
+ return "";
713
+ }
714
+ function parseOptionalBigInt(value, field) {
715
+ if (value == null)
716
+ return undefined;
717
+ if (typeof value === "bigint")
718
+ return value;
719
+ if (typeof value === "number") {
720
+ if (!Number.isFinite(value) || !Number.isInteger(value) || !Number.isSafeInteger(value)) {
721
+ throw new Error(`uploadFile: invalid ${field} in response`);
722
+ }
723
+ return BigInt(value);
724
+ }
725
+ if (typeof value === "string") {
726
+ const trimmed = value.trim();
727
+ if (!trimmed)
728
+ return undefined;
729
+ try {
730
+ return BigInt(trimmed);
731
+ }
732
+ catch {
733
+ throw new Error(`uploadFile: invalid ${field} in response`);
734
+ }
735
+ }
736
+ throw new Error(`uploadFile: invalid ${field} in response`);
737
+ }
533
738
  const resolveRealtimeUrl = (baseUrl) => {
534
739
  const url = new URL(baseUrl);
535
740
  const isSecure = url.protocol === "https:";
@@ -1,14 +1,65 @@
1
- import type { Message, Peer, Reaction, RpcCall, RpcResult, Update, UpdateBucket, UpdatesPayload } from "@inline-chat/protocol/core";
2
- import type { InlineId } from "../ids.js";
1
+ import type { Message, MessageEntities, Peer, Reaction, RpcCall, RpcResult, Update, UpdateBucket, UpdatesPayload } from "@inline-chat/protocol/core";
2
+ import type { InlineId, InlineIdLike } from "../ids.js";
3
3
  import type { InlineUnixSeconds } from "../time.js";
4
4
  import type { InlineSdkLogger } from "./logger.js";
5
5
  import type { Transport } from "../realtime/transport.js";
6
6
  export type InlineSdkClientOptions = {
7
7
  baseUrl?: string;
8
8
  token: string;
9
+ rpcTimeoutMs?: number | null;
9
10
  logger?: InlineSdkLogger;
10
11
  state?: InlineSdkStateStore;
11
12
  transport?: Transport;
13
+ fetch?: typeof fetch;
14
+ };
15
+ export type InlineSdkSendMessageMedia = {
16
+ kind: "photo";
17
+ photoId: InlineIdLike;
18
+ } | {
19
+ kind: "video";
20
+ videoId: InlineIdLike;
21
+ } | {
22
+ kind: "document";
23
+ documentId: InlineIdLike;
24
+ };
25
+ export type InlineSdkSendMessageParams = {
26
+ chatId: InlineIdLike;
27
+ userId?: never;
28
+ text?: string;
29
+ media?: InlineSdkSendMessageMedia;
30
+ replyToMsgId?: InlineIdLike;
31
+ parseMarkdown?: boolean;
32
+ sendMode?: "silent";
33
+ entities?: MessageEntities;
34
+ } | {
35
+ userId: InlineIdLike;
36
+ chatId?: never;
37
+ text?: string;
38
+ media?: InlineSdkSendMessageMedia;
39
+ replyToMsgId?: InlineIdLike;
40
+ parseMarkdown?: boolean;
41
+ sendMode?: "silent";
42
+ entities?: MessageEntities;
43
+ };
44
+ export type InlineSdkBinaryInput = Blob | Uint8Array | ArrayBuffer | SharedArrayBuffer;
45
+ export type InlineSdkUploadFileType = "photo" | "video" | "document";
46
+ export type InlineSdkUploadFileParams = {
47
+ type: InlineSdkUploadFileType;
48
+ file: InlineSdkBinaryInput;
49
+ fileName?: string;
50
+ contentType?: string;
51
+ thumbnail?: InlineSdkBinaryInput;
52
+ thumbnailFileName?: string;
53
+ thumbnailContentType?: string;
54
+ width?: number;
55
+ height?: number;
56
+ duration?: number;
57
+ };
58
+ export type InlineSdkUploadFileResult = {
59
+ fileUniqueId: string;
60
+ photoId?: bigint;
61
+ videoId?: bigint;
62
+ documentId?: bigint;
12
63
  };
13
64
  export type InlineInboundEvent = {
14
65
  kind: "message.new";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inline-chat/realtime-sdk",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [