@furystack/rest-service 11.0.3 → 11.0.5

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [11.0.5] - 2026-02-19
4
+
5
+ ### ⬆️ Dependencies
6
+
7
+ - Updated `@furystack/core` and `@furystack/repository`
8
+
9
+ ## [11.0.4] - 2026-02-11
10
+
11
+ ### 🐛 Bug Fixes
12
+
13
+ - Preserve original error cause in `PathProcessor.validateUrl()` using `{ cause: error }` for better error traceability
14
+
15
+ ### ♻️ Refactoring
16
+
17
+ - 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.
18
+ - Simplified `[Symbol.asyncDispose]()` — disposal now awaits pending server creations directly instead of waiting on a semaphore lock with a timeout.
19
+
20
+ ### ⬆️ Dependencies
21
+
22
+ - Bump `vitest` from `^4.0.17` to `^4.0.18`
23
+ - Bump `@types/node` from `^25.0.10` to `^25.2.3`
24
+ - Removed `semaphore-async-await` dependency
25
+ - Updated internal dependencies
26
+
3
27
  ## [11.0.3] - 2026-02-09
4
28
 
5
29
  ### ⬆️ 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;IAQrD;;;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"}
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"}
@@ -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;QACrG,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"}
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"}
@@ -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 listenLock;
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;AAInE,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,UAAU,CAAa;IACxC,OAAO,CAAC,UAAU,CAC0D;IAE5E,OAAO,CAAC,YAAY,CAGnB;IACY,CAAC,MAAM,CAAC,YAAY,CAAC;IAoBrB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;CA0CxE"}
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"}
@@ -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
- listenLock = new Lock();
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
- try {
25
- await this.listenLock.waitFor(5000);
26
- }
27
- finally {
28
- this.openedSockets.forEach((s) => s.destroy());
29
- await Promise.allSettled([...this.servers.values()].map((s) => new Promise((resolve, reject) => {
30
- s.server.close((err) => (err ? reject(err) : resolve()));
31
- s.server.off('connection', this.onConnection);
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
- if (!this.servers.has(url)) {
41
- await this.listenLock.acquire();
42
- try {
43
- if (!this.servers.has(url)) {
44
- await new Promise((resolve, reject) => {
45
- const apis = [];
46
- const server = createServer((req, res) => {
47
- const apiMatch = apis.find((api) => api.shouldExec({ req, res }));
48
- if (apiMatch) {
49
- apiMatch.onRequest({ req, res }).catch((error) => {
50
- this.emit('onRequestFailed', [error, req, res]);
51
- });
52
- }
53
- else {
54
- res.destroy();
55
- }
56
- });
57
- server.on('upgrade', (req, socket, head) => {
58
- const apiMatch = apis.find((api) => api.shouldExec({ req, res: {} }));
59
- if (apiMatch?.onUpgrade) {
60
- apiMatch.onUpgrade({ req, socket, head }).catch((error) => {
61
- this.emit('onRequestFailed', [error, req, {}]);
62
- socket.destroy();
63
- });
64
- }
65
- else {
66
- socket.destroy();
67
- }
68
- });
69
- server.on('connection', this.onConnection);
70
- server.on('listening', () => resolve());
71
- server.on('error', (err) => reject(err));
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
- finally {
78
- this.listenLock.release();
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;AAEnC,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AA+BrC,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,UAAU,GAAG,IAAI,IAAI,EAAE,CAAA;IAChC,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,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACrC,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;YAC9C,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;gBACpC,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;gBACxD,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;YAC/C,CAAC,CAAC,CACL,CACF,CAAA;YACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;YACpB,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;YACzB,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,EAAE,CAAA;QAC3B,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,WAAW,CAAC,OAAsB;QAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;QACpC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;YAC/B,IAAI,CAAC;gBACH,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC3B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;wBAC1C,MAAM,IAAI,GAAyB,EAAE,CAAA;wBACrC,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;4BACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;4BACjE,IAAI,QAAQ,EAAE,CAAC;gCACb,QAAQ,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;oCAC/C,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;gCACjD,CAAC,CAAC,CAAA;4BACJ,CAAC;iCAAM,CAAC;gCACN,GAAG,CAAC,OAAO,EAAE,CAAA;4BACf,CAAC;wBACH,CAAC,CAAC,CAAA;wBACF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;4BACzC,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;4BACvF,IAAI,QAAQ,EAAE,SAAS,EAAE,CAAC;gCACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;oCACxD,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAoB,CAAC,CAAC,CAAA;oCAChE,MAAM,CAAC,OAAO,EAAE,CAAA;gCAClB,CAAC,CAAC,CAAA;4BACJ,CAAC;iCAAM,CAAC;gCACN,MAAM,CAAC,OAAO,EAAE,CAAA;4BAClB,CAAC;wBACH,CAAC,CAAC,CAAA;wBACF,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;wBAC1C,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;wBACvC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;wBACxC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;wBAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;oBACzC,CAAC,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;YAC3B,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAiB,CAAA;IAC9C,CAAC;;AA5EU,aAAa;IADzB,UAAU,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;GACzB,aAAa,CA6EzB"}
1
+ {"version":3,"file":"server-manager.js","sourceRoot":"","sources":["../src/server-manager.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furystack/rest-service",
3
- "version": "11.0.3",
3
+ "version": "11.0.5",
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.35",
43
- "@furystack/inject": "^12.0.29",
44
- "@furystack/repository": "^10.0.35",
45
- "@furystack/rest": "^8.0.35",
46
- "@furystack/security": "^6.0.35",
47
- "@furystack/utils": "^8.1.9",
48
- "ajv": "^8.17.1",
42
+ "@furystack/core": "^15.1.0",
43
+ "@furystack/inject": "^12.0.30",
44
+ "@furystack/repository": "^10.0.37",
45
+ "@furystack/rest": "^8.0.37",
46
+ "@furystack/security": "^6.0.37",
47
+ "@furystack/utils": "^8.1.10",
48
+ "ajv": "^8.18.0",
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.35",
55
- "@types/node": "^25.0.10",
53
+ "@furystack/rest-client-fetch": "^8.0.37",
54
+ "@types/node": "^25.3.0",
56
55
  "@types/ws": "^8.18.1",
57
56
  "typescript": "^5.9.3",
58
- "vitest": "^4.0.17",
57
+ "vitest": "^4.0.18",
59
58
  "ws": "^8.19.0"
60
59
  },
61
60
  "engines": {
@@ -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
 
@@ -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 listenLock = new Lock()
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
- try {
54
- await this.listenLock.waitFor(5000)
55
- } finally {
56
- this.openedSockets.forEach((s) => s.destroy())
57
- await Promise.allSettled(
58
- [...this.servers.values()].map(
59
- (s) =>
60
- new Promise<void>((resolve, reject) => {
61
- s.server.close((err) => (err ? reject(err) : resolve()))
62
- s.server.off('connection', this.onConnection)
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
- if (!this.servers.has(url)) {
75
- await this.listenLock.acquire()
76
- try {
77
- if (!this.servers.has(url)) {
78
- await new Promise<void>((resolve, reject) => {
79
- const apis: ServerRecord['apis'] = []
80
- const server = createServer((req, res) => {
81
- const apiMatch = apis.find((api) => api.shouldExec({ req, res }))
82
- if (apiMatch) {
83
- apiMatch.onRequest({ req, res }).catch((error) => {
84
- this.emit('onRequestFailed', [error, req, res])
85
- })
86
- } else {
87
- res.destroy()
88
- }
89
- })
90
- server.on('upgrade', (req, socket, head) => {
91
- const apiMatch = apis.find((api) => api.shouldExec({ req, res: {} as ServerResponse }))
92
- if (apiMatch?.onUpgrade) {
93
- apiMatch.onUpgrade({ req, socket, head }).catch((error) => {
94
- this.emit('onRequestFailed', [error, req, {} as ServerResponse])
95
- socket.destroy()
96
- })
97
- } else {
98
- socket.destroy()
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
  }