@inline-chat/realtime-sdk 0.0.2 → 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.
@@ -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);
@@ -42,13 +42,13 @@ export declare class InlineSdkClient {
42
42
  typing: boolean;
43
43
  }): Promise<void>;
44
44
  invokeRaw(method: Method, input?: RpcCall["input"], options?: {
45
- timeoutMs?: number;
45
+ timeoutMs?: number | null;
46
46
  }): Promise<RpcResult["result"]>;
47
47
  invokeUncheckedRaw(method: Method, input?: RpcCall["input"], options?: {
48
- timeoutMs?: number;
48
+ timeoutMs?: number | null;
49
49
  }): Promise<RpcResult["result"]>;
50
50
  invoke<M extends MappedMethod>(method: M, input: RpcInputForMethod<M>, options?: {
51
- timeoutMs?: number;
51
+ timeoutMs?: number | null;
52
52
  }): Promise<RpcResultForMethod<M>>;
53
53
  private assertMethodInputMatch;
54
54
  private assertMethodResultMatch;
@@ -53,6 +53,7 @@ export class InlineSdkClient {
53
53
  clientVersion: getSdkVersion(),
54
54
  }),
55
55
  logger: options.logger,
56
+ defaultRpcTimeoutMs: options.rpcTimeoutMs,
56
57
  });
57
58
  void this.startListeners();
58
59
  }
@@ -6,6 +6,7 @@ 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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inline-chat/realtime-sdk",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [