@furystack/rest-service 11.0.3 → 11.0.4
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/CHANGELOG.md +18 -0
- package/esm/path-processor.d.ts.map +1 -1
- package/esm/path-processor.js +3 -1
- package/esm/path-processor.js.map +1 -1
- package/esm/server-manager.d.ts +2 -1
- package/esm/server-manager.d.ts.map +1 -1
- package/esm/server-manager.js +57 -54
- package/esm/server-manager.js.map +1 -1
- package/package.json +11 -12
- package/src/path-processor.ts +3 -1
- package/src/server-manager.ts +64 -56
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [11.0.4] - 2026-02-11
|
|
4
|
+
|
|
5
|
+
### 🐛 Bug Fixes
|
|
6
|
+
|
|
7
|
+
- Preserve original error cause in `PathProcessor.validateUrl()` using `{ cause: error }` for better error traceability
|
|
8
|
+
|
|
9
|
+
### ♻️ Refactoring
|
|
10
|
+
|
|
11
|
+
- Replaced semaphore-based server creation lock with a `pendingCreates` Map for deduplicating concurrent `getOrCreate()` calls. In-flight server creation promises are now reused instead of serialized behind a semaphore.
|
|
12
|
+
- Simplified `[Symbol.asyncDispose]()` — disposal now awaits pending server creations directly instead of waiting on a semaphore lock with a timeout.
|
|
13
|
+
|
|
14
|
+
### ⬆️ Dependencies
|
|
15
|
+
|
|
16
|
+
- Bump `vitest` from `^4.0.17` to `^4.0.18`
|
|
17
|
+
- Bump `@types/node` from `^25.0.10` to `^25.2.3`
|
|
18
|
+
- Removed `semaphore-async-await` dependency
|
|
19
|
+
- Updated internal dependencies
|
|
20
|
+
|
|
3
21
|
## [11.0.3] - 2026-02-09
|
|
4
22
|
|
|
5
23
|
### ⬆️ Dependencies
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-processor.d.ts","sourceRoot":"","sources":["../src/path-processor.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,qBAAa,aAAa;IACxB;;;OAGG;IACI,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,SAAQ,GAAG,GAAG;
|
|
1
|
+
{"version":3,"file":"path-processor.d.ts","sourceRoot":"","sources":["../src/path-processor.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,qBAAa,aAAa;IACxB;;;OAGG;IACI,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,SAAQ,GAAG,GAAG;IAUrD;;;OAGG;IACI,oBAAoB,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI;IAM3C;;OAEG;IACI,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM;IAI3E;;OAEG;IACI,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAI3F;;OAEG;IACI,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAIxE;;;OAGG;IACI,UAAU,CACf,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GACrC,MAAM;CAUV"}
|
package/esm/path-processor.js
CHANGED
|
@@ -12,7 +12,9 @@ export class PathProcessor {
|
|
|
12
12
|
return new URL(url);
|
|
13
13
|
}
|
|
14
14
|
catch (error) {
|
|
15
|
-
throw new Error(`Invalid ${context}: ${url}${error instanceof Error ? ` (${error.message})` : ''}
|
|
15
|
+
throw new Error(`Invalid ${context}: ${url}${error instanceof Error ? ` (${error.message})` : ''}`, {
|
|
16
|
+
cause: error,
|
|
17
|
+
});
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-processor.js","sourceRoot":"","sources":["../src/path-processor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C;;GAEG;AACH,MAAM,OAAO,aAAa;IACxB;;;OAGG;IACI,WAAW,CAAC,GAAW,EAAE,OAAO,GAAG,KAAK;QAC7C,IAAI,CAAC;YACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,WAAW,OAAO,KAAK,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"path-processor.js","sourceRoot":"","sources":["../src/path-processor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C;;GAEG;AACH,MAAM,OAAO,aAAa;IACxB;;;OAGG;IACI,WAAW,CAAC,GAAW,EAAE,OAAO,GAAG,KAAK;QAC7C,IAAI,CAAC;YACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,WAAW,OAAO,KAAK,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE;gBAClG,KAAK,EAAE,KAAK;aACb,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED;;;OAGG;IACI,oBAAoB,CAAC,GAAQ;QAClC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,mCAAmC,GAAG,CAAC,QAAQ,0BAA0B,CAAC,CAAA;QAC5F,CAAC;IACH,CAAC;IAED;;OAEG;IACI,iBAAiB,CAAC,UAAkB,EAAE,aAAqB;QAChE,OAAO,UAAU,CAAC,WAAW,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;IAC1D,CAAC;IAED;;OAEG;IACI,gBAAgB,CAAC,UAAkB,EAAE,WAAsC;QAChF,OAAO,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAA;IAC3D,CAAC;IAED;;OAEG;IACI,cAAc,CAAC,aAAqB,EAAE,UAAkB;QAC7D,OAAO,UAAU,CAAC,OAAO,CAAC,aAAa,EAAE,UAAU,CAAC,CAAA;IACtD,CAAC;IAED;;;OAGG;IACI,UAAU,CACf,UAAkB,EAClB,aAAqB,EACrB,aAAqB,EACrB,WAAsC;QAEtC,MAAM,UAAU,GAAG,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;QACpE,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAA;QACjE,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,aAAa,EAAE,UAAU,CAAC,CAAA;QAEhE,oCAAoC;QACpC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,YAAY,CAAC,CAAA;QAEzC,OAAO,SAAS,CAAA;IAClB,CAAC;CACF"}
|
package/esm/server-manager.d.ts
CHANGED
|
@@ -29,10 +29,11 @@ export declare class ServerManager extends EventHub<{
|
|
|
29
29
|
static DEFAULT_HOST: string;
|
|
30
30
|
servers: Map<string, ServerRecord>;
|
|
31
31
|
private openedSockets;
|
|
32
|
-
private readonly
|
|
32
|
+
private readonly pendingCreates;
|
|
33
33
|
private getHostUrl;
|
|
34
34
|
private onConnection;
|
|
35
35
|
[Symbol.asyncDispose](): Promise<void>;
|
|
36
36
|
getOrCreate(options: ServerOptions): Promise<ServerRecord>;
|
|
37
|
+
private createServer;
|
|
37
38
|
}
|
|
38
39
|
//# sourceMappingURL=server-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-manager.d.ts","sourceRoot":"","sources":["../src/server-manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"server-manager.d.ts","sourceRoot":"","sources":["../src/server-manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,MAAM,CAAA;AAGnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEpC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,eAAe,CAAA;IACpB,GAAG,EAAE,cAAc,CAAA;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,eAAe,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,OAAO,CAAA;IAC3C,SAAS,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClD;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,SAAS,EAAE,CAAA;CAClB;AAED,qBACa,aACX,SAAQ,QAAQ,CAAC;IAAE,eAAe,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC,CAAA;CAAE,CACjG,YAAW,eAAe;IAE1B,OAAc,YAAY,SAAc;IACjC,OAAO,4BAAkC;IAChD,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,UAAU,CAC0D;IAE5E,OAAO,CAAC,YAAY,CAGnB;IACY,CAAC,MAAM,CAAC,YAAY,CAAC;IAgBrB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YAkBzD,YAAY;CAqC3B"}
|
package/esm/server-manager.js
CHANGED
|
@@ -8,77 +8,80 @@ var ServerManager_1;
|
|
|
8
8
|
import { Injectable } from '@furystack/inject';
|
|
9
9
|
import { EventHub } from '@furystack/utils';
|
|
10
10
|
import { createServer } from 'http';
|
|
11
|
-
import { Lock } from 'semaphore-async-await';
|
|
12
11
|
let ServerManager = class ServerManager extends EventHub {
|
|
13
12
|
static { ServerManager_1 = this; }
|
|
14
13
|
static DEFAULT_HOST = 'localhost';
|
|
15
14
|
servers = new Map();
|
|
16
15
|
openedSockets = new Set();
|
|
17
|
-
|
|
16
|
+
pendingCreates = new Map();
|
|
18
17
|
getHostUrl = (options) => `http://${options.hostName || ServerManager_1.DEFAULT_HOST}:${options.port}`;
|
|
19
18
|
onConnection = (socket) => {
|
|
20
19
|
this.openedSockets.add(socket);
|
|
21
20
|
socket.once('close', () => this.openedSockets.delete(socket));
|
|
22
21
|
};
|
|
23
22
|
async [Symbol.asyncDispose]() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})));
|
|
33
|
-
this.servers.clear();
|
|
34
|
-
this.listenLock.release();
|
|
35
|
-
super[Symbol.dispose]?.();
|
|
36
|
-
}
|
|
23
|
+
await Promise.allSettled([...this.pendingCreates.values()]);
|
|
24
|
+
this.openedSockets.forEach((s) => s.destroy());
|
|
25
|
+
await Promise.allSettled([...this.servers.values()].map((s) => new Promise((resolve, reject) => {
|
|
26
|
+
s.server.close((err) => (err ? reject(err) : resolve()));
|
|
27
|
+
s.server.off('connection', this.onConnection);
|
|
28
|
+
})));
|
|
29
|
+
this.servers.clear();
|
|
30
|
+
super[Symbol.dispose]?.();
|
|
37
31
|
}
|
|
38
32
|
async getOrCreate(options) {
|
|
39
33
|
const url = this.getHostUrl(options);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
server.listen(options.port, options.hostName);
|
|
73
|
-
this.servers.set(url, { server, apis });
|
|
74
|
-
});
|
|
75
|
-
}
|
|
34
|
+
const existing = this.servers.get(url);
|
|
35
|
+
if (existing) {
|
|
36
|
+
return existing;
|
|
37
|
+
}
|
|
38
|
+
const pending = this.pendingCreates.get(url);
|
|
39
|
+
if (pending) {
|
|
40
|
+
return pending;
|
|
41
|
+
}
|
|
42
|
+
const createPromise = this.createServer(url, options);
|
|
43
|
+
this.pendingCreates.set(url, createPromise);
|
|
44
|
+
return createPromise;
|
|
45
|
+
}
|
|
46
|
+
async createServer(url, options) {
|
|
47
|
+
const apis = [];
|
|
48
|
+
const server = createServer((req, res) => {
|
|
49
|
+
const apiMatch = apis.find((api) => api.shouldExec({ req, res }));
|
|
50
|
+
if (apiMatch) {
|
|
51
|
+
apiMatch.onRequest({ req, res }).catch((error) => {
|
|
52
|
+
this.emit('onRequestFailed', [error, req, res]);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
res.destroy();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
server.on('upgrade', (req, socket, head) => {
|
|
60
|
+
const apiMatch = apis.find((api) => api.shouldExec({ req, res: {} }));
|
|
61
|
+
if (apiMatch?.onUpgrade) {
|
|
62
|
+
apiMatch.onUpgrade({ req, socket, head }).catch((error) => {
|
|
63
|
+
this.emit('onRequestFailed', [error, req, {}]);
|
|
64
|
+
socket.destroy();
|
|
65
|
+
});
|
|
76
66
|
}
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
else {
|
|
68
|
+
socket.destroy();
|
|
79
69
|
}
|
|
70
|
+
});
|
|
71
|
+
server.on('connection', this.onConnection);
|
|
72
|
+
try {
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
server.on('listening', () => resolve());
|
|
75
|
+
server.on('error', (err) => reject(err));
|
|
76
|
+
server.listen(options.port, options.hostName);
|
|
77
|
+
});
|
|
78
|
+
const record = { server, apis };
|
|
79
|
+
this.servers.set(url, record);
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
this.pendingCreates.delete(url);
|
|
80
84
|
}
|
|
81
|
-
return this.servers.get(url);
|
|
82
85
|
}
|
|
83
86
|
};
|
|
84
87
|
ServerManager = ServerManager_1 = __decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-manager.js","sourceRoot":"","sources":["../src/server-manager.ts"],"names":[],"mappings":";;;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"server-manager.js","sourceRoot":"","sources":["../src/server-manager.ts"],"names":[],"mappings":";;;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;AAgC5B,IAAM,aAAa,GAAnB,MAAM,aACX,SAAQ,QAA0F;;IAG3F,MAAM,CAAC,YAAY,GAAG,WAAW,CAAA;IACjC,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAA;IACxC,aAAa,GAAG,IAAI,GAAG,EAAU,CAAA;IACxB,cAAc,GAAG,IAAI,GAAG,EAAiC,CAAA;IAClE,UAAU,GAAG,CAAC,OAAsB,EAAE,EAAE,CAC9C,UAAU,OAAO,CAAC,QAAQ,IAAI,eAAa,CAAC,YAAY,IAAI,OAAO,CAAC,IAAI,EAAE,CAAA;IAEpE,YAAY,GAAG,CAAC,MAAc,EAAE,EAAE;QACxC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAC9B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;IAC/D,CAAC,CAAA;IACM,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;QAChC,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;QAC3D,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9C,MAAM,OAAO,CAAC,UAAU,CACtB,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAC5B,CAAC,CAAC,EAAE,EAAE,CACJ,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACpC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;YACxD,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;QAC/C,CAAC,CAAC,CACL,CACF,CAAA;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;QACpB,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,EAAE,CAAA;IAC3B,CAAC;IAEM,KAAK,CAAC,WAAW,CAAC,OAAsB;QAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;QAEpC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACtC,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,QAAQ,CAAA;QACjB,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC5C,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QACrD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAC3C,OAAO,aAAa,CAAA;IACtB,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,GAAW,EAAE,OAAsB;QAC5D,MAAM,IAAI,GAAyB,EAAE,CAAA;QACrC,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;YACjE,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;oBAC/C,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;gBACjD,CAAC,CAAC,CAAA;YACJ,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,OAAO,EAAE,CAAA;YACf,CAAC;QACH,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;YACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAoB,EAAE,CAAC,CAAC,CAAA;YACvF,IAAI,QAAQ,EAAE,SAAS,EAAE,CAAC;gBACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;oBACxD,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAoB,CAAC,CAAC,CAAA;oBAChE,MAAM,CAAC,OAAO,EAAE,CAAA;gBAClB,CAAC,CAAC,CAAA;YACJ,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,OAAO,EAAE,CAAA;YAClB,CAAC;QACH,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;QAC1C,IAAI,CAAC;YACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC1C,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;gBACvC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;gBACxC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;YAC/C,CAAC,CAAC,CAAA;YACF,MAAM,MAAM,GAAiB,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;YAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;YAC7B,OAAO,MAAM,CAAA;QACf,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;;AArFU,aAAa;IADzB,UAAU,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;GACzB,aAAa,CAsFzB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/rest-service",
|
|
3
|
-
"version": "11.0.
|
|
3
|
+
"version": "11.0.4",
|
|
4
4
|
"description": "REST API service implementation for FuryStack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -39,23 +39,22 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/furystack/furystack",
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@furystack/core": "^15.0.
|
|
43
|
-
"@furystack/inject": "^12.0.
|
|
44
|
-
"@furystack/repository": "^10.0.
|
|
45
|
-
"@furystack/rest": "^8.0.
|
|
46
|
-
"@furystack/security": "^6.0.
|
|
47
|
-
"@furystack/utils": "^8.1.
|
|
42
|
+
"@furystack/core": "^15.0.36",
|
|
43
|
+
"@furystack/inject": "^12.0.30",
|
|
44
|
+
"@furystack/repository": "^10.0.36",
|
|
45
|
+
"@furystack/rest": "^8.0.36",
|
|
46
|
+
"@furystack/security": "^6.0.36",
|
|
47
|
+
"@furystack/utils": "^8.1.10",
|
|
48
48
|
"ajv": "^8.17.1",
|
|
49
49
|
"ajv-formats": "^3.0.1",
|
|
50
|
-
"path-to-regexp": "^8.3.0"
|
|
51
|
-
"semaphore-async-await": "^1.5.1"
|
|
50
|
+
"path-to-regexp": "^8.3.0"
|
|
52
51
|
},
|
|
53
52
|
"devDependencies": {
|
|
54
|
-
"@furystack/rest-client-fetch": "^8.0.
|
|
55
|
-
"@types/node": "^25.
|
|
53
|
+
"@furystack/rest-client-fetch": "^8.0.36",
|
|
54
|
+
"@types/node": "^25.2.3",
|
|
56
55
|
"@types/ws": "^8.18.1",
|
|
57
56
|
"typescript": "^5.9.3",
|
|
58
|
-
"vitest": "^4.0.
|
|
57
|
+
"vitest": "^4.0.18",
|
|
59
58
|
"ws": "^8.19.0"
|
|
60
59
|
},
|
|
61
60
|
"engines": {
|
package/src/path-processor.ts
CHANGED
|
@@ -12,7 +12,9 @@ export class PathProcessor {
|
|
|
12
12
|
try {
|
|
13
13
|
return new URL(url)
|
|
14
14
|
} catch (error) {
|
|
15
|
-
throw new Error(`Invalid ${context}: ${url}${error instanceof Error ? ` (${error.message})` : ''}
|
|
15
|
+
throw new Error(`Invalid ${context}: ${url}${error instanceof Error ? ` (${error.message})` : ''}`, {
|
|
16
|
+
cause: error,
|
|
17
|
+
})
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
package/src/server-manager.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { EventHub } from '@furystack/utils'
|
|
|
3
3
|
import type { IncomingMessage, Server, ServerResponse } from 'http'
|
|
4
4
|
import { createServer } from 'http'
|
|
5
5
|
import type { Socket } from 'net'
|
|
6
|
-
import { Lock } from 'semaphore-async-await'
|
|
7
6
|
import type { Duplex } from 'stream'
|
|
8
7
|
|
|
9
8
|
export interface ServerOptions {
|
|
@@ -41,7 +40,7 @@ export class ServerManager
|
|
|
41
40
|
public static DEFAULT_HOST = 'localhost'
|
|
42
41
|
public servers = new Map<string, ServerRecord>()
|
|
43
42
|
private openedSockets = new Set<Socket>()
|
|
44
|
-
private readonly
|
|
43
|
+
private readonly pendingCreates = new Map<string, Promise<ServerRecord>>()
|
|
45
44
|
private getHostUrl = (options: ServerOptions) =>
|
|
46
45
|
`http://${options.hostName || ServerManager.DEFAULT_HOST}:${options.port}`
|
|
47
46
|
|
|
@@ -50,65 +49,74 @@ export class ServerManager
|
|
|
50
49
|
socket.once('close', () => this.openedSockets.delete(socket))
|
|
51
50
|
}
|
|
52
51
|
public async [Symbol.asyncDispose]() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.servers.clear()
|
|
67
|
-
this.listenLock.release()
|
|
68
|
-
super[Symbol.dispose]?.()
|
|
69
|
-
}
|
|
52
|
+
await Promise.allSettled([...this.pendingCreates.values()])
|
|
53
|
+
this.openedSockets.forEach((s) => s.destroy())
|
|
54
|
+
await Promise.allSettled(
|
|
55
|
+
[...this.servers.values()].map(
|
|
56
|
+
(s) =>
|
|
57
|
+
new Promise<void>((resolve, reject) => {
|
|
58
|
+
s.server.close((err) => (err ? reject(err) : resolve()))
|
|
59
|
+
s.server.off('connection', this.onConnection)
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
this.servers.clear()
|
|
64
|
+
super[Symbol.dispose]?.()
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
public async getOrCreate(options: ServerOptions): Promise<ServerRecord> {
|
|
73
68
|
const url = this.getHostUrl(options)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
})
|
|
101
|
-
server.on('connection', this.onConnection)
|
|
102
|
-
server.on('listening', () => resolve())
|
|
103
|
-
server.on('error', (err) => reject(err))
|
|
104
|
-
server.listen(options.port, options.hostName)
|
|
105
|
-
this.servers.set(url, { server, apis })
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
} finally {
|
|
109
|
-
this.listenLock.release()
|
|
69
|
+
|
|
70
|
+
const existing = this.servers.get(url)
|
|
71
|
+
if (existing) {
|
|
72
|
+
return existing
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const pending = this.pendingCreates.get(url)
|
|
76
|
+
if (pending) {
|
|
77
|
+
return pending
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const createPromise = this.createServer(url, options)
|
|
81
|
+
this.pendingCreates.set(url, createPromise)
|
|
82
|
+
return createPromise
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async createServer(url: string, options: ServerOptions): Promise<ServerRecord> {
|
|
86
|
+
const apis: ServerRecord['apis'] = []
|
|
87
|
+
const server = createServer((req, res) => {
|
|
88
|
+
const apiMatch = apis.find((api) => api.shouldExec({ req, res }))
|
|
89
|
+
if (apiMatch) {
|
|
90
|
+
apiMatch.onRequest({ req, res }).catch((error) => {
|
|
91
|
+
this.emit('onRequestFailed', [error, req, res])
|
|
92
|
+
})
|
|
93
|
+
} else {
|
|
94
|
+
res.destroy()
|
|
110
95
|
}
|
|
96
|
+
})
|
|
97
|
+
server.on('upgrade', (req, socket, head) => {
|
|
98
|
+
const apiMatch = apis.find((api) => api.shouldExec({ req, res: {} as ServerResponse }))
|
|
99
|
+
if (apiMatch?.onUpgrade) {
|
|
100
|
+
apiMatch.onUpgrade({ req, socket, head }).catch((error) => {
|
|
101
|
+
this.emit('onRequestFailed', [error, req, {} as ServerResponse])
|
|
102
|
+
socket.destroy()
|
|
103
|
+
})
|
|
104
|
+
} else {
|
|
105
|
+
socket.destroy()
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
server.on('connection', this.onConnection)
|
|
109
|
+
try {
|
|
110
|
+
await new Promise<void>((resolve, reject) => {
|
|
111
|
+
server.on('listening', () => resolve())
|
|
112
|
+
server.on('error', (err) => reject(err))
|
|
113
|
+
server.listen(options.port, options.hostName)
|
|
114
|
+
})
|
|
115
|
+
const record: ServerRecord = { server, apis }
|
|
116
|
+
this.servers.set(url, record)
|
|
117
|
+
return record
|
|
118
|
+
} finally {
|
|
119
|
+
this.pendingCreates.delete(url)
|
|
111
120
|
}
|
|
112
|
-
return this.servers.get(url) as ServerRecord
|
|
113
121
|
}
|
|
114
122
|
}
|