@dooboostore/simple-boot-http-server 1.0.26 → 1.0.27

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.
Files changed (41) hide show
  1. package/README.md +100 -14
  2. package/dist/cjs/SimpleBootHttpServer.js +20 -16
  3. package/dist/cjs/SimpleBootHttpServer.js.map +2 -2
  4. package/dist/cjs/codes/Mimes.js +2 -0
  5. package/dist/cjs/codes/Mimes.js.map +2 -2
  6. package/dist/cjs/decorators/MethodMapping.js +2 -2
  7. package/dist/cjs/decorators/MethodMapping.js.map +2 -2
  8. package/dist/cjs/filters/ResourceFilter.js +0 -1
  9. package/dist/cjs/filters/ResourceFilter.js.map +2 -2
  10. package/dist/cjs/websocket/WebSocketManager.js +68 -40
  11. package/dist/cjs/websocket/WebSocketManager.js.map +2 -2
  12. package/dist/esm/SimpleBootHttpServer.js +20 -16
  13. package/dist/esm/SimpleBootHttpServer.js.map +2 -2
  14. package/dist/esm/codes/Mimes.js +2 -0
  15. package/dist/esm/codes/Mimes.js.map +2 -2
  16. package/dist/esm/decorators/MethodMapping.js +2 -2
  17. package/dist/esm/decorators/MethodMapping.js.map +2 -2
  18. package/dist/esm/filters/ResourceFilter.js +0 -1
  19. package/dist/esm/filters/ResourceFilter.js.map +2 -2
  20. package/dist/esm/websocket/WebSocketManager.js +68 -40
  21. package/dist/esm/websocket/WebSocketManager.js.map +2 -2
  22. package/dist/esm-bundle/dooboostore-simple-boot-http-server.esm.js +279 -137
  23. package/dist/esm-bundle/dooboostore-simple-boot-http-server.esm.js.map +4 -4
  24. package/dist/types/SimpleBootHttpServer.d.ts.map +1 -1
  25. package/dist/types/codes/Mimes.d.ts +2 -0
  26. package/dist/types/codes/Mimes.d.ts.map +1 -1
  27. package/dist/types/decorators/MethodMapping.d.ts +8 -0
  28. package/dist/types/decorators/MethodMapping.d.ts.map +1 -1
  29. package/dist/types/filters/ResourceFilter.d.ts.map +1 -1
  30. package/dist/types/websocket/WebSocketManager.d.ts +3 -1
  31. package/dist/types/websocket/WebSocketManager.d.ts.map +1 -1
  32. package/dist/umd-bundle/dooboostore-simple-boot-http-server.umd.js +279 -137
  33. package/dist/umd-bundle/dooboostore-simple-boot-http-server.umd.js.map +4 -4
  34. package/dist/umd-bundle/websocket-client.umd.js +209 -9
  35. package/dist/umd-bundle/websocket-client.umd.js.map +4 -4
  36. package/package.json +8 -8
  37. package/src/SimpleBootHttpServer.ts +21 -16
  38. package/src/codes/Mimes.ts +2 -0
  39. package/src/decorators/MethodMapping.ts +14 -7
  40. package/src/filters/ResourceFilter.ts +0 -1
  41. package/src/websocket/WebSocketManager.ts +69 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dooboostore/simple-boot-http-server",
3
- "version": "1.0.26",
3
+ "version": "1.0.27",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -67,12 +67,7 @@
67
67
  "url": "https://github.com/dooboostore-develop/packages"
68
68
  },
69
69
  "peerDependencies": {
70
- "reflect-metadata": "^0.2.2",
71
- "@dooboostore/core": "1.0.32",
72
- "@dooboostore/core-node": "1.0.13",
73
- "@dooboostore/core-web": "1.0.13",
74
- "@dooboostore/dom-render": "1.0.114",
75
- "@dooboostore/simple-boot": "1.0.57"
70
+ "reflect-metadata": "^0.2.2"
76
71
  },
77
72
  "devDependencies": {
78
73
  "@babel/core": "^7.24.0",
@@ -102,7 +97,12 @@
102
97
  "fast-json-patch": "^3.1.0",
103
98
  "mime-types": "^2.1.34",
104
99
  "node-gzip": "^1.1.2",
105
- "ws": "^8.18.3"
100
+ "ws": "^8.18.3",
101
+ "@dooboostore/core": "1.0.32",
102
+ "@dooboostore/core-node": "1.0.13",
103
+ "@dooboostore/core-web": "1.0.13",
104
+ "@dooboostore/simple-boot": "1.0.57",
105
+ "@dooboostore/dom-render": "1.0.114"
106
106
  },
107
107
  "scripts": {
108
108
  "clean": "rm -rf dist",
@@ -326,23 +326,28 @@ export class SimpleBootHttpServer extends SimpleApplication {
326
326
  여기에서 정해진 타입 리턴에 따른 조치
327
327
  ...
328
328
  */
329
- if (it.config?.resolver) {
330
- const execute = typeof it.config.resolver === 'function' ? this.simstanceManager.getOrNewSim({target: it.config.resolver}) : it.config.resolver;
331
- data = await execute?.resolve?.(data, rr);
332
- }
333
- const status = it.config?.res?.status ?? HttpStatus.Ok;
334
- const headers = it.config?.res?.header ?? {};
335
- if (it.config?.res?.contentType) {
336
- headers[HttpHeaders.ContentType] = it.config?.res?.contentType;
337
- }
338
- if ((it.config?.res?.contentType?.toLowerCase().indexOf(Mimes.ApplicationJson.toLowerCase()) ?? -1) > -1) {
339
- data = JSON.stringify(data);
340
- } else if (data && typeof data === 'object') {
341
- data = JSON.stringify(data);
329
+ const resConfig = it.config?.res;
330
+ if (resConfig?.manual === true) {
331
+ // 핸들러가 ServerResponse를 직접 주입받아 스스로 write/end를 제어한다 (예: SSE 스트리밍)
332
+ } else {
333
+ if (it.config?.resolver) {
334
+ const execute = typeof it.config.resolver === 'function' ? this.simstanceManager.getOrNewSim({target: it.config.resolver}) : it.config.resolver;
335
+ data = await execute?.resolve?.(data, rr);
336
+ }
337
+ const status = resConfig?.status ?? HttpStatus.Ok;
338
+ const headers = resConfig?.header ?? {};
339
+ if (resConfig?.contentType) {
340
+ headers[HttpHeaders.ContentType] = resConfig.contentType;
341
+ }
342
+ if ((resConfig?.contentType?.toLowerCase().indexOf(Mimes.ApplicationJson.toLowerCase()) ?? -1) > -1) {
343
+ data = JSON.stringify(data);
344
+ } else if (data && typeof data === 'object') {
345
+ data = JSON.stringify(data);
346
+ }
347
+ rr.resSetHeaders(headers)
348
+ rr.resSetStatusCode(status);
349
+ rr.resWrite(data);
342
350
  }
343
- rr.resSetHeaders(headers)
344
- rr.resSetStatusCode(status);
345
- rr.resWrite(data);
346
351
  }
347
352
  }
348
353
 
@@ -24,6 +24,8 @@ export enum Mimes {
24
24
  ImageXpixmap = 'image/x-xpixmap',
25
25
  ImageXxbm = 'image/x-xbm',
26
26
  MultipartFormData = 'multipart/form-data',
27
+ TextEventStream = 'text/event-stream', // SSE 실시간 텍스트 스트리밍
28
+ ApplicationXndjson = 'application/x-ndjson',
27
29
  ApplicationJsonPostSimpleBootSsrIntentScheme = 'application/json-post+simple-boot-ssr-intent-scheme',
28
30
  Intent = 'intent'
29
31
  }
@@ -24,11 +24,18 @@ export type MappingConfig = {
24
24
  accept?: (Mimes|string)[];
25
25
  };
26
26
 
27
- res?: {
28
- status?: number;
29
- header?: {[key: string]: string};
30
- contentType?: Mimes | string;
31
- }
27
+ res?:
28
+ | {
29
+ status?: number;
30
+ header?: {[key: string]: string};
31
+ contentType?: Mimes | string;
32
+ manual?: false;
33
+ }
34
+ /**
35
+ * manual: true — 리턴값을 이용한 자동 status/header/body 처리(및 resolver 호출)를 건너뛴다.
36
+ * SSE 등 핸들러가 ServerResponse를 직접 주입받아 writeHead/write/end를 스스로 제어하는 경우 사용.
37
+ */
38
+ | { manual: true };
32
39
  resolver?: Resolver|ConstructorType<Resolver>;
33
40
  }
34
41
  export type SaveMappingConfig = {propertyKey: string | symbol; config: MappingConfig}
@@ -156,7 +163,7 @@ export const getPATCH = (target: any, propertyKey: string | symbol): MappingConf
156
163
  return getUrlMapping(target, propertyKey);
157
164
  }
158
165
  export const getPATCHS = (target: any): SaveMappingConfig[] => {
159
- return getUrlMappings(target)?.filter(it => it.config?.method.toUpperCase() === HttpMethod.GET);
166
+ return getUrlMappings(target)?.filter(it => it.config?.method.toUpperCase() === HttpMethod.PATCH);
160
167
  }
161
168
  export function OPTIONS(config: Omit<MappingConfig, 'method'>): ReflectMethod;
162
169
  export function OPTIONS(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
@@ -194,7 +201,7 @@ export const getHEAD = (target: any, propertyKey: string | symbol): MappingConfi
194
201
  return getUrlMapping(target, propertyKey);
195
202
  }
196
203
  export const getHEADS = (target: any): SaveMappingConfig[] => {
197
- return getUrlMappings(target)?.filter(it => it.config?.method.toUpperCase() === HttpMethod.GET);
204
+ return getUrlMappings(target)?.filter(it => it.config?.method.toUpperCase() === HttpMethod.HEAD);
198
205
  }
199
206
  export function TRACE(config: Omit<MappingConfig, 'method'>): ReflectMethod;
200
207
  export function TRACE(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): void;
@@ -54,7 +54,6 @@ export class ResourceFilter implements Filter {
54
54
  }
55
55
 
56
56
  if (typeof it === 'string') {
57
- console.log('url!!!!!!!', url)
58
57
  return it === url;
59
58
  // return RegExp(it).test(url);
60
59
  }
@@ -90,7 +90,9 @@ type RegisteredClientSet<T = any> = RegisteredClient<T> & { webSocketSet: WebSoc
90
90
 
91
91
  @Sim
92
92
  export class WebSocketManager implements WebSocketEndPoint {
93
- private clients: Map<WebSocketSet, RegisteredClient[]> = new Map();
93
+ private connections: Map<WebSocketSet, Set<string>> = new Map();
94
+ private registrations: Map<string, RegisteredClientSet> = new Map();
95
+ private targets: Map<string, Set<string>> = new Map();
94
96
  private eventSubscriptions: Map<string, Map<string, EventResponseCallBack>> = new Map();
95
97
  private subject = new Subject<TopicRequest | TopicUnsubscribeRequest | TopicSubscribeRequest | TopicServerEventResponse>();
96
98
  constructor(
@@ -102,67 +104,82 @@ export class WebSocketManager implements WebSocketEndPoint {
102
104
  return this.subject.asObservable();
103
105
  }
104
106
  connect(wsSet: WebSocketSet) {
105
- this.clients.set(wsSet, []);
107
+ this.connections.set(wsSet, new Set<string>());
106
108
  console.log('WebSocket connected:', wsSet.request.url);
107
109
  }
108
110
  close(wsSet: WebSocketSet) {
109
111
  this.removeAllClientsByWebSocketSet(wsSet);
110
- this.clients.delete(wsSet);
112
+ this.connections.delete(wsSet);
111
113
  console.log('WebSocket disconnected:', wsSet.request.url);
112
114
  }
113
115
 
114
116
  setClientTopic(wsSet: WebSocketSet, uuid: string, target: string, subscribeBody?: any) {
115
- const clientTopics = this.clients.get(wsSet);
116
- if (clientTopics && !clientTopics.find(it => it.uuid === uuid)) {
117
- clientTopics.push({ uuid, target, subscribeBody });
118
- this.clients.set(wsSet, clientTopics);
119
- } else if (clientTopics) {
120
- throw { message: 'Already subscribed topic' };
117
+ const uuidSet = this.connections.get(wsSet);
118
+ if (!uuidSet) {
119
+ return;
120
+ }
121
+ if (this.registrations.has(uuid)) {
122
+ throw new Error('Already subscribed topic');
123
+ }
124
+ this.registrations.set(uuid, { uuid, target, subscribeBody, webSocketSet: wsSet });
125
+ uuidSet.add(uuid);
126
+ if (!this.targets.get(target)) {
127
+ this.targets.set(target, new Set<string>());
121
128
  }
129
+ this.targets.get(target)?.add(uuid);
122
130
  }
123
131
 
124
132
  findAllClientsByTarget(target: string): RegisteredClientSet[] {
133
+ const uuids = this.targets.get(target);
134
+ if (!uuids) {
135
+ return [];
136
+ }
125
137
  const results: RegisteredClientSet[] = [];
126
- for (const [wsSet, topics] of Array.from(this.clients.entries())) {
127
- const find = topics.find(it => it.target === target);
128
- if (find) {
129
- results.push({ ...find, webSocketSet: wsSet });
138
+ uuids.forEach(uuid => {
139
+ const reg = this.registrations.get(uuid);
140
+ if (reg) {
141
+ results.push({ ...reg });
130
142
  }
131
- }
143
+ });
132
144
  return results;
133
145
  }
134
146
 
135
147
  findClientsByTarget(target: string): RegisteredClientSet {
136
- for (const [wsSet, topics] of Array.from(this.clients.entries())) {
137
- const find = topics.find(it => it.target === target);
138
- if (find) {
139
- return { ...find, webSocketSet: wsSet };
148
+ const uuids = this.targets.get(target);
149
+ if (!uuids) {
150
+ return undefined as any;
151
+ }
152
+ for (const uuid of Array.from(uuids)) {
153
+ const reg = this.registrations.get(uuid);
154
+ if (reg) {
155
+ return { ...reg };
140
156
  }
141
157
  }
158
+ return undefined as any;
142
159
  }
143
160
 
144
161
  removeClientByUUID(uuid: string) {
145
- for (const [wsSet, topics] of Array.from(this.clients.entries())) {
146
- const findIndex = topics.findIndex(it => it.uuid === uuid);
147
- if (findIndex >= 0) {
148
- topics.splice(findIndex, 1);
149
- this.clients.set(wsSet, topics);
150
- // 이벤트 구독도 함께 정리
151
- this.eventSubscriptions.delete(uuid);
152
- return true;
162
+ const reg = this.registrations.get(uuid);
163
+ if (!reg) {
164
+ return false;
165
+ }
166
+ this.registrations.delete(uuid);
167
+ this.connections.get(reg.webSocketSet)?.delete(uuid);
168
+ const targetSet = this.targets.get(reg.target);
169
+ if (targetSet) {
170
+ targetSet.delete(uuid);
171
+ if (targetSet.size <= 0) {
172
+ this.targets.delete(reg.target);
153
173
  }
154
174
  }
155
- return false;
175
+ // 이벤트 구독도 함께 정리
176
+ this.eventSubscriptions.delete(uuid);
177
+ return true;
156
178
  }
157
179
 
158
180
  findClientByUUID(uuid: string): RegisteredClientSet | undefined {
159
- for (const [wsSet, topics] of Array.from(this.clients.entries())) {
160
- const find = topics.find(it => it.uuid === uuid);
161
- if (find) {
162
- return { ...find, webSocketSet: wsSet };
163
- }
164
- }
165
- return undefined;
181
+ const reg = this.registrations.get(uuid);
182
+ return reg ? { ...reg } : undefined;
166
183
  }
167
184
 
168
185
  sendDataByUUID({ uuid, body }: { uuid: string; body?: any }) {
@@ -342,7 +359,7 @@ export class WebSocketManager implements WebSocketEndPoint {
342
359
  }
343
360
 
344
361
  getOpenClients() {
345
- return Array.from(this.clients.keys()).filter(it => it.socket.readyState === WebSocket.OPEN);
362
+ return Array.from(this.connections.keys()).filter(it => it.socket.readyState === WebSocket.OPEN);
346
363
  }
347
364
 
348
365
  private isBinaryPayload(value: any) {
@@ -701,13 +718,24 @@ export class WebSocketManager implements WebSocketEndPoint {
701
718
  }
702
719
 
703
720
  private removeAllClientsByWebSocketSet(wsSet: WebSocketSet) {
704
- const topics = this.clients.get(wsSet);
705
- if (!topics) {
721
+ const uuidSet = this.connections.get(wsSet);
722
+ if (!uuidSet) {
706
723
  return;
707
724
  }
708
- for (const topic of topics) {
709
- this.eventSubscriptions.delete(topic.uuid);
710
- }
711
- this.clients.set(wsSet, []);
725
+ uuidSet.forEach(uuid => {
726
+ const reg = this.registrations.get(uuid);
727
+ if (reg) {
728
+ this.registrations.delete(uuid);
729
+ const targetSet = this.targets.get(reg.target);
730
+ if (targetSet) {
731
+ targetSet.delete(uuid);
732
+ if (targetSet.size <= 0) {
733
+ this.targets.delete(reg.target);
734
+ }
735
+ }
736
+ }
737
+ this.eventSubscriptions.delete(uuid);
738
+ });
739
+ uuidSet.clear();
712
740
  }
713
741
  }