@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/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.dropLogTag = () => { };
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
- /* raw request origin (no host will result in '_'; host will be lower-case) */
47
+ /** raw request origin (no host will result in '_'; host will be lower-case) */
50
48
  url;
51
- /* path relative to current module */
49
+ /** path relative to current module */
52
50
  get path() {
53
51
  return this._path;
54
52
  }
55
- /* configuration used by this client */
53
+ /** configuration used by this client */
56
54
  get config() {
57
55
  return this._config;
58
56
  }
59
- /* check if the path relative to the current module is a sub path of the given test base path */
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
- /* check if the path relative to the current module is inside of the given test base path */
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
- /* create a path relative from the current module into the clients traversed server space */
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
- _cache;
189
- constructor(cache, config, protocol, request, response) {
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._cache = cache;
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
- this.error(`Connection broken: [${reason}]`);
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
- const update = this.tagLog(identity);
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
- /* instantiate a request client from a web request structure (must be followed by one finalizeConnection call under all circumstances) */
838
- static fromRequest(protocol, request, response, options) {
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
- /* instantiate a request client from a web socket upgrade structure (instantiates a new no-server wss if none is provided; must be followed by one finalizeConnection call under all circumstances) */
843
- static fromUpgrade(protocol, request, socket, head, options) {
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
- /* finalize the connection by ensuring the response is completed and pontentially also closed (must be called once at
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
- /* respond with an internal error and kill the connection */
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
- /* cache host to be used with this client */
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._cache;
898
+ return this._server.cache;
904
899
  }
905
- /* request has not yet been acknowledged in any way */
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
- /* request has been acknowledged or already processed */
904
+ /** request has been acknowledged or already processed */
910
905
  get claimed() {
911
906
  return (this._state.response != ResponseState.none);
912
907
  }
913
- /* resolves whenever the response has been determined (is broken or a response header has been sent) */
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
- /* resolves whenever the request has been fully processed */
912
+ /** resolves whenever the request has been fully processed */
918
913
  get completed() {
919
914
  return this._state.completedPromise;
920
915
  }
921
- /* http request headers */
916
+ /** http request headers */
922
917
  get headers() {
923
918
  return this._request.headers;
924
919
  }
925
- /* http request method */
920
+ /** http request method */
926
921
  get method() {
927
922
  return this._request.method ?? '';
928
923
  }
929
- /* was the http request a head request */
924
+ /** was the http request a head request */
930
925
  get isHead() {
931
926
  return (this._request.method == 'HEAD');
932
927
  }
933
- /* return the string formatted media-type (or empty string for no media type) */
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
- /* check the content-type for a media-type and otherwise return the default type */
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
- /* 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]) */
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
- /* ensure the method is one of the list and otherwise return null and auto-respond with [method-not-allowed]
975
- * if [headExplicit] is false, method will substitute HEAD for GET, framework will consume the remaining body */
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
- /* register a callback to check if the request is still being processed (delays throughput
990
- * termintion and resets connection timeout; will only be considered within this handler context) */
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
- /* register a callback to be invoked once the response is sent, to adjust the
995
- * headers to be sent (will only be considered within this handler context) */
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
- /* register a callback to be invoked if html is built, to adjust the headers or
1000
- * the content to be sent (will only be considered within this handler context) */
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
- /* respond with [internal-error] and a default text response (always considered an error; reason is logged server-side only) */
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
- /* respond with [forbidden] and a default text response (reason is logged server-side only) */
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
- /* respond with a any response of the given configuration (defaults to media-type: text/unknown/-, status: ok);
1017
- * if [lightResponse], the content length is suppressed for head responses (to accomodate short-circuiting responding) */
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
- /* respond with [ok] and either a message or a default response */
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
- /* respond with [created] and either a message or a default response (ensure target is properly URI encoded) */
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
- /* respond with [not-modified] and no body (ensure the etag and/or last-modified is set) */
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
- /* respond with [precondition-failed] and a default text response (ensure the etag and/or last-modified is set) */
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
- /* respond with [bad-request] and a default text response */
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
- /* respond with [range-not-satisfiable] and a default text response */
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
- /* respond with [conflict] and a default text response */
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
- /* respond with [not-found] and a default text response */
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
- /* respond with [unsupported-media-type] and a default text response */
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
- /* respond with [invalid-method] and a default text response */
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
- /* respond with [request-timeout] and a default text response */
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
- /* respond with [content-too-large] and a default text response */
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
- /* respond with [update-required] and a default text response */
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
- /* respond with [see-other] to the given target and a default text response (forces method GET; ensure target is properly URI encoded) */
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
- /* respond with [temporary-redirect] to the given target and a default text response (preserves method; ensure target is properly URI encoded) */
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
- /* respond with [permanent-redirect] to the given target and a default text response (preserves method; ensure target is properly URI encoded) */
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
- /* respond with html, can be built on by parent modules, sent once the request has been fully processed
1154
- * (default status is ok; for HEAD builds, no actual content will be constructed or estimated in size)
1155
- * automatically adds ClientConfig.responseCacheControl, if no other cache control is specified */
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
- /* [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);
1183
- * if a content size is provided, stream expects exactly this amount of bytes; if [dynamicEncode], the encoder will be dynamically negotiated
1184
- * based on the content; for a HEAD request, no encoding will be negotiated, no lengths verified, and the written data will just be drained
1185
- * (can immediately be ended using '.end()'); automatically adds ClientConfig.responseCacheControl, if no other cache control is specified */
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
- /* try to respond with the given file, return false, if the file does not exist (range aware, HEAD aware); specify [checkFreshness] to
1195
- * re-validate the file stats on disk before serving from cache; the media type can be overwritten (defaults to extracting media-type
1196
- * from the file-path); [encoding] describes the encoding of a pre-encoded file (warning: no checks against accepted encodings
1197
- * performed!); status will be [Ok], [partial-content], [not-modified] or according errors cache aware and etag/last-modified aware;
1198
- * automatically adds ClientConfig.fileCacheControl/ClientConfig.immutableCacheControl, if no other cache control is specified */
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
- /* [throws] receive the payload of given max length and write it directly to a file; will fail
1334
- * if the file already exists and delete the file if it could not be received in full
1335
- * automatically responds with given exceptions if the payload cannot be received properly or file operations fail */
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
- /* [no-throw but errors] receive the payload of given max length as a readable stream
1377
- * automatically responds with given exceptions if the payload cannot be received properly
1378
- * automatically drained if the readable stream is destroyed before reading all data */
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
- /* [throws] receive the payload of given max length as a single complete buffer
1383
- * automatically responds with given exceptions if the payload cannot be received properly */
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
- /* [throws] receive the payload of given max length as a single complete decoded string
1394
- * automatically responds with given exceptions if the payload cannot be received properly */
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
- /* marks the object as having been handled and returns a web socket or
1407
- * automatically responds with a corresponding error and returns null */
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
- const wss = (native.wss ?? new libWs.WebSocketServer({ noServer: true, clientTracking: false }));
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
- _extension;
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`, { extension: this._extension });
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`, { extension: this._extension });
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, { extension: this._extension });
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', { extension: this._extension });
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', { extension: this._extension });
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
- /* send data to the remote (ignored if connection is being closed) */
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
- /* close the web socket (promise resolved once the close callback has been fully invoked) */
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}`, { extension: this._extension });
1637
+ this.error(`Unhandled exception in ${event} listener: ${err.message}`, { identity: this._log.identity });
1614
1638
  }
1615
1639
  }
1616
1640
  }