create-fuzionx 0.1.33 → 0.1.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fuzionx",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Create a new FuzionX application — npx create-fuzionx my-app",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,8 +9,8 @@
9
9
  "test": "vitest run"
10
10
  },
11
11
  "dependencies": {
12
- "@fuzionx/framework": "^0.1.33",
13
- "@fuzionx/client": "^0.1.33",
12
+ "@fuzionx/framework": "^0.1.35",
13
+ "@fuzionx/client": "^0.1.35",
14
14
  "joi": "^18.1.1"
15
15
  },
16
16
  "devDependencies": {
@@ -4,7 +4,7 @@
4
4
  "description": "Vue.js 3 SPA + Tera SSR 하이브리드. WASM 암호화 통신.",
5
5
  "features": ["auth", "board", "i18n", "asp", "wasm"],
6
6
  "dependencies": {
7
- "@fuzionx/client": "^0.1.33"
7
+ "@fuzionx/client": "^0.1.35"
8
8
  },
9
9
  "devDependencies": {},
10
10
  "spaDevDependencies": {
@@ -1,7 +1,138 @@
1
- /**
2
- * SPA ChatHandler — SSR ChatHandler 참조.
3
- *
4
- * SSR과 동일한 WebSocket 채팅 핸들러를 사용.
5
- * 코드 중복 방지를 위해 SSR 핸들러를 re-export.
6
- */
7
- export { default } from '../../ssr/ws/ChatHandler.js';
1
+ import { WsHandler } from '@fuzionx/framework';
2
+
3
+ /** 채팅 WebSocket 핸들러 */
4
+ export default class ChatHandler extends WsHandler {
5
+ static namespace = '/chat';
6
+ static middleware = ['auth'];
7
+
8
+ /**
9
+ * disconnect 시 metadata가 이미 삭제되므로,
10
+ * setUser 시점에 이름을 캐시하여 disconnect에서 사용.
11
+ * 연결/해제는 항상 같은 워커에서 발생하므로 per-worker 캐시로 충분.
12
+ */
13
+ static _nameCache = new Map();
14
+
15
+ static events(e) {
16
+ e.on('message', ChatHandler.prototype.handleMessage);
17
+ e.on('broadcast', ChatHandler.prototype.handleBroadcast);
18
+ e.on('typing', ChatHandler.prototype.handleTyping);
19
+ e.on('userlist', ChatHandler.prototype.handleUserList);
20
+ e.on('setUser', ChatHandler.prototype.handleSetUser);
21
+ }
22
+
23
+ /* ── helpers ── */
24
+
25
+ /** 사용자 이름 조회 (캐시 → metadata → UUID 폴백) */
26
+ _getName(socket, sid) {
27
+ return ChatHandler._nameCache.get(sid)
28
+ || socket.getMetadataFor(sid, 'name')
29
+ || sid;
30
+ }
31
+
32
+ /** 단일 사용자 정보 빌드 */
33
+ _buildUserInfo(socket, sid) {
34
+ return {
35
+ sid,
36
+ name: this._getName(socket, sid),
37
+ email: socket.getMetadataFor(sid, 'email') || '',
38
+ };
39
+ }
40
+
41
+ /** 전체 접속자 목록 빌드 */
42
+ _buildUserList(socket) {
43
+ const sessions = socket.sessionIds;
44
+ return {
45
+ count: socket.onlineCount,
46
+ users: sessions.map(sid => this._buildUserInfo(socket, sid)),
47
+ };
48
+ }
49
+
50
+ /* ── lifecycle ── */
51
+
52
+ async onConnect(socket) {
53
+ const sid = socket.sessionId;
54
+ console.log(`[Chat] 연결: ${sid}`);
55
+
56
+ socket.send(JSON.stringify({
57
+ type: 'chat_ready',
58
+ data: { sessionId: sid, timestamp: Date.now() },
59
+ }));
60
+ }
61
+
62
+ async onDisconnect(socket, code, reason) {
63
+ const sid = socket.sessionId;
64
+ const name = ChatHandler._nameCache.get(sid);
65
+ ChatHandler._nameCache.delete(sid);
66
+
67
+ // setUser 미완료 세션은 user_joined도 안 보냈으므로 user_left 불필요
68
+ if (!name) return;
69
+
70
+ console.log(`[Chat] 해제: ${name} code=${code ?? 'N/A'}`);
71
+ socket.broadcastExcluding(JSON.stringify({
72
+ type: 'user_left',
73
+ data: { sid, name, timestamp: Date.now() },
74
+ }));
75
+ }
76
+
77
+ /* ── event handlers ── */
78
+
79
+ /** setUser — 사용자 정보 등록 + userlist 응답 */
80
+ async handleSetUser(socket, data) {
81
+ const sid = socket.sessionId;
82
+ const name = (data.name || '').trim() || sid;
83
+
84
+ // Bridge metadata + 로컬 캐시 동시 저장
85
+ socket.setMetadata('name', name);
86
+ if (data.email) socket.setMetadata('email', data.email);
87
+ if (data.id) socket.setMetadata('userId', String(data.id));
88
+ ChatHandler._nameCache.set(sid, name);
89
+ console.log(`[Chat] 사용자 등록: ${sid} → ${name}`);
90
+
91
+ // 다른 사용자에게 참여 알림
92
+ socket.broadcastExcluding(JSON.stringify({
93
+ type: 'user_joined',
94
+ data: this._buildUserInfo(socket, sid),
95
+ }));
96
+
97
+ // 본인에게 전체 목록 응답
98
+ return {
99
+ type: 'userlist',
100
+ data: this._buildUserList(socket),
101
+ };
102
+ }
103
+
104
+ /** chat_msg — 본인 제외 전체 전송 */
105
+ async handleMessage(socket, data) {
106
+ const name = this._getName(socket, socket.sessionId);
107
+ socket.broadcastExcluding(JSON.stringify({
108
+ type: 'chat_msg',
109
+ data: { user: name, text: data.text || data, timestamp: Date.now() },
110
+ }));
111
+ }
112
+
113
+ /** broadcast — 공지 */
114
+ async handleBroadcast(socket, data) {
115
+ const name = this._getName(socket, socket.sessionId);
116
+ socket.broadcastExcluding(JSON.stringify({
117
+ type: 'broadcast',
118
+ data: { user: name, text: data.text || data, timestamp: Date.now() },
119
+ }));
120
+ }
121
+
122
+ /** typing — 타이핑 표시 */
123
+ async handleTyping(socket) {
124
+ const name = this._getName(socket, socket.sessionId);
125
+ socket.broadcastExcluding(JSON.stringify({
126
+ type: 'typing',
127
+ data: { user: name },
128
+ }));
129
+ }
130
+
131
+ /** userlist — 목록 요청 */
132
+ async handleUserList(socket) {
133
+ return {
134
+ type: 'userlist',
135
+ data: this._buildUserList(socket),
136
+ };
137
+ }
138
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * FuzionX SSR Client — WASM ASP HTTP + WebSocket 통합 클라이언트
3
+ *
4
+ * create-fuzionx tester 레이아웃 패턴 기반.
5
+ * WASM 로드 → FuzionXClient, FuzionXSocket, aspFetch 글로벌 노출.
6
+ *
7
+ * 사용법 (인증 유저):
8
+ * FxClient.init(clientSecret, encSecret, headerSignal)
9
+ *
10
+ * 사용법 (공개 페이지):
11
+ * FxClient.initPublic(masterSecret, headerSignal)
12
+ */
13
+ (function () {
14
+ 'use strict';
15
+
16
+ /**
17
+ * 인증 유저용 — 암호화된 masterSecret 복호화 후 ASP 클라이언트 생성
18
+ */
19
+ function init(clientSecret, encSecret, headerSignal) {
20
+ if (!clientSecret || !encSecret) return;
21
+
22
+ window.aspEnabled = true;
23
+ window._aspReady = (async () => {
24
+ try {
25
+ const _v = Date.now();
26
+ const mod = await import(`/wasm/fuzionx_client_wasm.js?v=${_v}`);
27
+ await mod.default({ module_or_path: `/wasm/fuzionx_client_wasm_bg.wasm?v=${_v}` });
28
+ const { FuzionXClient, FuzionXSocket } = mod;
29
+ window.FuzionXSocket = FuzionXSocket;
30
+
31
+ // 임시 클라이언트로 masterSecret 복호화
32
+ const tmp = new FuzionXClient('_init_');
33
+ const masterSecret = tmp.decrypt_custom(clientSecret, encSecret);
34
+ window._aspMasterSecret = masterSecret;
35
+
36
+ // ASP 활성 클라이언트 생성
37
+ window._aspClient = FuzionXClient.new_with_options(
38
+ masterSecret,
39
+ headerSignal || 'Ruxy-Enc-Mode',
40
+ );
41
+ console.log('[FxClient] WASM ASP client ready');
42
+ } catch (e) {
43
+ console.warn('[FxClient] WASM init failed:', e);
44
+ }
45
+ })();
46
+ }
47
+
48
+ /**
49
+ * 공개 페이지용 — masterSecret 직접 사용
50
+ */
51
+ function initPublic(masterSecret, headerSignal) {
52
+ window.aspEnabled = !!masterSecret;
53
+ if (masterSecret) window._aspMasterSecret = masterSecret;
54
+
55
+ window._aspReady = (async () => {
56
+ try {
57
+ const _v = Date.now();
58
+ const mod = await import(`/wasm/fuzionx_client_wasm.js?v=${_v}`);
59
+ await mod.default({ module_or_path: `/wasm/fuzionx_client_wasm_bg.wasm?v=${_v}` });
60
+ const { FuzionXClient, FuzionXSocket } = mod;
61
+ window.FuzionXSocket = FuzionXSocket;
62
+
63
+ if (masterSecret) {
64
+ window._aspClient = FuzionXClient.new_with_options(
65
+ masterSecret,
66
+ headerSignal || 'Ruxy-Enc-Mode',
67
+ );
68
+ }
69
+ console.log('[FxClient] WASM client ready (public)');
70
+ } catch (e) {
71
+ console.warn('[FxClient] WASM init failed:', e);
72
+ }
73
+ })();
74
+ }
75
+
76
+ /**
77
+ * ASP 암호화 fetch — create-fuzionx tester aspFetch 동일 패턴
78
+ */
79
+ window.aspFetch = async function (url, opts = {}) {
80
+ if (window._aspReady) await window._aspReady;
81
+ const method = (opts.method || 'GET').toUpperCase();
82
+ if (!url.startsWith('/api') || !window._aspClient) return fetch(url, opts);
83
+
84
+ if (opts.body instanceof FormData) {
85
+ return window._aspClient.upload(url, opts.body);
86
+ }
87
+
88
+ let bodyObj = undefined;
89
+ if (opts.body) {
90
+ bodyObj = typeof opts.body === 'string' ? JSON.parse(opts.body) : opts.body;
91
+ }
92
+
93
+ switch (method) {
94
+ case 'GET': return window._aspClient.get(url);
95
+ case 'POST': return window._aspClient.post(url, bodyObj);
96
+ case 'PUT': return window._aspClient.put(url, bodyObj);
97
+ case 'PATCH': return window._aspClient.patch(url, bodyObj);
98
+ case 'DELETE': return window._aspClient.delete(url);
99
+ default: return window._aspClient.get(url);
100
+ }
101
+ };
102
+
103
+ // 글로벌 노출
104
+ window.FxClient = { init, initPublic };
105
+ window.aspEnabled = false;
106
+ window._aspReady = Promise.resolve();
107
+ })();