@bjoernboss/mws 1.0.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/LICENSE.txt +29 -0
- package/README.md +301 -0
- package/dist/base.d.ts +198 -0
- package/dist/base.js +73 -0
- package/dist/builder.d.ts +58 -0
- package/dist/builder.js +146 -0
- package/dist/cache.d.ts +66 -0
- package/dist/cache.js +708 -0
- package/dist/client.d.ts +228 -0
- package/dist/client.js +1646 -0
- package/dist/handler.d.ts +119 -0
- package/dist/handler.js +542 -0
- package/dist/helper.d.ts +33 -0
- package/dist/helper.js +363 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +12 -0
- package/dist/log.d.ts +52 -0
- package/dist/log.js +288 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +257 -0
- package/package.json +46 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as libClient from "./client.js";
|
|
2
|
+
import * as libLog from "./log.js";
|
|
3
|
+
import * as libServer from "./server.js";
|
|
4
|
+
export type PathTranslation = Record<string, string | null>;
|
|
5
|
+
export interface AttachedModule {
|
|
6
|
+
handle(client: libClient.ClientRequest, options?: {
|
|
7
|
+
params?: object;
|
|
8
|
+
translate?: PathTranslation;
|
|
9
|
+
}): Promise<boolean>;
|
|
10
|
+
unlink(): Promise<void>;
|
|
11
|
+
module: ModuleHandler;
|
|
12
|
+
linked(): boolean;
|
|
13
|
+
unlinked(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare abstract class ModuleHandler extends libLog.Logger {
|
|
16
|
+
private _stopped;
|
|
17
|
+
private _config;
|
|
18
|
+
private _handling;
|
|
19
|
+
private _attachment;
|
|
20
|
+
protected constructor(name: string);
|
|
21
|
+
private _drainTaskQueue;
|
|
22
|
+
private _pushHandleTask;
|
|
23
|
+
private _checkIsModuleAttached;
|
|
24
|
+
private _processIncomingClient;
|
|
25
|
+
private _performAttachSelf;
|
|
26
|
+
private _performDetachSelf;
|
|
27
|
+
private _performAttachToParent;
|
|
28
|
+
_rootAttachToServer(server: libServer.Server, unlinked: () => void): AttachedModule;
|
|
29
|
+
protected handleInitialize(server: libServer.Server): Promise<void>;
|
|
30
|
+
protected abstract handleRequest(client: libClient.ClientRequest, params?: object): Promise<void>;
|
|
31
|
+
protected handleStop(): Promise<void>;
|
|
32
|
+
get moduleName(): string;
|
|
33
|
+
get moduleServer(): libServer.Server | null;
|
|
34
|
+
tagClients(tag: boolean): this;
|
|
35
|
+
tagString(tag: string): this;
|
|
36
|
+
stopOnDetach(stop: boolean): this;
|
|
37
|
+
linkModule(child: ModuleHandler, unlinked?: () => void, detail?: string): AttachedModule;
|
|
38
|
+
stop(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export declare function dispatch(map: Record<string, ModuleHandler>, options?: {
|
|
41
|
+
name?: string;
|
|
42
|
+
}): DispatchModule;
|
|
43
|
+
export declare class DispatchModule extends ModuleHandler {
|
|
44
|
+
private mapping;
|
|
45
|
+
constructor(map: Record<string, ModuleHandler>, options?: {
|
|
46
|
+
name?: string;
|
|
47
|
+
});
|
|
48
|
+
protected handleRequest(client: libClient.ClientRequest, _?: object): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export declare function host(map: Record<string, ModuleHandler>, options?: {
|
|
51
|
+
name?: string;
|
|
52
|
+
}): HostModule;
|
|
53
|
+
export declare class HostModule extends ModuleHandler {
|
|
54
|
+
private mapping;
|
|
55
|
+
constructor(map: Record<string, ModuleHandler>, options?: {
|
|
56
|
+
name?: string;
|
|
57
|
+
});
|
|
58
|
+
private testSubHost;
|
|
59
|
+
protected handleRequest(client: libClient.ClientRequest): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export declare function bind(handler: ModuleHandler, options?: {
|
|
62
|
+
params?: object;
|
|
63
|
+
translate?: PathTranslation;
|
|
64
|
+
name?: string;
|
|
65
|
+
}): BindModule;
|
|
66
|
+
export declare class BindModule extends ModuleHandler {
|
|
67
|
+
private handler;
|
|
68
|
+
private params?;
|
|
69
|
+
private translate?;
|
|
70
|
+
constructor(handler: ModuleHandler, options?: {
|
|
71
|
+
params?: object;
|
|
72
|
+
translate?: PathTranslation;
|
|
73
|
+
name?: string;
|
|
74
|
+
});
|
|
75
|
+
protected handleRequest(client: libClient.ClientRequest): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
export declare function check(handler: ModuleHandler, host: string | string[], options?: {
|
|
78
|
+
name?: string;
|
|
79
|
+
port?: number;
|
|
80
|
+
}): CheckModule;
|
|
81
|
+
export declare class CheckModule extends ModuleHandler {
|
|
82
|
+
private handler;
|
|
83
|
+
private port?;
|
|
84
|
+
private hosts;
|
|
85
|
+
constructor(handler: ModuleHandler, host: string | string[], options?: {
|
|
86
|
+
name?: string;
|
|
87
|
+
port?: number;
|
|
88
|
+
});
|
|
89
|
+
private respondBadEndpoint;
|
|
90
|
+
protected handleRequest(client: libClient.ClientRequest): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
export declare function lambda(options?: {
|
|
93
|
+
attach?: Record<string, ModuleHandler>;
|
|
94
|
+
setup?: CallbackSetup;
|
|
95
|
+
handle?: CallbackHandle;
|
|
96
|
+
stop?: CallbackStop;
|
|
97
|
+
name?: string;
|
|
98
|
+
}): LambdaModule;
|
|
99
|
+
export declare class LambdaModule extends ModuleHandler {
|
|
100
|
+
private setupLambda?;
|
|
101
|
+
private handleLambda?;
|
|
102
|
+
private stopLambda?;
|
|
103
|
+
private links;
|
|
104
|
+
private active;
|
|
105
|
+
constructor(options?: {
|
|
106
|
+
attach?: Record<string, ModuleHandler>;
|
|
107
|
+
setup?: CallbackSetup;
|
|
108
|
+
handle?: CallbackHandle;
|
|
109
|
+
stop?: CallbackStop;
|
|
110
|
+
name?: string;
|
|
111
|
+
});
|
|
112
|
+
protected handleInitialize(server: libServer.Server): Promise<void>;
|
|
113
|
+
protected handleRequest(client: libClient.ClientRequest, params?: object): Promise<void>;
|
|
114
|
+
protected handleStop(): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
export type CallbackSetup = (this: ModuleHandler, server: libServer.Server, links: Record<string, AttachedModule>) => Promise<void>;
|
|
117
|
+
export type CallbackHandle = (this: ModuleHandler, client: libClient.ClientRequest, params: object | undefined, links: Record<string, AttachedModule>) => Promise<void>;
|
|
118
|
+
export type CallbackStop = (this: ModuleHandler, links: Record<string, AttachedModule>) => Promise<void>;
|
|
119
|
+
//# sourceMappingURL=handler.d.ts.map
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import * as libLog from "./log.js";
|
|
2
|
+
import * as libBase from "./base.js";
|
|
3
|
+
import * as libServer from "./server.js";
|
|
4
|
+
/*
|
|
5
|
+
* Modules will be stopped by default, once it is fully unlinked from the server again.
|
|
6
|
+
* If a module is initialized, or stopped, the stop-handler is called.
|
|
7
|
+
* An initialization will always be followed by a stop-handler call.
|
|
8
|
+
* Any request will first happen after successful initialization, and will complete before stop-handlers.
|
|
9
|
+
* WebSockets will not be automatically closed and must be closed by the module.
|
|
10
|
+
* Modules can either be attached to other modules (allowing for recursive detaches/cleanups) or to the server.
|
|
11
|
+
*/
|
|
12
|
+
export class ModuleHandler extends libLog.Logger {
|
|
13
|
+
_stopped;
|
|
14
|
+
_config;
|
|
15
|
+
_handling;
|
|
16
|
+
_attachment;
|
|
17
|
+
constructor(name) {
|
|
18
|
+
super(name);
|
|
19
|
+
this._config = { name, tagClients: true, tagString: '', stopOnDetach: true };
|
|
20
|
+
this._handling = { active: new Set(), promise: null, resolver: () => { } };
|
|
21
|
+
this._stopped = null;
|
|
22
|
+
this._attachment = { links: new Set(), attached: false, server: null, task: { promise: null, order: [], count: 0, resolver: () => { } } };
|
|
23
|
+
}
|
|
24
|
+
async _drainTaskQueue(connections, order) {
|
|
25
|
+
const tasks = this._attachment.task;
|
|
26
|
+
/* wait for the tasks and connections to drain accordingly */
|
|
27
|
+
while (true) {
|
|
28
|
+
const promises = [];
|
|
29
|
+
if (tasks.promise != null)
|
|
30
|
+
promises.push(tasks.promise);
|
|
31
|
+
if (connections && this._handling.promise != null)
|
|
32
|
+
promises.push(this._handling.promise);
|
|
33
|
+
const index = (order == null ? tasks.order.length - 1 : tasks.order.indexOf(order) - 1);
|
|
34
|
+
if (index >= 0)
|
|
35
|
+
promises.push(tasks.order[index]);
|
|
36
|
+
if (promises.length == 0)
|
|
37
|
+
return;
|
|
38
|
+
await Promise.all(promises);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
_pushHandleTask(promise) {
|
|
42
|
+
const task = this._attachment.task;
|
|
43
|
+
if (task.count++ == 0)
|
|
44
|
+
task.promise = new Promise((res) => task.resolver = res);
|
|
45
|
+
promise.then(() => {
|
|
46
|
+
if (--task.count == 0) {
|
|
47
|
+
task.promise = null;
|
|
48
|
+
task.resolver();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
_checkIsModuleAttached(handler) {
|
|
53
|
+
for (const entry of handler._attachment.links) {
|
|
54
|
+
if (entry.parent == handler)
|
|
55
|
+
continue;
|
|
56
|
+
if (entry.parent != null && !entry.parent._attachment.attached)
|
|
57
|
+
continue;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
async _processIncomingClient(client, params, translate) {
|
|
63
|
+
/* check if the client is already being handled or has already
|
|
64
|
+
* been handled and otherwise process any outstanding tasks */
|
|
65
|
+
if (this._handling.active.has(client) || client.claimed)
|
|
66
|
+
return client.claimed;
|
|
67
|
+
await this._drainTaskQueue(false, null);
|
|
68
|
+
/* ensure that the handler is attached and push the mapping and path (client will validate it) */
|
|
69
|
+
if (!this._attachment.attached)
|
|
70
|
+
return client.claimed;
|
|
71
|
+
const mapping = translate ?? { '/': '/' };
|
|
72
|
+
const logTag = (this._config.tagClients ? (this._config.tagString == '' ? this._config.name : this._config.tagString) : '');
|
|
73
|
+
const snapshot = client._pushTranslation(mapping, logTag);
|
|
74
|
+
if (snapshot == null)
|
|
75
|
+
return client.claimed;
|
|
76
|
+
/* check if the cleanup handler needs to be setup and push the client in general to be handled right now */
|
|
77
|
+
if (this._handling.active.size == 0)
|
|
78
|
+
this._handling.promise = new Promise((res) => this._handling.resolver = res);
|
|
79
|
+
this._handling.active.add(client);
|
|
80
|
+
/* handle the client but ensure to catch any exceptions and re-throw them once the handler has been cleaned up */
|
|
81
|
+
let error = null;
|
|
82
|
+
try {
|
|
83
|
+
await this.handleRequest(client, params);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
error = err;
|
|
87
|
+
}
|
|
88
|
+
/* restore the context, clear the handling promise and remove the client from actively handled clients */
|
|
89
|
+
this._handling.active.delete(client);
|
|
90
|
+
client._restoreSnapshot(snapshot);
|
|
91
|
+
if (this._handling.active.size == 0) {
|
|
92
|
+
this._handling.promise = null;
|
|
93
|
+
this._handling.resolver();
|
|
94
|
+
}
|
|
95
|
+
if (error != null)
|
|
96
|
+
throw error;
|
|
97
|
+
return client.claimed;
|
|
98
|
+
}
|
|
99
|
+
async _performAttachSelf(server) {
|
|
100
|
+
const firstAttachment = (this._attachment.server == null);
|
|
101
|
+
this._attachment.attached = true;
|
|
102
|
+
this._attachment.server = server;
|
|
103
|
+
/* push the next ordered promise, to ensure the attach is fully performed before the next detach */
|
|
104
|
+
let taskResolver = () => { };
|
|
105
|
+
const taskPromise = new Promise((res) => taskResolver = res);
|
|
106
|
+
this._attachment.task.order.push(taskPromise);
|
|
107
|
+
/* trigger any attach calls (may push new independent tasks) */
|
|
108
|
+
for (const link of this._attachment.links) {
|
|
109
|
+
if (link.setup != null)
|
|
110
|
+
link.setup(taskPromise);
|
|
111
|
+
}
|
|
112
|
+
/* drain the current task queue and convert the task back to the unordered next step */
|
|
113
|
+
await this._drainTaskQueue(true, taskPromise);
|
|
114
|
+
this._attachment.task.order.shift();
|
|
115
|
+
this._pushHandleTask(taskPromise);
|
|
116
|
+
/* notify the module itself about the initialization */
|
|
117
|
+
try {
|
|
118
|
+
if (firstAttachment)
|
|
119
|
+
await this.handleInitialize(this._attachment.server);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
this.error(`Unhandled exception while initializing: ${err.message}`);
|
|
123
|
+
this.stop();
|
|
124
|
+
}
|
|
125
|
+
taskResolver();
|
|
126
|
+
return firstAttachment;
|
|
127
|
+
}
|
|
128
|
+
async _performDetachSelf() {
|
|
129
|
+
if (!this._attachment.attached)
|
|
130
|
+
return;
|
|
131
|
+
this._attachment.attached = false;
|
|
132
|
+
/* push the next ordered promise, to ensure the detach is fully performed before the next attach */
|
|
133
|
+
let taskResolver = () => { };
|
|
134
|
+
const taskPromise = new Promise((res) => taskResolver = res);
|
|
135
|
+
this._attachment.task.order.push(taskPromise);
|
|
136
|
+
/* trigger the detach of all now detached children */
|
|
137
|
+
for (const entry of this._attachment.links) {
|
|
138
|
+
if (entry.parent == this && !this._checkIsModuleAttached(entry.child))
|
|
139
|
+
this._pushHandleTask(entry.child._performDetachSelf());
|
|
140
|
+
}
|
|
141
|
+
/* check if the module should be stopped due to being detached */
|
|
142
|
+
if (this._config.stopOnDetach)
|
|
143
|
+
this.stop();
|
|
144
|
+
/* wait for any current tasks and connections to complete and remove the ordered task */
|
|
145
|
+
await this._drainTaskQueue(false, taskPromise);
|
|
146
|
+
this._attachment.task.order.shift();
|
|
147
|
+
taskResolver();
|
|
148
|
+
/* check if the module was stopped and await the proper stopping */
|
|
149
|
+
if (this._stopped != null)
|
|
150
|
+
await this._stopped;
|
|
151
|
+
}
|
|
152
|
+
_performAttachToParent(parent, unlinked, detail) {
|
|
153
|
+
const recSearchParents = (parent) => {
|
|
154
|
+
if (parent == this)
|
|
155
|
+
return false;
|
|
156
|
+
for (const entry of parent._attachment.links) {
|
|
157
|
+
if (entry.child == parent && entry.parent != null && !recSearchParents(entry.parent))
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
};
|
|
162
|
+
const validateLinkState = (parentServer) => {
|
|
163
|
+
if (this._stopped != null) {
|
|
164
|
+
this.warning(`Stopped module cannot be attached to [${parent.logIdentity}]`);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const thisServer = this._attachment.server;
|
|
168
|
+
if (thisServer != null && parentServer != null && thisServer != parentServer) {
|
|
169
|
+
this.warning(`Module attached to server [${thisServer.logIdentity}] cannot be attached to server [${parentServer.logIdentity}]`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (parent instanceof libServer.Server)
|
|
173
|
+
return true;
|
|
174
|
+
if (parent._stopped != null) {
|
|
175
|
+
this.warning(`Module cannot be attached to stopped module [${parent.logIdentity}]`);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
if (!recSearchParents(parent)) {
|
|
179
|
+
this.warning('Module cannot be directly or indirectly attach to itself');
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
};
|
|
184
|
+
if (detail != '')
|
|
185
|
+
detail = `: ${detail}`;
|
|
186
|
+
/* setup the link object and its creation and cleanup methods */
|
|
187
|
+
let unlinkResolver = () => { }, taskResolver = () => { };
|
|
188
|
+
const unlinkPromise = new Promise((res) => unlinkResolver = res);
|
|
189
|
+
const taskPromise = new Promise((res) => taskResolver = res);
|
|
190
|
+
let logged = false, stopping = false;
|
|
191
|
+
const link = {
|
|
192
|
+
parent: (parent instanceof libServer.Server ? null : parent),
|
|
193
|
+
child: this,
|
|
194
|
+
setup: async (realized) => {
|
|
195
|
+
if (stopping)
|
|
196
|
+
return;
|
|
197
|
+
const parentServer = (parent instanceof libServer.Server ? parent : parent._attachment.server);
|
|
198
|
+
/* check if the attachment is possible in theory */
|
|
199
|
+
let firstAttachment = false;
|
|
200
|
+
if (!validateLinkState(parentServer))
|
|
201
|
+
link.cleanup();
|
|
202
|
+
/* check if the attachment is still certain (uncertainty can only happen once on the first time
|
|
203
|
+
* attaching, as the second execution is performed after the parent has just been attached) */
|
|
204
|
+
else if (parentServer != null) {
|
|
205
|
+
link.setup = null;
|
|
206
|
+
if (!this._attachment.attached) {
|
|
207
|
+
if (realized != null)
|
|
208
|
+
this._pushHandleTask(realized);
|
|
209
|
+
firstAttachment = await this._performAttachSelf(parentServer);
|
|
210
|
+
}
|
|
211
|
+
logged = !stopping;
|
|
212
|
+
if (logged) {
|
|
213
|
+
if (parent instanceof libServer.Server || !firstAttachment)
|
|
214
|
+
this.info(`Attached to [${parent.logIdentity}]${detail}`);
|
|
215
|
+
else
|
|
216
|
+
this.info(`Attached to [${parent.logIdentity}] and server [${parentServer.logIdentity}]${detail}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
cleanup: async () => {
|
|
221
|
+
if (stopping)
|
|
222
|
+
return unlinkPromise;
|
|
223
|
+
stopping = true, link.setup = null;
|
|
224
|
+
if (logged)
|
|
225
|
+
this.info(`Detached from [${parent.logIdentity}]${detail}`);
|
|
226
|
+
/* remove the link from the parent and add its cleanup to its task list (cleanup calls are only linked as task to the parent,
|
|
227
|
+
* as the child does not care for them; this implies that the module's stop method itself does not await them either) */
|
|
228
|
+
if (!(parent instanceof libServer.Server)) {
|
|
229
|
+
parent._attachment.links.delete(link);
|
|
230
|
+
parent._pushHandleTask(taskPromise);
|
|
231
|
+
}
|
|
232
|
+
/* remove the link from this handler and check if the object should be detached */
|
|
233
|
+
this._attachment.links.delete(link);
|
|
234
|
+
if (!this._checkIsModuleAttached(this))
|
|
235
|
+
await this._performDetachSelf();
|
|
236
|
+
/* start the actual unlink call */
|
|
237
|
+
process.nextTick(async () => {
|
|
238
|
+
try {
|
|
239
|
+
unlinked();
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
this.error(`Unhandled exception while unlinking: ${err.message}`);
|
|
243
|
+
}
|
|
244
|
+
taskResolver();
|
|
245
|
+
/* check if the module was stopped, in which case the stop should be awaited before resolving the cleanup promise */
|
|
246
|
+
if (this._stopped != null)
|
|
247
|
+
await this._stopped;
|
|
248
|
+
unlinkResolver();
|
|
249
|
+
});
|
|
250
|
+
return unlinkPromise;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
/* register the link between the parent and this handler */
|
|
254
|
+
this._attachment.links.add(link);
|
|
255
|
+
if (!(parent instanceof libServer.Server))
|
|
256
|
+
parent._attachment.links.add(link);
|
|
257
|
+
/* try to immediately perform the initial load and setup the attach module interface */
|
|
258
|
+
link.setup(null);
|
|
259
|
+
return {
|
|
260
|
+
handle: (client, options) => (stopping ? Promise.resolve(client.claimed) : this._processIncomingClient(client, options?.params, options?.translate)),
|
|
261
|
+
unlink: () => link.cleanup(),
|
|
262
|
+
module: this,
|
|
263
|
+
linked: () => !stopping,
|
|
264
|
+
unlinked: () => unlinkPromise
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
_rootAttachToServer(server, unlinked) {
|
|
268
|
+
return this._performAttachToParent(server, unlinked, '');
|
|
269
|
+
}
|
|
270
|
+
/* module is attached directly or indirectly to the server (will be the first call being performed before any other calls; will only be called once) */
|
|
271
|
+
async handleInitialize(server) { }
|
|
272
|
+
/* module has been stopped (all clients are guaranteed to have left, but accepted WebSockets will be left intact and
|
|
273
|
+
* must be closed manually by this module; will only be called once; module should cleanup any resources and timers) */
|
|
274
|
+
async handleStop() { }
|
|
275
|
+
/* name of the module */
|
|
276
|
+
get moduleName() {
|
|
277
|
+
return this._config.name;
|
|
278
|
+
}
|
|
279
|
+
/* server the module has been attached to (null if not yet attached) */
|
|
280
|
+
get moduleServer() {
|
|
281
|
+
return this._attachment.server;
|
|
282
|
+
}
|
|
283
|
+
/* enable or disable the module tagging the logging of clients [default: true] */
|
|
284
|
+
tagClients(tag) {
|
|
285
|
+
this._config.tagClients = tag;
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
/* set the tagging string the module should use in the logging of clients with empty string being module name [default: ''] */
|
|
289
|
+
tagString(tag) {
|
|
290
|
+
this._config.tagString = tag;
|
|
291
|
+
return this;
|
|
292
|
+
}
|
|
293
|
+
/* enable or disable the module being stopped once detached the next time from the server [default: true] */
|
|
294
|
+
stopOnDetach(stop) {
|
|
295
|
+
this._config.stopOnDetach = stop;
|
|
296
|
+
return this;
|
|
297
|
+
}
|
|
298
|
+
/* link the child to this module (unlinked will be called once the child has been removed or is identified to not
|
|
299
|
+
* be suited for this module; will not be called if this module is not attached or will not be attached anymore;
|
|
300
|
+
* unless already destroyed; detail is an additional information to be logged upon linking and unlinking) */
|
|
301
|
+
linkModule(child, unlinked, detail) {
|
|
302
|
+
return child._performAttachToParent(this, (unlinked == null ? () => { } : unlinked), detail ?? '');
|
|
303
|
+
}
|
|
304
|
+
/* close any connections to the module and stop the module itself (stopping must not
|
|
305
|
+
* be awaited while stopping itself or a parent as this can result in deadlocks) */
|
|
306
|
+
async stop() {
|
|
307
|
+
if (this._stopped != null)
|
|
308
|
+
return this._stopped;
|
|
309
|
+
/* setup the stopped-promise (before performing any operations, as the first operations might otherwise
|
|
310
|
+
* call stop again, without the stopped-promise value being set, thereby recursively stopping again) */
|
|
311
|
+
let resolver = () => { };
|
|
312
|
+
this._stopped = new Promise((res) => resolver = res);
|
|
313
|
+
/* kill any connections and kill all links and drain the remaining task queue
|
|
314
|
+
* (will not await the unlinked calls of links where this element is the child) */
|
|
315
|
+
this._performDetachSelf();
|
|
316
|
+
for (const client of this._handling.active)
|
|
317
|
+
client.killConnection('Module detached');
|
|
318
|
+
for (const link of this._attachment.links)
|
|
319
|
+
link.cleanup();
|
|
320
|
+
this.info('Handler stopped');
|
|
321
|
+
await this._drainTaskQueue(true, null);
|
|
322
|
+
try {
|
|
323
|
+
await this.handleStop();
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
this.error(`Unhandled exception while stopping: ${err.message}`);
|
|
327
|
+
}
|
|
328
|
+
resolver();
|
|
329
|
+
return this._stopped;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/*
|
|
333
|
+
* Simple module handler implementation, which dispatches requests to different children based on the request path (longest match).
|
|
334
|
+
* Stops itself once all children have been unlinked. Own parameters are not forwarded.
|
|
335
|
+
*/
|
|
336
|
+
export function dispatch(map, options) {
|
|
337
|
+
return new DispatchModule(map, options);
|
|
338
|
+
}
|
|
339
|
+
export class DispatchModule extends ModuleHandler {
|
|
340
|
+
mapping;
|
|
341
|
+
constructor(map, options) {
|
|
342
|
+
super(options?.name ?? 'dispatch');
|
|
343
|
+
this.mapping = {};
|
|
344
|
+
for (const [key, handler] of Object.entries(map)) {
|
|
345
|
+
if (key in this.mapping) {
|
|
346
|
+
this.warning(`Ignoring duplicate mapping [${key}] by [${handler.logIdentity}]`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
this.mapping[key] = this.linkModule(handler, () => {
|
|
350
|
+
delete this.mapping[key];
|
|
351
|
+
if (Object.keys(this.mapping).length == 0)
|
|
352
|
+
this.stop();
|
|
353
|
+
}, `Bound to path [${key}]`);
|
|
354
|
+
}
|
|
355
|
+
if (Object.keys(this.mapping).length == 0)
|
|
356
|
+
this.stop();
|
|
357
|
+
}
|
|
358
|
+
async handleRequest(client, _) {
|
|
359
|
+
let bestMatch = null;
|
|
360
|
+
/* iterate over the mappings and look for the corresponding best handler */
|
|
361
|
+
for (const path in this.mapping) {
|
|
362
|
+
if (!client.isSubPathOf(path))
|
|
363
|
+
continue;
|
|
364
|
+
if (bestMatch == null || bestMatch.length < path.length)
|
|
365
|
+
bestMatch = path;
|
|
366
|
+
}
|
|
367
|
+
if (bestMatch != null) {
|
|
368
|
+
client.trace(`Client dispatched to handler [${this.mapping[bestMatch].module.logIdentity}] for path [${bestMatch}]`);
|
|
369
|
+
await this.mapping[bestMatch].handle(client, { translate: { [bestMatch]: '/' } });
|
|
370
|
+
}
|
|
371
|
+
else
|
|
372
|
+
client.trace(`Request cannot be dispatched`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/*
|
|
376
|
+
* Simple module handler implementation, which dispatches requests to different children based on the request hostname (longest match).
|
|
377
|
+
* Stops itself once all children have been unlinked. Own parameters are not forwarded.
|
|
378
|
+
*/
|
|
379
|
+
export function host(map, options) {
|
|
380
|
+
return new HostModule(map, options);
|
|
381
|
+
}
|
|
382
|
+
export class HostModule extends ModuleHandler {
|
|
383
|
+
mapping;
|
|
384
|
+
constructor(map, options) {
|
|
385
|
+
super(options?.name ?? 'host');
|
|
386
|
+
this.mapping = {};
|
|
387
|
+
for (const [host, handler] of Object.entries(map)) {
|
|
388
|
+
if (host in this.mapping) {
|
|
389
|
+
this.warning(`Ignoring duplicate mapping of host [${host}] by [${handler.logIdentity}]`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
this.mapping[host] = this.linkModule(handler, () => {
|
|
393
|
+
delete this.mapping[host];
|
|
394
|
+
if (Object.keys(this.mapping).length == 0)
|
|
395
|
+
this.stop();
|
|
396
|
+
}, `Bound to host [${host}]`);
|
|
397
|
+
}
|
|
398
|
+
if (Object.keys(this.mapping).length == 0)
|
|
399
|
+
this.stop();
|
|
400
|
+
}
|
|
401
|
+
testSubHost(host, test) {
|
|
402
|
+
if (host.length > test.length)
|
|
403
|
+
return false;
|
|
404
|
+
if (host.length == test.length)
|
|
405
|
+
return (host == test);
|
|
406
|
+
if (!test.endsWith(host))
|
|
407
|
+
return false;
|
|
408
|
+
return (test[test.length - host.length - 1] == '.' || host.startsWith('.') || host == '');
|
|
409
|
+
}
|
|
410
|
+
async handleRequest(client) {
|
|
411
|
+
let bestMatch = null;
|
|
412
|
+
/* iterate over the mappings and look for the corresponding best handler */
|
|
413
|
+
for (const host in this.mapping) {
|
|
414
|
+
if (!this.testSubHost(host, client.url.hostname))
|
|
415
|
+
continue;
|
|
416
|
+
if (bestMatch == null || bestMatch.length < host.length)
|
|
417
|
+
bestMatch = host;
|
|
418
|
+
}
|
|
419
|
+
if (bestMatch != null) {
|
|
420
|
+
client.trace(`Client dispatched to handler [${this.mapping[bestMatch].module.logIdentity}] for host [${bestMatch}]`);
|
|
421
|
+
await this.mapping[bestMatch].handle(client);
|
|
422
|
+
}
|
|
423
|
+
else
|
|
424
|
+
client.trace(`Request cannot be dispatched`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/*
|
|
428
|
+
* Simple module interface implementation, which forwards any requests to a child handler
|
|
429
|
+
* with optional parameter and translation binding. Stops itself once the child has been unlinked.
|
|
430
|
+
* Own parameters are not forwarded.
|
|
431
|
+
*/
|
|
432
|
+
export function bind(handler, options) {
|
|
433
|
+
return new BindModule(handler, options);
|
|
434
|
+
}
|
|
435
|
+
export class BindModule extends ModuleHandler {
|
|
436
|
+
handler;
|
|
437
|
+
params;
|
|
438
|
+
translate;
|
|
439
|
+
constructor(handler, options) {
|
|
440
|
+
super(options?.name ?? 'bind');
|
|
441
|
+
this.handler = this.linkModule(handler, () => this.stop());
|
|
442
|
+
this.params = options?.params;
|
|
443
|
+
if (options?.translate != null)
|
|
444
|
+
this.translate = { ...options?.translate };
|
|
445
|
+
}
|
|
446
|
+
async handleRequest(client) {
|
|
447
|
+
await this.handler.handle(client, { params: this.params, translate: this.translate });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/*
|
|
451
|
+
* Simple module interface implementation, which validates the connected host and port before forwarding the client.
|
|
452
|
+
* Stops itself once the child has been unlinked. Own parameters are not forwarded.
|
|
453
|
+
*/
|
|
454
|
+
export function check(handler, host, options) {
|
|
455
|
+
return new CheckModule(handler, host, options);
|
|
456
|
+
}
|
|
457
|
+
export class CheckModule extends ModuleHandler {
|
|
458
|
+
handler;
|
|
459
|
+
port;
|
|
460
|
+
hosts;
|
|
461
|
+
constructor(handler, host, options) {
|
|
462
|
+
super(options?.name ?? 'check');
|
|
463
|
+
this.handler = this.linkModule(handler, () => this.stop());
|
|
464
|
+
this.hosts = (typeof host == 'string' ? [host] : host);
|
|
465
|
+
this.port = options?.port;
|
|
466
|
+
}
|
|
467
|
+
respondBadEndpoint(client) {
|
|
468
|
+
client.respond(`No resource found at [${client.url.host}]:[${client.url.pathname}]`, {
|
|
469
|
+
status: libBase.Status.NotFound,
|
|
470
|
+
media: libBase.Media.Text,
|
|
471
|
+
headers: { 'Connection': 'close' }
|
|
472
|
+
});
|
|
473
|
+
client.killConnection('Invalid endpoint description');
|
|
474
|
+
}
|
|
475
|
+
async handleRequest(client) {
|
|
476
|
+
let matches = false;
|
|
477
|
+
/* validate that the host matches */
|
|
478
|
+
for (const host of this.hosts) {
|
|
479
|
+
if (host == client.url.hostname) {
|
|
480
|
+
matches = true;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (!matches) {
|
|
485
|
+
client.warning(`Hostname [${client.url.hostname}] not allowed for this endpoint`);
|
|
486
|
+
return this.respondBadEndpoint(client);
|
|
487
|
+
}
|
|
488
|
+
/* validate that the port matches */
|
|
489
|
+
if (this.port != null && client.url.port) {
|
|
490
|
+
if (parseInt(client.url.port, 10) != this.port) {
|
|
491
|
+
client.warning(`Host [${client.url.port}] port does not match [${this.port}]`);
|
|
492
|
+
return this.respondBadEndpoint(client);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
await this.handler.handle(client);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/*
|
|
499
|
+
* Simple module handler implementation, which allows requests to be handled by lambdas, and child modules to be attached.
|
|
500
|
+
* Stops itself once all children have been unlinked. Forwards parameter to lambda functions.
|
|
501
|
+
*/
|
|
502
|
+
export function lambda(options) {
|
|
503
|
+
return new LambdaModule(options);
|
|
504
|
+
}
|
|
505
|
+
export class LambdaModule extends ModuleHandler {
|
|
506
|
+
setupLambda;
|
|
507
|
+
handleLambda;
|
|
508
|
+
stopLambda;
|
|
509
|
+
links;
|
|
510
|
+
active;
|
|
511
|
+
constructor(options) {
|
|
512
|
+
super(options?.name ?? 'lambda');
|
|
513
|
+
this.setupLambda = options?.setup;
|
|
514
|
+
this.handleLambda = options?.handle;
|
|
515
|
+
this.stopLambda = options?.stop;
|
|
516
|
+
this.links = {};
|
|
517
|
+
this.active = 0;
|
|
518
|
+
if (options?.attach != null)
|
|
519
|
+
for (const name in options.attach) {
|
|
520
|
+
++this.active;
|
|
521
|
+
this.links[name] = this.linkModule(options.attach[name], () => {
|
|
522
|
+
if (--this.active == 0)
|
|
523
|
+
this.stop();
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (options?.attach != null && this.active == 0)
|
|
527
|
+
this.stop();
|
|
528
|
+
}
|
|
529
|
+
async handleInitialize(server) {
|
|
530
|
+
if (this.setupLambda != null)
|
|
531
|
+
await this.setupLambda(server, this.links);
|
|
532
|
+
}
|
|
533
|
+
async handleRequest(client, params) {
|
|
534
|
+
if (this.handleLambda != null)
|
|
535
|
+
await this.handleLambda(client, params, this.links);
|
|
536
|
+
}
|
|
537
|
+
async handleStop() {
|
|
538
|
+
if (this.stopLambda != null)
|
|
539
|
+
await this.stopLambda(this.links);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
//# sourceMappingURL=handler.js.map
|
package/dist/helper.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as libBase from "./base.js";
|
|
2
|
+
export declare function lookupEncoding(name: string): libBase.EncodingType | null;
|
|
3
|
+
export declare function supportedEncodingNames(): string[];
|
|
4
|
+
export declare function lookupMediaTypeFromFile(filePath: string): libBase.MediaType | null;
|
|
5
|
+
export declare function buildMediaTypeIdentifier(media: libBase.MediaType): string;
|
|
6
|
+
export declare function negotiateEncoding(accept: string | null, atLeastSize: number | null, media: libBase.MediaType): libBase.EncodingType | null;
|
|
7
|
+
export declare enum RangeState {
|
|
8
|
+
noRange = 0,
|
|
9
|
+
valid = 1,
|
|
10
|
+
issue = 2,
|
|
11
|
+
malformed = 3
|
|
12
|
+
}
|
|
13
|
+
export declare function parseRangeHeader(range: string | null, fileSize: number): {
|
|
14
|
+
first: number;
|
|
15
|
+
last: number;
|
|
16
|
+
state: RangeState;
|
|
17
|
+
};
|
|
18
|
+
export declare function etagMatchesList(etag: string, header: string | null, strong: boolean): boolean;
|
|
19
|
+
export declare function timestampCompare(a: string, b: string): number | null;
|
|
20
|
+
export declare function splitAndTrimList(content: string | null, separator: string, quotesAware: boolean): string[];
|
|
21
|
+
export declare function escapeHtml(content: string): string;
|
|
22
|
+
export declare function expandPlaceholders(content: string, args: Record<string, string>, htmlEscape: boolean): string;
|
|
23
|
+
export declare function escapePlaceholders(content: string): string;
|
|
24
|
+
export declare function sanitize(path: string, relative: boolean): string;
|
|
25
|
+
export declare function joinSanitized(a: string, b: string): string;
|
|
26
|
+
export declare function isSubPath(base: string, path: string): boolean;
|
|
27
|
+
export declare function isInside(base: string, path: string): boolean;
|
|
28
|
+
export declare function childPath(base: string, path: string): string;
|
|
29
|
+
export declare function rebasePath(oldBase: string, newBase: string, path: string): string;
|
|
30
|
+
export declare function createPathLocation(path: string): (path: string) => string;
|
|
31
|
+
export declare function createPathSelf(urlFilePath: string, path?: string | null): (path: string) => string;
|
|
32
|
+
export declare function splitFilePath(path: string): [string, string, string];
|
|
33
|
+
//# sourceMappingURL=helper.d.ts.map
|