@bjoernboss/mws 1.0.0 → 1.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/README.md +31 -6
- package/dist/base.d.ts +7 -6
- package/dist/base.js +2 -2
- package/dist/builder.d.ts +14 -0
- package/dist/builder.js +10 -10
- package/dist/cache.d.ts +42 -2
- package/dist/cache.js +81 -41
- package/dist/client.d.ts +144 -19
- package/dist/client.js +177 -153
- package/dist/handler.d.ts +70 -9
- package/dist/handler.js +71 -71
- package/dist/helper.d.ts +24 -1
- package/dist/helper.js +23 -23
- package/dist/log.d.ts +25 -14
- package/dist/log.js +21 -57
- package/dist/server.d.ts +70 -13
- package/dist/server.js +237 -163
- package/package.json +5 -5
package/dist/client.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
/* SPDX-License-Identifier: BSD-3-Clause */
|
|
2
2
|
/* Copyright (c) 2024-2026 Bjoern Boss Henrichsen */
|
|
3
3
|
import * as libLog from "./log.js";
|
|
4
|
-
import * as libCache from "./cache.js";
|
|
5
4
|
import * as libHelper from "./helper.js";
|
|
6
5
|
import * as libBase from "./base.js";
|
|
7
6
|
import * as libEvents from "events";
|
|
8
7
|
import * as libFs from "fs";
|
|
9
8
|
import * as libStream from "stream";
|
|
10
9
|
import * as libUrl from "url";
|
|
11
|
-
import * as libWs from "ws";
|
|
12
10
|
import * as libHttp from "http";
|
|
13
11
|
const BAD_HTTP_STRING_REGEX = /[\x00-\x1f\x7f]/;
|
|
14
12
|
const BAD_HTTP_HEADER_NAME_REGEX = /[\x00-\x1f\x7f\(\)<>@,;:\\"/\[\]\?=\{\} \t]/;
|
|
15
13
|
class ClientContext {
|
|
16
|
-
dropLogTag;
|
|
17
14
|
path;
|
|
15
|
+
identity;
|
|
18
16
|
translationCount;
|
|
19
17
|
busyCount;
|
|
20
18
|
headerPatchCount;
|
|
21
19
|
htmlPatchCount;
|
|
22
|
-
constructor(path, translationCount, busyCount, headerPatchCount, htmlPatchCount) {
|
|
23
|
-
this.
|
|
20
|
+
constructor(path, identity, translationCount, busyCount, headerPatchCount, htmlPatchCount) {
|
|
21
|
+
this.identity = identity;
|
|
24
22
|
this.path = path;
|
|
25
23
|
this.translationCount = translationCount;
|
|
26
24
|
this.busyCount = busyCount;
|
|
@@ -46,25 +44,25 @@ class ClientBase extends libLog.Logger {
|
|
|
46
44
|
}
|
|
47
45
|
this._config = config;
|
|
48
46
|
}
|
|
49
|
-
|
|
47
|
+
/** raw request origin (no host will result in '_'; host will be lower-case) */
|
|
50
48
|
url;
|
|
51
|
-
|
|
49
|
+
/** path relative to current module */
|
|
52
50
|
get path() {
|
|
53
51
|
return this._path;
|
|
54
52
|
}
|
|
55
|
-
|
|
53
|
+
/** configuration used by this client */
|
|
56
54
|
get config() {
|
|
57
55
|
return this._config;
|
|
58
56
|
}
|
|
59
|
-
|
|
57
|
+
/** check if the path relative to the current module is a sub path or the same of the given test base path (can be /base or /base/...) */
|
|
60
58
|
isSubPathOf(base) {
|
|
61
59
|
return libHelper.isSubPath(base, this._path);
|
|
62
60
|
}
|
|
63
|
-
|
|
61
|
+
/** check if the path relative to the current module is inside of the given test base path (must be truly inside; /base/...) */
|
|
64
62
|
isInsideOf(base) {
|
|
65
63
|
return libHelper.isInside(base, this._path);
|
|
66
64
|
}
|
|
67
|
-
|
|
65
|
+
/** create a path relative from the current module into the clients traversed server space */
|
|
68
66
|
makePath(path) {
|
|
69
67
|
path = libHelper.sanitize(path, false);
|
|
70
68
|
let output = path;
|
|
@@ -146,38 +144,38 @@ class HttpRequestResponse extends libStream.Writable {
|
|
|
146
144
|
this.responseCompleted = false;
|
|
147
145
|
}
|
|
148
146
|
}
|
|
149
|
-
|
|
150
|
-
* Does not throw any exceptions, unless explicitly stated.
|
|
151
|
-
* Http HEAD aware (will silently drain any data sent from a HEAD request).
|
|
152
|
-
*
|
|
153
|
-
* Request is considered acknowledged, as soon as a response has been triggered or a preparation started.
|
|
154
|
-
* Path remains URI encoded, as it was received, and path building will use the same encoded paths.
|
|
155
|
-
* Repeated request responding may override any ongoing responses and may terminate the connection; depending on the prior state.
|
|
156
|
-
* Not responded to requests will result in [not-found].
|
|
157
|
-
*
|
|
158
|
-
* Receiving data: Will automatically decode the stream and ensure a given maximum is not passed
|
|
159
|
-
* => Any errors while receiving will either auto-respond or send the connection into the broken state, and fail the receive reader (stream user does not need to respond).
|
|
160
|
-
* => Will terminate a connection, if the upload is not consumed or the client errors.
|
|
161
|
-
* => Premature destroying of receive reader will result in the connection being gracefully terminated.
|
|
162
|
-
* => All data must have been received before the response is completed.
|
|
163
|
-
* Responding data: Will automatically encode the stream and send the header accordingly
|
|
164
|
-
* => Will automatically determine if encoding is to be used
|
|
165
|
-
* => Checks if promised number of bytes is provided
|
|
166
|
-
* => Will automatically error, if the broken state is detected, and will auto-respond or send the connection into the broken state (stream user does not need to respond).
|
|
167
|
-
*
|
|
168
|
-
* A response sent while another is being prepared (acknowledged) will override it and close the connection.
|
|
169
|
-
* A response sent while data is already being streamed (header sent) will break the connection.
|
|
170
|
-
* Normal responses automatically add ClientConfig.responseCacheControl, if no other cache control is specified.
|
|
171
|
-
* File responses will automatically add ClientConfig.fileCacheControl/ClientConfig.immutableCacheControl, if no other cache control is specified.
|
|
172
|
-
* Responses will either use the dedicated responder interface and its highWaterMark, or a responder interface, which caches up to socket.highWaterMark.
|
|
173
|
-
*
|
|
174
|
-
* Upgrade requests, which were not accepted, will be closed after responding.
|
|
175
|
-
* An accept attempt must be fully awaited before completing the handling procedure.
|
|
176
|
-
*
|
|
177
|
-
* Defaults [Accept-Ranges] normally to 'none' or to 'bytes' for files
|
|
178
|
-
* Defaults [Vary] to 'Accept-Encoding'.
|
|
179
|
-
* Defaults [Connection] to 'close' for upgrade requests and for some error responses.
|
|
180
|
-
*/
|
|
147
|
+
/**
|
|
148
|
+
* Does not throw any exceptions, unless explicitly stated.
|
|
149
|
+
* Http HEAD aware (will silently drain any data sent from a HEAD request).
|
|
150
|
+
*
|
|
151
|
+
* Request is considered acknowledged, as soon as a response has been triggered or a preparation started.
|
|
152
|
+
* Path remains URI encoded, as it was received, and path building will use the same encoded paths.
|
|
153
|
+
* Repeated request responding may override any ongoing responses and may terminate the connection; depending on the prior state.
|
|
154
|
+
* Not responded to requests will result in [not-found].
|
|
155
|
+
*
|
|
156
|
+
* Receiving data: Will automatically decode the stream and ensure a given maximum is not passed
|
|
157
|
+
* => Any errors while receiving will either auto-respond or send the connection into the broken state, and fail the receive reader (stream user does not need to respond).
|
|
158
|
+
* => Will terminate a connection, if the upload is not consumed or the client errors.
|
|
159
|
+
* => Premature destroying of receive reader will result in the connection being gracefully terminated.
|
|
160
|
+
* => All data must have been received before the response is completed.
|
|
161
|
+
* Responding data: Will automatically encode the stream and send the header accordingly
|
|
162
|
+
* => Will automatically determine if encoding is to be used
|
|
163
|
+
* => Checks if promised number of bytes is provided
|
|
164
|
+
* => Will automatically error, if the broken state is detected, and will auto-respond or send the connection into the broken state (stream user does not need to respond).
|
|
165
|
+
*
|
|
166
|
+
* A response sent while another is being prepared (acknowledged) will override it and close the connection.
|
|
167
|
+
* A response sent while data is already being streamed (header sent) will break the connection.
|
|
168
|
+
* Normal responses automatically add ClientConfig.responseCacheControl, if no other cache control is specified.
|
|
169
|
+
* File responses will automatically add ClientConfig.fileCacheControl/ClientConfig.immutableCacheControl, if no other cache control is specified.
|
|
170
|
+
* Responses will either use the dedicated responder interface and its highWaterMark, or a responder interface, which caches up to socket.highWaterMark.
|
|
171
|
+
*
|
|
172
|
+
* Upgrade requests, which were not accepted, will be closed after responding.
|
|
173
|
+
* An accept attempt must be fully awaited before completing the handling procedure.
|
|
174
|
+
*
|
|
175
|
+
* Defaults [Accept-Ranges] normally to 'none' or to 'bytes' for files
|
|
176
|
+
* Defaults [Vary] to 'Accept-Encoding'.
|
|
177
|
+
* Defaults [Connection] to 'close' for upgrade requests and for some error responses.
|
|
178
|
+
*/
|
|
181
179
|
export class ClientRequest extends ClientBase {
|
|
182
180
|
_headerPatcher;
|
|
183
181
|
_htmlPatcher;
|
|
@@ -185,8 +183,8 @@ export class ClientRequest extends ClientBase {
|
|
|
185
183
|
_throughput;
|
|
186
184
|
_native;
|
|
187
185
|
_request;
|
|
188
|
-
|
|
189
|
-
constructor(
|
|
186
|
+
_server;
|
|
187
|
+
constructor(server, config, protocol, request, response) {
|
|
190
188
|
super(new libUrl.URL(`${protocol}://${request.headers.host?.toLowerCase() ?? '_'}${request.url}`), 'request', config);
|
|
191
189
|
this._headerPatcher = [];
|
|
192
190
|
this._htmlPatcher = [];
|
|
@@ -207,7 +205,7 @@ export class ClientRequest extends ClientBase {
|
|
|
207
205
|
this._state.completedResolve = completedResolve;
|
|
208
206
|
this._state.breakResolve = breakResolve;
|
|
209
207
|
this._request = request;
|
|
210
|
-
this.
|
|
208
|
+
this._server = server;
|
|
211
209
|
/* setup the throughput measurement to detect any stalling connections */
|
|
212
210
|
this._throughput = { timer: null, deadline: 0, start: 0, active: true, busyCheck: [] };
|
|
213
211
|
if (this.config.throughputThreshold > 0) {
|
|
@@ -440,7 +438,8 @@ export class ClientRequest extends ClientBase {
|
|
|
440
438
|
});
|
|
441
439
|
}
|
|
442
440
|
markAsBroken(reason, graceful) {
|
|
443
|
-
|
|
441
|
+
if (reason != '')
|
|
442
|
+
this.error(`Connection broken: [${reason}]`);
|
|
444
443
|
this._state.response = ResponseState.broken;
|
|
445
444
|
if (this._state.breaking != null) {
|
|
446
445
|
if (!graceful)
|
|
@@ -799,7 +798,7 @@ export class ClientRequest extends ClientBase {
|
|
|
799
798
|
let sanitized = null;
|
|
800
799
|
let match = null;
|
|
801
800
|
/* check if this is only an identity map, in which case nothing complex needs to be evaluated */
|
|
802
|
-
if (Object.keys(map).length == 1 && map['/'] == '/')
|
|
801
|
+
if (map == null || Object.keys(map).length == 1 && map['/'] == '/')
|
|
803
802
|
match = ['/', '/'];
|
|
804
803
|
/* create the merged reverse map and check if the map applies to the current translation */
|
|
805
804
|
else {
|
|
@@ -815,38 +814,30 @@ export class ClientRequest extends ClientBase {
|
|
|
815
814
|
if (match == null || match[1] == null)
|
|
816
815
|
return null;
|
|
817
816
|
}
|
|
818
|
-
const current = new ClientContext(this._path, this._translation.length, this._throughput.busyCheck.length, this._headerPatcher.length, this._htmlPatcher.length);
|
|
817
|
+
const current = new ClientContext(this._path, this.identity, this._translation.length, this._throughput.busyCheck.length, this._headerPatcher.length, this._htmlPatcher.length);
|
|
819
818
|
/* setup the new path, all path translations, and the tagged logging identity */
|
|
820
819
|
this._path = libHelper.rebasePath(match[0], match[1], this._path);
|
|
821
820
|
if (sanitized != null)
|
|
822
821
|
this._translation.push(sanitized);
|
|
823
|
-
if (identity != '')
|
|
824
|
-
|
|
825
|
-
current.dropLogTag = () => update();
|
|
826
|
-
}
|
|
822
|
+
if (identity != '')
|
|
823
|
+
this.logSetIdentity(`${this.identity}.${identity}`);
|
|
827
824
|
return current;
|
|
828
825
|
}
|
|
829
826
|
_restoreSnapshot(snapshot) {
|
|
830
827
|
this._path = snapshot.path;
|
|
828
|
+
this.logSetIdentity(snapshot.identity);
|
|
831
829
|
this._translation.splice(snapshot.translationCount);
|
|
832
830
|
this._throughput.busyCheck.splice(snapshot.busyCount);
|
|
833
831
|
this._headerPatcher.splice(snapshot.headerPatchCount);
|
|
834
832
|
this._htmlPatcher.splice(snapshot.htmlPatchCount);
|
|
835
|
-
snapshot.dropLogTag();
|
|
836
833
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const cache = (options?.cache instanceof libCache.CacheHost ? options.cache : libCache.createCache(options?.cache));
|
|
840
|
-
return new ClientRequest(cache, BurntClientConfig.from(options?.config), protocol, request, response);
|
|
834
|
+
static _fromRequest(protocol, request, response, config, server) {
|
|
835
|
+
return new ClientRequest(server, config, protocol, request, response);
|
|
841
836
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const cache = (options?.cache instanceof libCache.CacheHost ? options.cache : libCache.createCache(options?.cache));
|
|
845
|
-
return new ClientRequest(cache, BurntClientConfig.from(options?.config), protocol, request, { socket, head, wss: options?.wss });
|
|
837
|
+
static _fromUpgrade(protocol, request, socket, head, config, server, wss) {
|
|
838
|
+
return new ClientRequest(server, config, protocol, request, { socket, head, wss });
|
|
846
839
|
}
|
|
847
|
-
|
|
848
|
-
* the end; must have been fully processed and responded to; default responds with not-found for unhandled requests) */
|
|
849
|
-
async finalizeConnection() {
|
|
840
|
+
async _finalizeConnection() {
|
|
850
841
|
/* ensure the connection is default replied with not-found */
|
|
851
842
|
if (this._state.response == ResponseState.none)
|
|
852
843
|
this.respondNotFound();
|
|
@@ -890,7 +881,7 @@ export class ClientRequest extends ClientBase {
|
|
|
890
881
|
this._request.socket.setTimeout(this._native.timeout ?? 0);
|
|
891
882
|
this._state.completedResolve();
|
|
892
883
|
}
|
|
893
|
-
|
|
884
|
+
/** respond with an internal error and kill the connection */
|
|
894
885
|
killConnection(reason) {
|
|
895
886
|
const description = `Connection killed: ${reason}`;
|
|
896
887
|
const closing = (this._state.response == ResponseState.none || this._state.response == ResponseState.acknowledged);
|
|
@@ -898,44 +889,48 @@ export class ClientRequest extends ClientBase {
|
|
|
898
889
|
this.respondInternalError(description, { headers: { 'Connection': 'close' } });
|
|
899
890
|
this.markAsBroken((closing ? '' : description), closing);
|
|
900
891
|
}
|
|
901
|
-
|
|
892
|
+
/** server the client originates from */
|
|
893
|
+
get server() {
|
|
894
|
+
return this._server;
|
|
895
|
+
}
|
|
896
|
+
/** cache host used by this server and client */
|
|
902
897
|
get cache() {
|
|
903
|
-
return this.
|
|
898
|
+
return this._server.cache;
|
|
904
899
|
}
|
|
905
|
-
|
|
900
|
+
/** request has not yet been acknowledged in any way */
|
|
906
901
|
get unhandled() {
|
|
907
902
|
return (this._state.response == ResponseState.none);
|
|
908
903
|
}
|
|
909
|
-
|
|
904
|
+
/** request has been acknowledged or already processed */
|
|
910
905
|
get claimed() {
|
|
911
906
|
return (this._state.response != ResponseState.none);
|
|
912
907
|
}
|
|
913
|
-
|
|
908
|
+
/** resolves whenever the response has been determined (is broken or a response header has been sent) */
|
|
914
909
|
get responded() {
|
|
915
910
|
return this._state.respondedPromise;
|
|
916
911
|
}
|
|
917
|
-
|
|
912
|
+
/** resolves whenever the request has been fully processed */
|
|
918
913
|
get completed() {
|
|
919
914
|
return this._state.completedPromise;
|
|
920
915
|
}
|
|
921
|
-
|
|
916
|
+
/** http request headers */
|
|
922
917
|
get headers() {
|
|
923
918
|
return this._request.headers;
|
|
924
919
|
}
|
|
925
|
-
|
|
920
|
+
/** http request method */
|
|
926
921
|
get method() {
|
|
927
922
|
return this._request.method ?? '';
|
|
928
923
|
}
|
|
929
|
-
|
|
924
|
+
/** was the http request a head request */
|
|
930
925
|
get isHead() {
|
|
931
926
|
return (this._request.method == 'HEAD');
|
|
932
927
|
}
|
|
933
|
-
|
|
928
|
+
/** return the string formatted media-type (or empty string for no media type) */
|
|
934
929
|
getMediaType() {
|
|
935
930
|
const type = libHelper.splitAndTrimList(this.headers['content-type'] ?? null, ';', true)[0] ?? '';
|
|
936
931
|
return type.toLowerCase();
|
|
937
932
|
}
|
|
938
|
-
|
|
933
|
+
/** check the content-type for a media-type and otherwise return the default type */
|
|
939
934
|
getMediaTypeCharset(defEncoding) {
|
|
940
935
|
const type = this.headers['content-type'];
|
|
941
936
|
if (type == null)
|
|
@@ -957,7 +952,7 @@ export class ClientRequest extends ClientBase {
|
|
|
957
952
|
}
|
|
958
953
|
return defEncoding;
|
|
959
954
|
}
|
|
960
|
-
|
|
955
|
+
/** ensure the media-type is one of the list and otherwise return null and auto-respond with [unsupported-media-type] (defaults to first type, if [noneIsFirst]) */
|
|
961
956
|
requireMediaType(types, options) {
|
|
962
957
|
if (!Array.isArray(types))
|
|
963
958
|
types = [types];
|
|
@@ -971,8 +966,8 @@ export class ClientRequest extends ClientBase {
|
|
|
971
966
|
this.respondUnsupported(type, types.map(t => t.mediaType).join(','), options);
|
|
972
967
|
return null;
|
|
973
968
|
}
|
|
974
|
-
|
|
975
|
-
|
|
969
|
+
/** ensure the method is one of the list and otherwise return null and auto-respond with [method-not-allowed]
|
|
970
|
+
* if [headExplicit] is false, method will substitute HEAD for GET, framework will consume the remaining body */
|
|
976
971
|
requireMethod(methods, options) {
|
|
977
972
|
if (!Array.isArray(methods))
|
|
978
973
|
methods = [methods];
|
|
@@ -986,35 +981,35 @@ export class ClientRequest extends ClientBase {
|
|
|
986
981
|
this.respondMethodNotAllowed(this.method, allowed, options);
|
|
987
982
|
return null;
|
|
988
983
|
}
|
|
989
|
-
|
|
990
|
-
|
|
984
|
+
/** register a callback to check if the request is still being processed (delays throughput
|
|
985
|
+
* termintion and resets connection timeout; will only be considered within this handler context) */
|
|
991
986
|
busyCheck(cb) {
|
|
992
987
|
this._throughput.busyCheck.push(cb);
|
|
993
988
|
}
|
|
994
|
-
|
|
995
|
-
|
|
989
|
+
/** register a callback to be invoked once the response is sent, to adjust the
|
|
990
|
+
* headers to be sent (will only be considered within this handler context) */
|
|
996
991
|
patchHeaders(cb) {
|
|
997
992
|
this._headerPatcher.push(cb);
|
|
998
993
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
994
|
+
/** register a callback to be invoked if html is built, to adjust the headers or
|
|
995
|
+
* the content to be sent (will only be considered within this handler context) */
|
|
1001
996
|
patchHtmlPage(cb) {
|
|
1002
997
|
this._htmlPatcher.push(cb);
|
|
1003
998
|
}
|
|
1004
|
-
|
|
999
|
+
/** respond with [internal-error] and a default text response (always considered an error; reason is logged server-side only) */
|
|
1005
1000
|
respondInternalError(reason, options) {
|
|
1006
1001
|
this.constructQuickResponse(libBase.Status.InternalError, `Failure Reason (not sent): ${reason}`, options?.headers, {
|
|
1007
1002
|
media: libBase.Media.Text, body: Buffer.from(`An internal server error occurred while processing the request for [${this.url.pathname}].`, 'utf-8')
|
|
1008
1003
|
});
|
|
1009
1004
|
}
|
|
1010
|
-
|
|
1005
|
+
/** respond with [forbidden] and a default text response (reason is logged server-side only) */
|
|
1011
1006
|
respondForbidden(reason, options) {
|
|
1012
1007
|
this.constructQuickResponse(libBase.Status.Forbidden, `Forbidden Reason (not sent): ${reason}`, options?.headers, {
|
|
1013
1008
|
media: libBase.Media.Text, body: Buffer.from(`Access to [${this.url.pathname}] denied.`, 'utf-8')
|
|
1014
1009
|
});
|
|
1015
1010
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1011
|
+
/** respond with a any response of the given configuration (defaults to media-type: text/unknown/-, status: ok);
|
|
1012
|
+
* if [lightResponse], the content length is suppressed for head responses (to accomodate short-circuiting responding) */
|
|
1018
1013
|
respond(content, options) {
|
|
1019
1014
|
const status = options?.status ?? libBase.Status.Ok;
|
|
1020
1015
|
if (content == null)
|
|
@@ -1028,13 +1023,13 @@ export class ClientRequest extends ClientBase {
|
|
|
1028
1023
|
media, body: (options?.lightResponse && this.isHead ? undefined : content)
|
|
1029
1024
|
});
|
|
1030
1025
|
}
|
|
1031
|
-
|
|
1026
|
+
/** respond with [ok] and either a message or a default response */
|
|
1032
1027
|
respondOk(options) {
|
|
1033
1028
|
this.constructQuickResponse(libBase.Status.Ok, options?.message ?? null, options?.headers, {
|
|
1034
1029
|
media: libBase.Media.Text, body: Buffer.from(options?.message ?? `${this.method} was successful for [${this.url.pathname}].`, 'utf-8')
|
|
1035
1030
|
});
|
|
1036
1031
|
}
|
|
1037
|
-
|
|
1032
|
+
/** respond with [created] and either a message or a default response (ensure target is properly URI encoded) */
|
|
1038
1033
|
respondCreated(target, options) {
|
|
1039
1034
|
const header = (options?.headers ?? {});
|
|
1040
1035
|
header['Location'] = target;
|
|
@@ -1042,7 +1037,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1042
1037
|
media: libBase.Media.Text, body: Buffer.from(`Resource [${this.url.pathname}] successfully created:\n${target}`, 'utf-8')
|
|
1043
1038
|
});
|
|
1044
1039
|
}
|
|
1045
|
-
|
|
1040
|
+
/** respond with [not-modified] and no body (ensure the etag and/or last-modified is set) */
|
|
1046
1041
|
respondNotModified(options) {
|
|
1047
1042
|
const header = (options?.headers ?? {});
|
|
1048
1043
|
if (options?.etag != null && !('ETag' in header))
|
|
@@ -1051,7 +1046,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1051
1046
|
header['Last-Modified'] = options.lastModified;
|
|
1052
1047
|
this.constructQuickResponse(libBase.Status.NotModified, null, header, null);
|
|
1053
1048
|
}
|
|
1054
|
-
|
|
1049
|
+
/** respond with [precondition-failed] and a default text response (ensure the etag and/or last-modified is set) */
|
|
1055
1050
|
respondPreconditionFailed(reason, options) {
|
|
1056
1051
|
const header = (options?.headers ?? {});
|
|
1057
1052
|
if (options?.etag != null && !('ETag' in header))
|
|
@@ -1062,13 +1057,13 @@ export class ClientRequest extends ClientBase {
|
|
|
1062
1057
|
media: libBase.Media.Text, body: Buffer.from(`Precondition for resource [${this.url.pathname}] failed:\n${reason}`, 'utf-8')
|
|
1063
1058
|
});
|
|
1064
1059
|
}
|
|
1065
|
-
|
|
1060
|
+
/** respond with [bad-request] and a default text response */
|
|
1066
1061
|
respondBadRequest(reason, options) {
|
|
1067
1062
|
this.constructQuickResponse(libBase.Status.BadRequest, reason, options?.headers, {
|
|
1068
1063
|
media: libBase.Media.Text, body: Buffer.from(`Request for [${this.url.pathname}] is perceived as malformed:\n${reason}`, 'utf-8')
|
|
1069
1064
|
});
|
|
1070
1065
|
}
|
|
1071
|
-
|
|
1066
|
+
/** respond with [range-not-satisfiable] and a default text response */
|
|
1072
1067
|
respondRangeIssue(range, size, options) {
|
|
1073
1068
|
const header = (options?.headers ?? {});
|
|
1074
1069
|
header['Content-Range'] = `bytes */${size}`;
|
|
@@ -1076,25 +1071,25 @@ export class ClientRequest extends ClientBase {
|
|
|
1076
1071
|
media: libBase.Media.Text, body: Buffer.from(`Range [${range}] cannot be satisfied for [${this.url.pathname}] of size ${size}.`, 'utf-8')
|
|
1077
1072
|
});
|
|
1078
1073
|
}
|
|
1079
|
-
|
|
1074
|
+
/** respond with [conflict] and a default text response */
|
|
1080
1075
|
respondConflict(conflict, options) {
|
|
1081
1076
|
this.constructQuickResponse(libBase.Status.Conflict, conflict, options?.headers, {
|
|
1082
1077
|
media: libBase.Media.Text, body: Buffer.from(`Conflict for resource [${this.url.pathname}]:\n${conflict}`, 'utf-8')
|
|
1083
1078
|
});
|
|
1084
1079
|
}
|
|
1085
|
-
|
|
1080
|
+
/** respond with [not-found] and a default text response */
|
|
1086
1081
|
respondNotFound(options) {
|
|
1087
1082
|
this.constructQuickResponse(libBase.Status.NotFound, null, options?.headers, {
|
|
1088
1083
|
media: libBase.Media.Text, body: Buffer.from(`Resource [${this.url.pathname}] could not be found.`, 'utf-8')
|
|
1089
1084
|
});
|
|
1090
1085
|
}
|
|
1091
|
-
|
|
1086
|
+
/** respond with [unsupported-media-type] and a default text response */
|
|
1092
1087
|
respondUnsupported(used, allowed, options) {
|
|
1093
1088
|
this.constructQuickResponse(libBase.Status.UnsupportedMediaType, `Allowed was [${allowed}] but [${used}] was used`, options?.headers, {
|
|
1094
1089
|
media: libBase.Media.Text, body: Buffer.from(`Media type [${used}] not supported for [${this.url.pathname}].\nAllowed: ${allowed}`, 'utf-8')
|
|
1095
1090
|
});
|
|
1096
1091
|
}
|
|
1097
|
-
|
|
1092
|
+
/** respond with [method-not-allowed] and a default text response */
|
|
1098
1093
|
respondMethodNotAllowed(method, allowed, options) {
|
|
1099
1094
|
const header = (options?.headers ?? {});
|
|
1100
1095
|
header['Allow'] = allowed;
|
|
@@ -1102,7 +1097,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1102
1097
|
media: libBase.Media.Text, body: Buffer.from(`Method ${method} not allowed for [${this.url.pathname}].\nAllowed: ${allowed}.`, 'utf-8')
|
|
1103
1098
|
});
|
|
1104
1099
|
}
|
|
1105
|
-
|
|
1100
|
+
/** respond with [request-timeout] and a default text response */
|
|
1106
1101
|
respondRequestTimeout(reason, options) {
|
|
1107
1102
|
const header = (options?.headers ?? {});
|
|
1108
1103
|
header['Connection'] = 'close';
|
|
@@ -1110,13 +1105,13 @@ export class ClientRequest extends ClientBase {
|
|
|
1110
1105
|
media: libBase.Media.Text, body: Buffer.from(`Request processing of [${this.url.pathname}] timed out:\n${reason}`, 'utf-8')
|
|
1111
1106
|
});
|
|
1112
1107
|
}
|
|
1113
|
-
|
|
1108
|
+
/** respond with [content-too-large] and a default text response */
|
|
1114
1109
|
respondContentTooLarge(allowed, atLeastProvided, options) {
|
|
1115
1110
|
this.constructQuickResponse(libBase.Status.ContentTooLarge, `[${atLeastProvided}] > [${allowed}]`, options?.headers, {
|
|
1116
1111
|
media: libBase.Media.Text, body: Buffer.from(`Content of at least size ${atLeastProvided} too large for [${this.url.pathname}].\nAt most ${allowed} bytes are allowed.`, 'utf-8')
|
|
1117
1112
|
});
|
|
1118
1113
|
}
|
|
1119
|
-
|
|
1114
|
+
/** respond with [update-required] and a default text response */
|
|
1120
1115
|
respondUpdateRequired(upgrade, options) {
|
|
1121
1116
|
const header = (options?.headers ?? {});
|
|
1122
1117
|
if (!('Connection' in header))
|
|
@@ -1126,7 +1121,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1126
1121
|
media: libBase.Media.Text, body: Buffer.from(`Endpoint [${this.url.pathname}] requires an upgrade.\nRequired: ${upgrade}`, 'utf-8')
|
|
1127
1122
|
});
|
|
1128
1123
|
}
|
|
1129
|
-
|
|
1124
|
+
/** respond with [see-other] to the given target and a default text response (forces method GET; ensure target is properly URI encoded) */
|
|
1130
1125
|
respondSeeOther(target, options) {
|
|
1131
1126
|
const header = (options?.headers ?? {});
|
|
1132
1127
|
header['Location'] = target;
|
|
@@ -1134,7 +1129,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1134
1129
|
media: libBase.Media.Text, body: Buffer.from(`Continue at: ${target}`, 'utf-8')
|
|
1135
1130
|
});
|
|
1136
1131
|
}
|
|
1137
|
-
|
|
1132
|
+
/** respond with [temporary-redirect] to the given target and a default text response (preserves method; ensure target is properly URI encoded) */
|
|
1138
1133
|
respondTemporaryRedirect(target, options) {
|
|
1139
1134
|
const header = (options?.headers ?? {});
|
|
1140
1135
|
header['Location'] = target;
|
|
@@ -1142,7 +1137,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1142
1137
|
media: libBase.Media.Text, body: Buffer.from(`Resource [${this.url.pathname}] temporarily redirects to:\n${target}`, 'utf-8')
|
|
1143
1138
|
});
|
|
1144
1139
|
}
|
|
1145
|
-
|
|
1140
|
+
/** respond with [permanent-redirect] to the given target and a default text response (preserves method; ensure target is properly URI encoded) */
|
|
1146
1141
|
respondPermanentRedirect(target, options) {
|
|
1147
1142
|
const header = (options?.headers ?? {});
|
|
1148
1143
|
header['Location'] = target;
|
|
@@ -1150,9 +1145,9 @@ export class ClientRequest extends ClientBase {
|
|
|
1150
1145
|
media: libBase.Media.Text, body: Buffer.from(`Resource [${this.url.pathname}] permanently redirects to:\n${target}`, 'utf-8')
|
|
1151
1146
|
});
|
|
1152
1147
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1148
|
+
/** respond with html, can be built on by parent modules, sent once the request has been fully processed
|
|
1149
|
+
* (default status is ok; for HEAD builds, no actual content will be constructed or estimated in size)
|
|
1150
|
+
* automatically adds ClientConfig.responseCacheControl, if no other cache control is specified */
|
|
1156
1151
|
async respondHtml(page, options) {
|
|
1157
1152
|
if (this._state.response != ResponseState.none)
|
|
1158
1153
|
return this.badClientUsage('HTML response on already claimed connection', false);
|
|
@@ -1179,10 +1174,10 @@ export class ClientRequest extends ClientBase {
|
|
|
1179
1174
|
this.log(`Responding with HTML content and status [${status.msg}]${this.isHead ? ' as light-build' : ''}`);
|
|
1180
1175
|
this.sendFullResponse(status, headers, { media: libBase.Media.Html, body: content });
|
|
1181
1176
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1177
|
+
/** [no-throw but errors] send data with [media type] and [status] and return a writable stream (default: status is ok, media is unknown, dynamicEncode is true);
|
|
1178
|
+
* if a content size is provided, stream expects exactly this amount of bytes; if [dynamicEncode], the encoder will be dynamically negotiated
|
|
1179
|
+
* based on the content; for a HEAD request, no encoding will be negotiated, no lengths verified, and the written data will just be drained
|
|
1180
|
+
* (can immediately be ended using '.end()'); automatically adds ClientConfig.responseCacheControl, if no other cache control is specified */
|
|
1186
1181
|
respondData(options) {
|
|
1187
1182
|
const status = options?.status ?? libBase.Status.Ok;
|
|
1188
1183
|
const headers = (options?.headers ?? {});
|
|
@@ -1191,11 +1186,11 @@ export class ClientRequest extends ClientBase {
|
|
|
1191
1186
|
this.log(`Responding with data and status [${status.msg}]`);
|
|
1192
1187
|
return this.sendClientData(status, options?.media ?? libBase.Media.Unknown, headers, options?.dynamicEncode ?? true, options?.contentSize ?? null);
|
|
1193
1188
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1189
|
+
/** try to respond with the given file, return false, if the file does not exist (range aware, HEAD aware); specify [checkFreshness] to
|
|
1190
|
+
* re-validate the file stats on disk before serving from cache; the media type can be overwritten (defaults to extracting media-type
|
|
1191
|
+
* from the file-path); [encoding] describes the encoding of a pre-encoded file (warning: no checks against accepted encodings
|
|
1192
|
+
* performed!); status will be [Ok], [partial-content], [not-modified] or according errors cache aware and etag/last-modified aware;
|
|
1193
|
+
* automatically adds ClientConfig.fileCacheControl/ClientConfig.immutableCacheControl, if no other cache control is specified */
|
|
1199
1194
|
async tryRespondFile(filePath, options) {
|
|
1200
1195
|
if (options == null)
|
|
1201
1196
|
options = {};
|
|
@@ -1211,7 +1206,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1211
1206
|
return false;
|
|
1212
1207
|
}
|
|
1213
1208
|
catch (err) {
|
|
1214
|
-
this.respondInternalError(`Failed to read file: ${err.message}`);
|
|
1209
|
+
this.respondInternalError(`Failed to read file [${filePath}]: ${err.message}`);
|
|
1215
1210
|
return true;
|
|
1216
1211
|
}
|
|
1217
1212
|
if (typeof cached == 'string') {
|
|
@@ -1315,7 +1310,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1315
1310
|
if (settled)
|
|
1316
1311
|
return;
|
|
1317
1312
|
settled = true;
|
|
1318
|
-
this.respondInternalError(`Failed to stream file: ${err.message}`);
|
|
1313
|
+
this.respondInternalError(`Failed to stream file [${filePath}]: ${err.message}`);
|
|
1319
1314
|
stream.destroy(err);
|
|
1320
1315
|
});
|
|
1321
1316
|
stream.once('error', (err) => {
|
|
@@ -1330,9 +1325,9 @@ export class ClientRequest extends ClientBase {
|
|
|
1330
1325
|
});
|
|
1331
1326
|
});
|
|
1332
1327
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1328
|
+
/** [throws] receive the payload of given max length and write it directly to a file; will fail
|
|
1329
|
+
* if the file already exists and delete the file if it could not be received in full
|
|
1330
|
+
* automatically responds with given exceptions if the payload cannot be received properly or file operations fail */
|
|
1336
1331
|
async receiveToFile(path, maxLength) {
|
|
1337
1332
|
this.trace(`Collecting data from [${this.url.pathname}] to: [${path}]`);
|
|
1338
1333
|
return new Promise((resolve, reject) => {
|
|
@@ -1373,14 +1368,14 @@ export class ClientRequest extends ClientBase {
|
|
|
1373
1368
|
});
|
|
1374
1369
|
});
|
|
1375
1370
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1371
|
+
/** [no-throw but errors] receive the payload of given max length as a readable stream
|
|
1372
|
+
* automatically responds with given exceptions if the payload cannot be received properly
|
|
1373
|
+
* automatically drained if the readable stream is destroyed before reading all data */
|
|
1379
1374
|
receiveData(maxLength) {
|
|
1380
1375
|
return this.receiveClientData(maxLength);
|
|
1381
1376
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1377
|
+
/** [throws] receive the payload of given max length as a single complete buffer
|
|
1378
|
+
* automatically responds with given exceptions if the payload cannot be received properly */
|
|
1384
1379
|
async receiveAllBuffer(maxLength) {
|
|
1385
1380
|
return new Promise((resolve, reject) => {
|
|
1386
1381
|
let stream = this.receiveClientData(maxLength);
|
|
@@ -1390,8 +1385,8 @@ export class ClientRequest extends ClientBase {
|
|
|
1390
1385
|
stream.once('error', (err) => reject(err));
|
|
1391
1386
|
});
|
|
1392
1387
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1388
|
+
/** [throws] receive the payload of given max length as a single complete decoded string
|
|
1389
|
+
* automatically responds with given exceptions if the payload cannot be received properly */
|
|
1395
1390
|
async receiveAllText(encoding, maxLength) {
|
|
1396
1391
|
/* wait for the buffer (let all errors propagate out) */
|
|
1397
1392
|
const buffer = await this.receiveAllBuffer(maxLength);
|
|
@@ -1403,8 +1398,8 @@ export class ClientRequest extends ClientBase {
|
|
|
1403
1398
|
throw err;
|
|
1404
1399
|
}
|
|
1405
1400
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1401
|
+
/** marks the object as having been handled and returns a web socket or
|
|
1402
|
+
* automatically responds with a corresponding error and returns null */
|
|
1408
1403
|
async acceptWebSocket() {
|
|
1409
1404
|
if (this._state.response != ResponseState.none) {
|
|
1410
1405
|
this.badClientUsage('WebSocket upgrade on already claimed connection', false);
|
|
@@ -1438,8 +1433,7 @@ export class ClientRequest extends ClientBase {
|
|
|
1438
1433
|
resolve(null);
|
|
1439
1434
|
});
|
|
1440
1435
|
/* start the upgrade process (web-socket upgrade handler will automatically send error messages) */
|
|
1441
|
-
|
|
1442
|
-
wss.handleUpgrade(this._request, native.socket, native.head, (ws, _) => {
|
|
1436
|
+
native.wss.handleUpgrade(this._request, native.socket, native.head, (ws, _) => {
|
|
1443
1437
|
if (!settled && this._state.response == ResponseState.headerSent) {
|
|
1444
1438
|
settled = true, this._state.response = ResponseState.completed;
|
|
1445
1439
|
/* ensure that the socket is valid as otherwise proper cleanup might not be guaranteed (no
|
|
@@ -1459,18 +1453,18 @@ export class ClientRequest extends ClientBase {
|
|
|
1459
1453
|
return ws;
|
|
1460
1454
|
}
|
|
1461
1455
|
}
|
|
1462
|
-
|
|
1463
|
-
* WebSocket with integrated alive checks.
|
|
1464
|
-
* Structured WebSocket, which takes care of error handling.
|
|
1465
|
-
* The 'close' event is guaranteed to fire exactly once and no 'data' events will follow.
|
|
1466
|
-
* Takes ownership of the socket.
|
|
1467
|
-
*/
|
|
1456
|
+
/**
|
|
1457
|
+
* WebSocket with integrated alive checks.
|
|
1458
|
+
* Structured WebSocket, which takes care of error handling.
|
|
1459
|
+
* The 'close' event is guaranteed to fire exactly once and no 'data' events will follow.
|
|
1460
|
+
* Takes ownership of the socket.
|
|
1461
|
+
*/
|
|
1468
1462
|
export class ClientSocket extends ClientBase {
|
|
1469
1463
|
_ws;
|
|
1470
1464
|
_alive;
|
|
1471
1465
|
_closing;
|
|
1472
1466
|
_emitter;
|
|
1473
|
-
|
|
1467
|
+
_log;
|
|
1474
1468
|
constructor(ws, source) {
|
|
1475
1469
|
super(source, 'socket', source.config);
|
|
1476
1470
|
this._ws = ws;
|
|
@@ -1478,7 +1472,7 @@ export class ClientSocket extends ClientBase {
|
|
|
1478
1472
|
this._closing = { promise: null, closed: null, defer: 0 };
|
|
1479
1473
|
this._emitter = new libEvents.EventEmitter();
|
|
1480
1474
|
this._ws.on('pong', () => {
|
|
1481
|
-
this.trace(`Alive check pong received`, {
|
|
1475
|
+
this.trace(`Alive check pong received`, { identity: this._log.identity });
|
|
1482
1476
|
this.selfIsAlive();
|
|
1483
1477
|
});
|
|
1484
1478
|
this._ws.on('message', (data) => {
|
|
@@ -1504,12 +1498,14 @@ export class ClientSocket extends ClientBase {
|
|
|
1504
1498
|
this._ws.once('error', (err) => {
|
|
1505
1499
|
this.handleClosing(`WebSocket error: ${err.message}`);
|
|
1506
1500
|
});
|
|
1501
|
+
/* perserve the log extension of the base and preserve it to be re-used for internal logs */
|
|
1502
|
+
const extIndex = source.identity.indexOf('.');
|
|
1503
|
+
if (extIndex > 0)
|
|
1504
|
+
this.logSetIdentity(`${this.identity}${source.identity.substring(extIndex)}`);
|
|
1505
|
+
source.log(`WebSocket accepted: [${this.identity}]`);
|
|
1506
|
+
this._log = { identity: this.identity, tagList: [] };
|
|
1507
1507
|
/* start the first alive check (no need to consider the socket timeout, as it will have been cleared already) */
|
|
1508
1508
|
this.selfIsAlive();
|
|
1509
|
-
/* perserve the log extension of the base and preserve it to be re-used for internal logs */
|
|
1510
|
-
source.log(`WebSocket accepted: [${this.logIdentity}]`);
|
|
1511
|
-
this.tagLog(source.logExtension);
|
|
1512
|
-
this._extension = source.logExtension;
|
|
1513
1509
|
}
|
|
1514
1510
|
checkIsAlive() {
|
|
1515
1511
|
if (this._closing.promise != null)
|
|
@@ -1524,7 +1520,7 @@ export class ClientSocket extends ClientBase {
|
|
|
1524
1520
|
this._alive.timer = setTimeout(() => this.checkIsAlive(), this.config.webSocketAliveTimeout);
|
|
1525
1521
|
/* try to ping the remote to check the liveliness */
|
|
1526
1522
|
try {
|
|
1527
|
-
this.trace(`Sending ping to determine if connection is alive`, {
|
|
1523
|
+
this.trace(`Sending ping to determine if connection is alive`, { identity: this._log.identity });
|
|
1528
1524
|
this._ws.ping();
|
|
1529
1525
|
}
|
|
1530
1526
|
catch (err) {
|
|
@@ -1549,14 +1545,14 @@ export class ClientSocket extends ClientBase {
|
|
|
1549
1545
|
this._alive.timer = null;
|
|
1550
1546
|
/* check if a termination should be triggered and otherwise start the grace termination timer */
|
|
1551
1547
|
if (terminate != null) {
|
|
1552
|
-
this.error(terminate, {
|
|
1548
|
+
this.error(terminate, { identity: this._log.identity });
|
|
1553
1549
|
this._ws.terminate();
|
|
1554
1550
|
}
|
|
1555
1551
|
else {
|
|
1556
1552
|
this._alive.timer = setTimeout(() => {
|
|
1557
1553
|
this._alive.timer = null;
|
|
1558
1554
|
if (this._closing.closed != null) {
|
|
1559
|
-
this.error('Closing connection', {
|
|
1555
|
+
this.error('Closing connection', { identity: this._log.identity });
|
|
1560
1556
|
this._ws.terminate();
|
|
1561
1557
|
}
|
|
1562
1558
|
}, this.config.killGraceTimeout);
|
|
@@ -1567,13 +1563,21 @@ export class ClientSocket extends ClientBase {
|
|
|
1567
1563
|
const closed = this._closing.closed;
|
|
1568
1564
|
this._closing.closed = null;
|
|
1569
1565
|
this.emitEventSync('close');
|
|
1570
|
-
this.trace('Socket connection closed', {
|
|
1566
|
+
this.trace('Socket connection closed', { identity: this._log.identity });
|
|
1571
1567
|
closed();
|
|
1572
1568
|
}
|
|
1569
|
+
updateLogIdentity() {
|
|
1570
|
+
let identity = this._log.identity;
|
|
1571
|
+
for (const tag of this._log.tagList) {
|
|
1572
|
+
if (tag.value != '')
|
|
1573
|
+
identity += `.${tag.value}`;
|
|
1574
|
+
}
|
|
1575
|
+
this.logSetIdentity(identity);
|
|
1576
|
+
}
|
|
1573
1577
|
static _fromRequest(ws, source) {
|
|
1574
1578
|
return new ClientSocket(ws, source);
|
|
1575
1579
|
}
|
|
1576
|
-
|
|
1580
|
+
/** send data to the remote (ignored if connection is being closed) */
|
|
1577
1581
|
send(data) {
|
|
1578
1582
|
if (this._closing.promise != null)
|
|
1579
1583
|
return;
|
|
@@ -1584,7 +1588,7 @@ export class ClientSocket extends ClientBase {
|
|
|
1584
1588
|
this.handleClosing(`WebSocket error while sending data: ${err.message}`);
|
|
1585
1589
|
}
|
|
1586
1590
|
}
|
|
1587
|
-
|
|
1591
|
+
/** close the web socket (promise resolved once the close callback has been fully invoked) */
|
|
1588
1592
|
close() {
|
|
1589
1593
|
if (this._closing.promise == null) {
|
|
1590
1594
|
this._ws.close();
|
|
@@ -1592,6 +1596,26 @@ export class ClientSocket extends ClientBase {
|
|
|
1592
1596
|
}
|
|
1593
1597
|
return this._closing.promise;
|
|
1594
1598
|
}
|
|
1599
|
+
/** tag the logging with the given identifier and return a callback to update the tag */
|
|
1600
|
+
tagLog(identifier) {
|
|
1601
|
+
let tag = { value: identifier };
|
|
1602
|
+
this._log.tagList.push(tag);
|
|
1603
|
+
if (tag.value != '')
|
|
1604
|
+
this.updateLogIdentity();
|
|
1605
|
+
/* setup the handler responsible to update the logging */
|
|
1606
|
+
return (value) => {
|
|
1607
|
+
if (tag == null)
|
|
1608
|
+
return;
|
|
1609
|
+
/* check if the tag should be removed or if the value should just be updated */
|
|
1610
|
+
if (value == null) {
|
|
1611
|
+
this._log.tagList = this._log.tagList.filter((v) => v != tag);
|
|
1612
|
+
tag = null;
|
|
1613
|
+
}
|
|
1614
|
+
else if (value != tag.value)
|
|
1615
|
+
tag.value = value;
|
|
1616
|
+
this.updateLogIdentity();
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1595
1619
|
/* -------- event handler interfaces -------- */
|
|
1596
1620
|
on(event, listener) {
|
|
1597
1621
|
this._emitter.on(event, listener);
|
|
@@ -1610,7 +1634,7 @@ export class ClientSocket extends ClientBase {
|
|
|
1610
1634
|
this._emitter.emit(event, ...args);
|
|
1611
1635
|
}
|
|
1612
1636
|
catch (err) {
|
|
1613
|
-
this.error(`Unhandled exception in ${event} listener: ${err.message}`, {
|
|
1637
|
+
this.error(`Unhandled exception in ${event} listener: ${err.message}`, { identity: this._log.identity });
|
|
1614
1638
|
}
|
|
1615
1639
|
}
|
|
1616
1640
|
}
|