polfan-server-js-client 0.2.102 → 0.2.103

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.
@@ -18,19 +18,21 @@ type WebApiEventMap = EventsMap & {
18
18
  [WebApiChatClientEvent.error]: Error;
19
19
  [WebApiChatClientEvent.destroy]: boolean;
20
20
  };
21
+ interface SendStackItem {
22
+ data: Envelope;
23
+ attempts: number;
24
+ lastTimeoutId: any;
25
+ }
21
26
  export declare class WebApiChatClient extends AbstractChatClient<Pick<WebApiEventMap, keyof WebApiEventMap>> implements ObservableInterface {
22
27
  private readonly options;
23
28
  readonly Event: typeof WebApiChatClientEvent;
24
- protected sendStack: {
25
- data: any;
26
- attempts: number;
27
- lastTimeoutId: any;
28
- }[];
29
+ protected sendStack: SendStackItem[];
29
30
  constructor(options: WebApiChatClientOptions);
30
31
  send<CommandType extends keyof CommandsMap>(commandType: CommandType, commandData: CommandRequest<CommandType>): Promise<CommandResult<CommandResponse<CommandType>>>;
31
32
  destroy(): void;
32
- protected onMessage(reqId: number, response: Response): Promise<void>;
33
- protected onError(reqId: number, body: string): void;
34
- protected makeApiCall(reqId: number): void;
33
+ protected onMessage(item: SendStackItem, response: Response): Promise<void>;
34
+ protected onError(item: SendStackItem, body: string): void;
35
+ protected makeApiCall(item: SendStackItem): void;
36
+ private removeFromStack;
35
37
  }
36
38
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polfan-server-js-client",
3
- "version": "0.2.102",
3
+ "version": "0.2.103",
4
4
  "description": "JavaScript client library for handling communication with Polfan chat server.",
5
5
  "author": "Jarosław Żak",
6
6
  "license": "MIT",
@@ -22,10 +22,16 @@ type WebApiEventMap = EventsMap & {
22
22
  [WebApiChatClientEvent.destroy]: boolean;
23
23
  };
24
24
 
25
+ interface SendStackItem {
26
+ data: Envelope;
27
+ attempts: number;
28
+ lastTimeoutId: any;
29
+ }
30
+
25
31
  export class WebApiChatClient extends AbstractChatClient<Pick<WebApiEventMap, keyof WebApiEventMap>> implements ObservableInterface {
26
32
  public readonly Event = WebApiChatClientEvent;
27
33
 
28
- protected sendStack: {data: any, attempts: number, lastTimeoutId: any}[];
34
+ protected sendStack: SendStackItem[] = [];
29
35
 
30
36
  public constructor(private readonly options: WebApiChatClientOptions) {
31
37
  super();
@@ -34,48 +40,72 @@ export class WebApiChatClient extends AbstractChatClient<Pick<WebApiEventMap, ke
34
40
  public async send<CommandType extends keyof CommandsMap>(commandType: CommandType, commandData: CommandRequest<CommandType>):
35
41
  Promise<CommandResult<CommandResponse<CommandType>>> {
36
42
  const envelope = this.createEnvelope(commandType, commandData);
37
- this.sendStack.push({data: envelope, attempts: 0, lastTimeoutId: null});
38
- this.makeApiCall(this.sendStack.length - 1);
43
+ const item: SendStackItem = {data: envelope, attempts: 0, lastTimeoutId: null};
44
+ this.sendStack.push(item);
45
+ this.makeApiCall(item);
39
46
  return this.createPromiseFromCommandEnvelope(envelope);
40
47
  }
41
48
 
42
49
  public destroy(): void {
43
- // Cancel all awaiting requests
44
- this.sendStack.forEach(item => {
50
+ // Cancel all awaiting requests, rejecting their promises so no caller
51
+ // is left awaiting forever
52
+ const pending = this.sendStack;
53
+ this.sendStack = [];
54
+ pending.forEach(item => {
45
55
  if (item.lastTimeoutId) {
46
56
  clearTimeout(item.lastTimeoutId);
47
57
  }
48
- this.awaitingResponse.delete(item.data.ref);
58
+ this.handleEnvelopeSendError(item.data, new Error('Client destroyed'));
49
59
  });
50
- this.sendStack = [];
51
60
  this.emit(this.Event.destroy, false);
52
61
  }
53
62
 
54
- protected async onMessage(reqId: number, response: Response): Promise<void> {
55
- this.sendStack.splice(reqId, 1);
56
- const envelope: Envelope = await response.json();
63
+ protected async onMessage(item: SendStackItem, response: Response): Promise<void> {
64
+ let envelope: Envelope;
65
+ try {
66
+ envelope = await response.json();
67
+ } catch (e) {
68
+ // Non-envelope response (e.g. a proxy error page) - failed attempt
69
+ this.onError(item, `envelope ${item.data.ref} (HTTP ${response.status}, unparsable body)`);
70
+ return;
71
+ }
72
+
73
+ if (!this.removeFromStack(item)) {
74
+ return; // Destroyed in the meantime - the promise is already rejected
75
+ }
76
+
57
77
  this.handleIncomingEnvelope(envelope);
58
78
  this.emit(envelope.type, envelope.data);
59
79
  this.emit(this.Event.message, envelope);
60
80
  }
61
81
 
62
- protected onError(reqId: number, body: string): void {
63
- if (this.sendStack[reqId].attempts >= (this.options.attemptsToSend ?? 10)) {
64
- this.sendStack.splice(reqId, 1);
65
- this.handleEnvelopeSendError(this.sendStack[reqId].data, new Error(
82
+ protected onError(item: SendStackItem, body: string): void {
83
+ if (!this.sendStack.includes(item)) {
84
+ return; // Destroyed in the meantime
85
+ }
86
+
87
+ if (item.attempts >= (this.options.attemptsToSend ?? 10)) {
88
+ this.removeFromStack(item);
89
+ this.handleEnvelopeSendError(item.data, new Error(
66
90
  `Cannot send ${body}; aborting after reaching the maximum connection errors`
67
91
  ));
68
92
  return;
69
93
  }
70
- this.sendStack[reqId].lastTimeoutId = setTimeout(
71
- () => this.makeApiCall(reqId),
94
+
95
+ item.lastTimeoutId = setTimeout(
96
+ () => this.makeApiCall(item),
72
97
  this.options.attemptDelayMs ?? 3000
73
98
  );
74
99
  }
75
100
 
76
- protected makeApiCall(reqId: number): void {
77
- this.sendStack[reqId].attempts++;
78
- const bodyJson = JSON.stringify(this.sendStack[reqId].data);
101
+ protected makeApiCall(item: SendStackItem): void {
102
+ if (!this.sendStack.includes(item)) {
103
+ return; // Destroyed before this (re)try fired
104
+ }
105
+
106
+ item.attempts++;
107
+ item.lastTimeoutId = null;
108
+ const bodyJson = JSON.stringify(item.data);
79
109
  const headers: any = {
80
110
  'Content-Type': 'application/json',
81
111
  Accept: 'application/json'
@@ -83,15 +113,24 @@ export class WebApiChatClient extends AbstractChatClient<Pick<WebApiEventMap, ke
83
113
 
84
114
  headers.Authorization = `Bearer ${this.options.token}`;
85
115
 
86
- const params = new URLSearchParams(this.options.queryParams ?? {});
87
- const url = `${this.options.url}${params ? '?' + params : ''}`;
116
+ const query = new URLSearchParams(this.options.queryParams ?? {}).toString();
117
+ const url = query ? `${this.options.url}?${query}` : this.options.url;
88
118
 
89
119
  fetch(url, {
90
120
  headers,
91
121
  body: bodyJson,
92
122
  method: 'POST',
93
123
  })
94
- .then(response => this.onMessage(reqId, response))
95
- .catch(() => this.onError(reqId, bodyJson));
124
+ .then(response => this.onMessage(item, response))
125
+ .catch(() => this.onError(item, bodyJson));
126
+ }
127
+
128
+ private removeFromStack(item: SendStackItem): boolean {
129
+ const index = this.sendStack.indexOf(item);
130
+ if (index === -1) {
131
+ return false;
132
+ }
133
+ this.sendStack.splice(index, 1);
134
+ return true;
96
135
  }
97
136
  }