underpost 3.1.3 → 3.2.2

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 (92) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/ghpkg.ci.yml +4 -4
  3. package/.github/workflows/npmpkg.ci.yml +28 -11
  4. package/.github/workflows/publish.ci.yml +6 -0
  5. package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
  6. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  7. package/.github/workflows/release.cd.yml +13 -8
  8. package/CHANGELOG.md +396 -1
  9. package/CLI-HELP.md +53 -6
  10. package/Dockerfile +4 -2
  11. package/README.md +3 -2
  12. package/bin/build.js +18 -12
  13. package/bin/deploy.js +177 -124
  14. package/bin/file.js +3 -0
  15. package/conf.js +3 -2
  16. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -2
  17. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -2
  18. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  19. package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
  20. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  21. package/manifests/deployment/playwright/deployment.yaml +1 -1
  22. package/nodemon.json +1 -1
  23. package/package.json +22 -15
  24. package/scripts/rhel-grpc-setup.sh +56 -0
  25. package/src/api/file/file.ref.json +18 -0
  26. package/src/api/user/user.service.js +8 -7
  27. package/src/cli/cluster.js +7 -7
  28. package/src/cli/db.js +726 -825
  29. package/src/cli/deploy.js +151 -93
  30. package/src/cli/env.js +19 -0
  31. package/src/cli/fs.js +5 -2
  32. package/src/cli/index.js +45 -2
  33. package/src/cli/kubectl.js +211 -0
  34. package/src/cli/release.js +284 -0
  35. package/src/cli/repository.js +434 -75
  36. package/src/cli/run.js +189 -34
  37. package/src/cli/secrets.js +73 -0
  38. package/src/cli/test.js +3 -3
  39. package/src/client/Default.index.js +3 -4
  40. package/src/client/components/core/AppStore.js +69 -0
  41. package/src/client/components/core/CalendarCore.js +2 -2
  42. package/src/client/components/core/DropDown.js +137 -17
  43. package/src/client/components/core/Keyboard.js +2 -2
  44. package/src/client/components/core/LogIn.js +2 -2
  45. package/src/client/components/core/LogOut.js +2 -2
  46. package/src/client/components/core/Modal.js +0 -1
  47. package/src/client/components/core/Panel.js +0 -1
  48. package/src/client/components/core/PanelForm.js +19 -19
  49. package/src/client/components/core/SocketIo.js +82 -29
  50. package/src/client/components/core/SocketIoHandler.js +75 -0
  51. package/src/client/components/core/Stream.js +143 -95
  52. package/src/client/components/core/Webhook.js +40 -7
  53. package/src/client/components/default/AppStoreDefault.js +5 -0
  54. package/src/client/components/default/LogInDefault.js +3 -3
  55. package/src/client/components/default/LogOutDefault.js +2 -2
  56. package/src/client/components/default/MenuDefault.js +5 -5
  57. package/src/client/components/default/SocketIoDefault.js +3 -51
  58. package/src/client/services/core/core.service.js +20 -8
  59. package/src/client/services/user/user.management.js +2 -2
  60. package/src/index.js +24 -1
  61. package/src/runtime/express/Dockerfile +4 -0
  62. package/src/runtime/express/Express.js +18 -1
  63. package/src/runtime/lampp/Dockerfile +13 -2
  64. package/src/runtime/lampp/Lampp.js +27 -4
  65. package/src/runtime/wp/Dockerfile +68 -0
  66. package/src/runtime/wp/Wp.js +639 -0
  67. package/src/server/auth.js +24 -1
  68. package/src/server/backup.js +57 -23
  69. package/src/server/client-build-docs.js +9 -2
  70. package/src/server/client-build.js +31 -31
  71. package/src/server/client-formatted.js +109 -57
  72. package/src/server/cron.js +23 -18
  73. package/src/server/ipfs-client.js +24 -1
  74. package/src/server/peer.js +8 -0
  75. package/src/server/runtime.js +25 -1
  76. package/src/server/start.js +3 -2
  77. package/src/ws/IoInterface.js +1 -10
  78. package/src/ws/IoServer.js +14 -33
  79. package/src/ws/core/channels/core.ws.chat.js +65 -20
  80. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  81. package/src/ws/core/channels/core.ws.stream.js +90 -31
  82. package/src/ws/core/core.ws.connection.js +12 -33
  83. package/src/ws/core/core.ws.emit.js +10 -26
  84. package/src/ws/core/core.ws.server.js +25 -58
  85. package/src/ws/default/channels/default.ws.main.js +53 -12
  86. package/src/ws/default/default.ws.connection.js +26 -13
  87. package/src/ws/default/default.ws.server.js +30 -12
  88. package/src/client/components/default/ElementsDefault.js +0 -38
  89. package/src/ws/core/management/core.ws.chat.js +0 -8
  90. package/src/ws/core/management/core.ws.mailer.js +0 -16
  91. package/src/ws/core/management/core.ws.stream.js +0 -8
  92. package/src/ws/default/management/default.ws.main.js +0 -8
@@ -1,4 +1,6 @@
1
+ import { Badge } from './Badge.js';
1
2
  import { getId, newInstance } from './CommonJs.js';
3
+ import { darkTheme, ThemeEvents } from './Css.js';
2
4
  import { Input } from './Input.js';
3
5
  import { ToggleSwitch } from './ToggleSwitch.js';
4
6
  import { Translate } from './Translate.js';
@@ -15,17 +17,83 @@ const DropDown = {
15
17
  originData: options.data ? newInstance(options.data) : [],
16
18
  };
17
19
 
20
+ const _renderSelectedBadges = async () => {
21
+ if (options.type !== 'checkbox') return;
22
+ const container = s(`.dropdown-current-${id}`);
23
+ if (!container) return;
24
+ const selected = Object.entries(DropDown.Tokens[id].oncheckvalues);
25
+ if (selected.length === 0) {
26
+ htmls(`.dropdown-current-${id}`, '');
27
+ return;
28
+ }
29
+ let badgesHtml = '';
30
+ for (const [key, val] of selected) {
31
+ badgesHtml += html`<span class="inl" style="display:inline-flex;align-items:center;margin:2px;">
32
+ ${await Badge.Render({
33
+ text: html`<i class="fa-solid fa-tag" style="margin-right:3px;font-size:9px;"></i>${val.display}`,
34
+ style: {
35
+ background: darkTheme ? '#335' : '#cde',
36
+ color: darkTheme ? '#adf' : '#246',
37
+ 'border-radius': '4px',
38
+ 'font-size': '11px',
39
+ height: 'auto',
40
+ 'min-width': 'auto',
41
+ },
42
+ })}
43
+ <span
44
+ class="dd-badge-del-${id}"
45
+ data-key="${key}"
46
+ style="cursor:pointer;padding:0 4px;font-size:14px;color:${darkTheme ? '#f88' : '#a00'};line-height:1;"
47
+ >&times;</span
48
+ >
49
+ </span>`;
50
+ }
51
+ htmls(`.dropdown-current-${id}`, badgesHtml);
52
+ container.querySelectorAll(`.dd-badge-del-${id}`).forEach((btn) => {
53
+ btn.onclick = async (e) => {
54
+ e.stopPropagation();
55
+ const key = btn.dataset.key;
56
+ delete DropDown.Tokens[id].oncheckvalues[key];
57
+ const dataItem = options.data.find((d) => d.value.trim().replaceAll(' ', '-') === key);
58
+ if (dataItem) dataItem.checked = false;
59
+ if (ToggleSwitch.Tokens[`checkbox-role-${key}`]) {
60
+ const checkbox = s(`.checkbox-role-${key}-checkbox`);
61
+ if (checkbox && checkbox.checked) ToggleSwitch.Tokens[`checkbox-role-${key}`].click();
62
+ }
63
+ DropDown.Tokens[id].value = Object.values(DropDown.Tokens[id].oncheckvalues).map((v) => v.data);
64
+ s(`.${id}`).value = DropDown.Tokens[id].value;
65
+ await _renderSelectedBadges();
66
+ };
67
+ });
68
+ };
69
+ DropDown.Tokens[id]._renderSelectedBadges = _renderSelectedBadges;
70
+
18
71
  options.data.push({
19
72
  value: 'reset',
20
73
  display: html`<i class="fa-solid fa-broom"></i> ${Translate.Render('clear')}`,
21
74
  onClick: () => {
22
75
  console.log('DropDown onClick', this.value);
23
76
  if (options && options.resetOnClick) options.resetOnClick();
24
- if (options && options.type === 'checkbox')
25
- for (const opt of DropDown.Tokens[id].value) {
26
- s(`.dropdown-option-${id}-${opt}`).click();
77
+ if (options && options.type === 'checkbox') {
78
+ DropDown.Tokens[id].oncheckvalues = {};
79
+ DropDown.Tokens[id].value = [];
80
+ s(`.${id}`).value = [];
81
+ htmls(`.dropdown-current-${id}`, '');
82
+ if (options.serviceProvider) {
83
+ htmls(`.${id}-render-container`, '');
84
+ } else {
85
+ for (const optionData of options.data) {
86
+ if (optionData.value !== 'reset' && optionData.value !== 'close' && optionData.checked) {
87
+ optionData.checked = false;
88
+ const vd = optionData.value.trim().replaceAll(' ', '-');
89
+ if (ToggleSwitch.Tokens[`checkbox-role-${vd}`]) {
90
+ const checkbox = s(`.checkbox-role-${vd}-checkbox`);
91
+ if (checkbox && checkbox.checked) ToggleSwitch.Tokens[`checkbox-role-${vd}`].click();
92
+ }
93
+ }
94
+ }
27
95
  }
28
- else this.Tokens[id].value = undefined;
96
+ } else this.Tokens[id].value = undefined;
29
97
  },
30
98
  });
31
99
 
@@ -52,7 +120,7 @@ const DropDown = {
52
120
  const i = index;
53
121
  const valueDisplay = optionData.value.trim().replaceAll(' ', '-');
54
122
  setTimeout(() => {
55
- const onclick = (e) => {
123
+ const onclick = async (e) => {
56
124
  if (options && options.lastSelectClass && s(`.dropdown-option-${this.Tokens[id].lastSelectValue}`)) {
57
125
  s(`.dropdown-option-${this.Tokens[id].lastSelectValue}`).classList.remove(options.lastSelectClass);
58
126
  }
@@ -72,22 +140,18 @@ const DropDown = {
72
140
  if (optionData.value !== 'close') {
73
141
  if (optionData.value !== 'reset') {
74
142
  if (options.type === 'checkbox') {
75
- // const _instanValue = data
76
- // .filter((d) => d.checked)
77
- // .map((v, i, a) => `${v.display}${i < a.length - 1 ? ',' : ''}`)
78
- // .join('');
79
- const value = Object.keys(DropDown.Tokens[id].oncheckvalues);
80
- htmls(
81
- `.dropdown-current-${id}`,
82
- value.map((v) => DropDown.Tokens[id].originData.find((_v) => _v.value === v).display),
83
- );
143
+ _renderSelectedBadges();
84
144
  } else {
85
145
  htmls(`.dropdown-current-${id}`, optionData.display);
86
146
  }
87
147
  } else htmls(`.dropdown-current-${id}`, '');
88
148
 
89
149
  this.Tokens[id].value =
90
- options.type === 'checkbox' ? data.filter((d) => d.checked).map((d) => d.data) : optionData.data;
150
+ options.type === 'checkbox'
151
+ ? options.serviceProvider
152
+ ? Object.values(DropDown.Tokens[id].oncheckvalues).map((v) => v.data)
153
+ : data.filter((d) => d.checked).map((d) => d.data)
154
+ : optionData.data;
91
155
 
92
156
  console.warn('current value dropdown id:' + id, this.Tokens[id].value);
93
157
 
@@ -126,7 +190,11 @@ const DropDown = {
126
190
  },
127
191
  checked: () => {
128
192
  optionData.checked = true;
129
- DropDown.Tokens[id].oncheckvalues[valueDisplay] = {};
193
+ DropDown.Tokens[id].oncheckvalues[valueDisplay] = {
194
+ data: optionData.data,
195
+ display: optionData.display,
196
+ value: optionData.value,
197
+ };
130
198
  },
131
199
  },
132
200
  })}
@@ -153,6 +221,10 @@ const DropDown = {
153
221
  s(`.dropdown-current-${id}`).onclick = switchOptionsPanel;
154
222
  if (options && options.open) switchOptionsPanel();
155
223
 
224
+ if (options.type === 'checkbox') {
225
+ ThemeEvents[`dropdown-badge-${id}`] = () => _renderSelectedBadges();
226
+ }
227
+
156
228
  const dropDownSearchHandle = async () => {
157
229
  const _data = [];
158
230
  if (!s(`.search-box-${id}`)) return;
@@ -160,6 +232,12 @@ const DropDown = {
160
232
  let _value = s(`.search-box-${id}`).value.toLowerCase();
161
233
 
162
234
  for (const objData of options.data) {
235
+ if (
236
+ options.excludeSelected &&
237
+ options.type === 'checkbox' &&
238
+ DropDown.Tokens[id].oncheckvalues[objData.value.trim().replaceAll(' ', '-')]
239
+ )
240
+ continue;
163
241
  const objValue = objData.value.toLowerCase();
164
242
  if (
165
243
  objValue.match(_value) ||
@@ -186,7 +264,49 @@ const DropDown = {
186
264
  }
187
265
  };
188
266
 
189
- s(`.search-box-${id}`).oninput = dropDownSearchHandle;
267
+ if (options.serviceProvider) {
268
+ let serviceSearchTimeout = null;
269
+ s(`.search-box-${id}`).oninput = () => {
270
+ clearTimeout(serviceSearchTimeout);
271
+ const q = s(`.search-box-${id}`).value.trim();
272
+ if (!q) {
273
+ htmls(`.${id}-render-container`, '');
274
+ return;
275
+ }
276
+ serviceSearchTimeout = setTimeout(async () => {
277
+ try {
278
+ let results = await options.serviceProvider(q);
279
+ if (options.type === 'checkbox') {
280
+ if (options.excludeSelected) {
281
+ const selectedKeys = Object.keys(DropDown.Tokens[id].oncheckvalues);
282
+ results = results.filter((item) => !selectedKeys.includes(item.value.trim().replaceAll(' ', '-')));
283
+ }
284
+ results = results.map((item) => {
285
+ const vd = item.value.trim().replaceAll(' ', '-');
286
+ return { ...item, checked: !!DropDown.Tokens[id].oncheckvalues[vd] };
287
+ });
288
+ }
289
+ const controlItems = options.data.filter((d) => d.value === 'reset' || d.value === 'close');
290
+ const allData = [...results, ...controlItems];
291
+ if (allData.length > controlItems.length) {
292
+ const { render } = await _render(allData);
293
+ htmls(`.${id}-render-container`, render);
294
+ } else {
295
+ htmls(
296
+ `.${id}-render-container`,
297
+ html` <div class="inl" style="padding: 10px; color: red">
298
+ <i class="fas fa-exclamation-circle"></i> ${Translate.Render('no-result-found')}
299
+ </div>`,
300
+ );
301
+ }
302
+ } catch (e) {
303
+ console.error('DropDown serviceProvider error:', e);
304
+ }
305
+ }, 200);
306
+ };
307
+ } else {
308
+ s(`.search-box-${id}`).oninput = dropDownSearchHandle;
309
+ }
190
310
 
191
311
  // Not use onblur generate bug on input toggle
192
312
  // s(`.search-box-${id}`).onblur = dropDownSearchHandle;
@@ -3,8 +3,8 @@ import { cap, getId } from './CommonJs.js';
3
3
  const Keyboard = {
4
4
  ActiveKey: {},
5
5
  Event: {},
6
- Init: async function (options = { callBackTime: 50 }) {
7
- const { callBackTime } = options;
6
+ Init: async function () {
7
+ const callBackTime = 45;
8
8
  window.onkeydown = (e = new KeyboardEvent()) => {
9
9
  this.ActiveKey[e.key] = true;
10
10
  // e.composedPath()
@@ -10,7 +10,7 @@ import { NotificationManager } from './NotificationManager.js';
10
10
  import { Translate } from './Translate.js';
11
11
  import { Validator } from './Validator.js';
12
12
  import { htmls, s } from './VanillaJs.js';
13
- import { Webhook } from './Webhook.js';
13
+ import { WebhookProvider } from './Webhook.js';
14
14
 
15
15
  const logger = loggerFactory(import.meta);
16
16
 
@@ -31,7 +31,7 @@ const LogIn = {
31
31
 
32
32
  for (const eventKey of Object.keys(this.Event)) await this.Event[eventKey](options);
33
33
  if (!user || user.role === 'guest') return;
34
- await Webhook.register({ user });
34
+ await WebhookProvider.register({ user });
35
35
  if (s(`.session`))
36
36
  htmls(
37
37
  `.session`,
@@ -3,13 +3,13 @@ import { BtnIcon } from './BtnIcon.js';
3
3
  import { LogIn } from './LogIn.js';
4
4
  import { Translate } from './Translate.js';
5
5
  import { htmls, s } from './VanillaJs.js';
6
- import { Webhook } from './Webhook.js';
6
+ import { WebhookProvider } from './Webhook.js';
7
7
  import { NotificationManager } from './NotificationManager.js';
8
8
 
9
9
  const LogOut = {
10
10
  Event: {},
11
11
  Trigger: async function (options) {
12
- await Webhook.unregister();
12
+ await WebhookProvider.unregister();
13
13
  for (const eventKey of Object.keys(this.Event)) await this.Event[eventKey](options);
14
14
  if (s(`.session`))
15
15
  htmls(
@@ -1357,7 +1357,6 @@ const Modal = {
1357
1357
  class: 'hide',
1358
1358
  style: {
1359
1359
  // overflow: 'hidden',
1360
- background: 'none',
1361
1360
  resize: 'none',
1362
1361
  'min-width': `${minWidth}px`,
1363
1362
  'z-index': 5,
@@ -453,7 +453,6 @@ const Panel = {
453
453
 
454
454
  tagRender += await Badge.Render({
455
455
  text: tag,
456
- style: { color: tagColor },
457
456
  classList: 'inl panel-tag-clickable',
458
457
  style: {
459
458
  margin: '3px',
@@ -85,7 +85,7 @@ const PanelForm = {
85
85
  options = {
86
86
  idPanel: '',
87
87
  defaultUrlImage: '',
88
- Elements: {},
88
+ appStore: {},
89
89
  parentIdModal: undefined,
90
90
  route: 'home',
91
91
  htmlFormHeader: async () => '',
@@ -97,7 +97,7 @@ const PanelForm = {
97
97
  showCreatorProfile: false,
98
98
  },
99
99
  ) {
100
- const { idPanel, defaultUrlImage, Elements } = options;
100
+ const { idPanel, defaultUrlImage, appStore } = options;
101
101
 
102
102
  // Authenticated users don't need 'public' tag - they see all their own posts
103
103
  // Only include 'public' for unauthenticated users (handled by backend)
@@ -458,10 +458,10 @@ const PanelForm = {
458
458
  baseNewDoc.mdFileId = hasMdContent
459
459
  ? `<div class="markdown-content">${marked.parse(data.mdFileId)}</div>`
460
460
  : null;
461
- baseNewDoc.userId = Elements.Data.user?.main?.model?.user?._id;
461
+ baseNewDoc.userId = appStore.Data.user?.main?.model?.user?._id;
462
462
 
463
463
  // Ensure profileImageId is properly formatted as object with _id property
464
- const profileImageIdValue = Elements.Data.user?.main?.model?.user?.profileImageId;
464
+ const profileImageIdValue = appStore.Data.user?.main?.model?.user?.profileImageId;
465
465
  const formattedProfileImageId = profileImageIdValue
466
466
  ? typeof profileImageIdValue === 'string'
467
467
  ? { _id: profileImageIdValue }
@@ -469,9 +469,9 @@ const PanelForm = {
469
469
  : null;
470
470
 
471
471
  baseNewDoc.userInfo = {
472
- username: Elements.Data.user?.main?.model?.user?.username,
473
- email: Elements.Data.user?.main?.model?.user?.email,
474
- _id: Elements.Data.user?.main?.model?.user?._id,
472
+ username: appStore.Data.user?.main?.model?.user?.username,
473
+ email: appStore.Data.user?.main?.model?.user?.email,
474
+ _id: appStore.Data.user?.main?.model?.user?._id,
475
475
  profileImageId: formattedProfileImageId,
476
476
  };
477
477
  baseNewDoc.tools = true;
@@ -737,8 +737,8 @@ const PanelForm = {
737
737
  tools:
738
738
  documentObject.userId &&
739
739
  typeof documentObject.userId === 'object' &&
740
- Elements.Data.user?.main?.model?.user?._id &&
741
- documentObject.userId._id === Elements.Data.user.main.model.user._id,
740
+ appStore.Data.user?.main?.model?.user?._id &&
741
+ documentObject.userId._id === appStore.Data.user.main.model.user._id,
742
742
  _id: documentObject._id,
743
743
  totalCopyShareLinkCount: documentObject.totalCopyShareLinkCount || 0,
744
744
  isPublic: documentObject.isPublic || false,
@@ -772,8 +772,8 @@ const PanelForm = {
772
772
  tools:
773
773
  documentObject.userId &&
774
774
  typeof documentObject.userId === 'object' &&
775
- Elements.Data.user?.main?.model?.user?._id &&
776
- documentObject.userId._id === Elements.Data.user.main.model.user._id,
775
+ appStore.Data.user?.main?.model?.user?._id &&
776
+ documentObject.userId._id === appStore.Data.user.main.model.user._id,
777
777
  _id: documentObject._id,
778
778
  totalCopyShareLinkCount: documentObject.totalCopyShareLinkCount || 0,
779
779
  isPublic: documentObject.isPublic || false,
@@ -867,10 +867,10 @@ const PanelForm = {
867
867
  try {
868
868
  const cid = getQueryParams().cid ? getQueryParams().cid : '';
869
869
  const forceUpdate =
870
- Elements.Data.user.main.model &&
871
- Elements.Data.user.main.model.user &&
872
- Elements.Data.user.main.model.user._id &&
873
- lastUserId !== Elements.Data.user.main.model.user._id;
870
+ appStore.Data.user.main.model &&
871
+ appStore.Data.user.main.model.user &&
872
+ appStore.Data.user.main.model.user._id &&
873
+ lastUserId !== appStore.Data.user.main.model.user._id;
874
874
 
875
875
  logger.warn(
876
876
  {
@@ -878,8 +878,8 @@ const PanelForm = {
878
878
  cid,
879
879
  forceUpdate,
880
880
  },
881
- Elements.Data.user?.main?.model?.user
882
- ? JSON.stringify(Elements.Data.user.main.model.user, null, 4)
881
+ appStore.Data.user?.main?.model?.user
882
+ ? JSON.stringify(appStore.Data.user.main.model.user, null, 4)
883
883
  : 'No user data',
884
884
  );
885
885
 
@@ -889,8 +889,8 @@ const PanelForm = {
889
889
 
890
890
  if (loadingGetData || (normalizedLastCid === normalizedCid && !forceUpdate)) return;
891
891
  loadingGetData = true;
892
- lastUserId = Elements.Data.user?.main?.model?.user?._id
893
- ? newInstance(Elements.Data.user.main.model.user._id)
892
+ lastUserId = appStore.Data.user?.main?.model?.user?._id
893
+ ? newInstance(appStore.Data.user.main.model.user._id)
894
894
  : null;
895
895
  lastCid = cid;
896
896
 
@@ -1,25 +1,81 @@
1
+ /**
2
+ * Client-side WebSocket provider using Socket.IO.
3
+ * Manages a singleton socket connection, channel registration, and event dispatching.
4
+ *
5
+ * @module client/core/SocketIo
6
+ * @namespace SocketIoProvider
7
+ */
1
8
  import { io } from 'socket.io/client-dist/socket.io.esm.min.js';
2
9
  import { loggerFactory } from './Logger.js';
3
10
  import { getWsBasePath, getWsBaseUrl } from '../../services/core/core.service.js';
4
11
 
5
12
  const logger = loggerFactory(import.meta);
6
13
 
7
- const SocketIo = {
8
- Event: {
14
+ /**
15
+ * @class SocketIoProvider
16
+ * @classdesc Provides static methods for managing a singleton Socket.IO client connection,
17
+ * channel event dispatching, and message emission.
18
+ * @memberof SocketIoProvider
19
+ */
20
+ class SocketIoProvider {
21
+ /**
22
+ * Event callback registry, keyed by channel name → unique callback ID → handler function.
23
+ * Built-in channels: `connect`, `connect_error`, `disconnect`.
24
+ *
25
+ * @static
26
+ * @type {Object.<string, Object.<string, function>>}
27
+ */
28
+ static Event = {
9
29
  connect: {},
10
30
  connect_error: {},
11
31
  disconnect: {},
12
- },
13
- /** @type {import('socket.io').Socket} */
14
- socket: null,
15
- Emit: function (channel = '', payload = {}) {
32
+ };
33
+
34
+ /**
35
+ * The active Socket.IO client socket instance.
36
+ *
37
+ * @static
38
+ * @type {import('socket.io-client').Socket|null}
39
+ */
40
+ static socket = null;
41
+
42
+ /**
43
+ * The current WebSocket host URL.
44
+ *
45
+ * @static
46
+ * @type {string|undefined}
47
+ */
48
+ static host;
49
+
50
+ /**
51
+ * Emits a JSON-serialized payload to the server on the specified channel.
52
+ *
53
+ * @static
54
+ * @param {string} [channel=''] - The channel/event name to emit on.
55
+ * @param {Object} [payload={}] - The data to send.
56
+ * @returns {void}
57
+ */
58
+ static Emit(channel = '', payload = {}) {
16
59
  try {
17
60
  this.socket.emit(channel, JSON.stringify(payload));
18
61
  } catch (error) {
19
62
  logger.error(error);
20
63
  }
21
- },
22
- Init: async function (options) {
64
+ }
65
+
66
+ /**
67
+ * Initializes (or re-initializes) the Socket.IO connection.
68
+ * Disconnects any existing socket, creates a new connection, and registers
69
+ * built-in event listeners and custom channels.
70
+ *
71
+ * @static
72
+ * @async
73
+ * @param {Object} options - Connection options.
74
+ * @param {string} [options.host] - Override WebSocket host URL.
75
+ * @param {Object.<string, Object>} [options.channels] - Channel definitions to register listeners for.
76
+ * @returns {Promise<void>}
77
+ */
78
+ static async Init(options) {
23
79
  if (this.socket) this.socket.disconnect();
24
80
  const path = getWsBasePath();
25
81
  this.host = options.host ? options.host : getWsBaseUrl({ wsBasePath: '' });
@@ -29,24 +85,10 @@ const SocketIo = {
29
85
  });
30
86
  const connectOptions = {
31
87
  path: path === '/' ? undefined : path,
32
- // auth: {
33
- // token: '',
34
- // },
35
- // query: {
36
- // 'my-key': 'my-value',
37
- // },
38
- // forceNew: true,
39
- // reconnectionAttempts: 'Infinity',
40
- // timeout: 10000,
41
- // autoConnect: 5000,
42
- // Custom auth socket io credentials:
43
88
  withCredentials: true,
44
- extraHeaders: {
45
- // "my-custom-header": "abcd"
46
- },
89
+ extraHeaders: {},
47
90
  transports: ['websocket', 'polling', 'flashsocket'],
48
91
  };
49
- // logger.error(`connect options:`, JSON.stringify(connectOptions, null, 4));
50
92
  this.socket = io(this.host, connectOptions);
51
93
 
52
94
  this.socket.on('connect', () => {
@@ -65,17 +107,28 @@ const SocketIo = {
65
107
  });
66
108
 
67
109
  if (options && 'channels' in options) this.setChannels(options.channels);
68
- },
69
- setChannels: function (channels) {
110
+ }
111
+
112
+ /**
113
+ * Registers socket listeners for each channel key, routing incoming messages
114
+ * to all registered callbacks in `Event[channel]`.
115
+ *
116
+ * @static
117
+ * @param {Object.<string, Object>} channels - Channel definitions keyed by channel name.
118
+ * @returns {void}
119
+ */
120
+ static setChannels(channels) {
70
121
  Object.keys(channels).map((type) => {
71
122
  logger.info(`load chanel`, type);
72
123
  this.Event[type] = {};
73
124
  this.socket.on(type, (...args) => {
74
- // logger.info(`event: ${type} | ${JSON.stringify(args, null, 4)}`);
75
125
  Object.keys(this.Event[type]).map((keyEvent) => this.Event[type][keyEvent](args));
76
126
  });
77
127
  });
78
- },
79
- };
128
+ }
129
+ }
130
+
131
+ /** @type {SocketIoProvider} Backward compatibility alias. */
132
+ const SocketIo = SocketIoProvider;
80
133
 
81
- export { SocketIo };
134
+ export { SocketIoProvider, SocketIo };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shared factory for app-specific WebSocket event handlers.
3
+ * Eliminates duplication across per-app SocketIo*.js modules by providing
4
+ * common channel event handling (chat, email-confirmed, etc.).
5
+ *
6
+ * @module client/core/SocketIoHandler
7
+ * @namespace SocketIoHandlerProvider
8
+ */
9
+ import { Account } from './Account.js';
10
+ import { Chat } from './Chat.js';
11
+ import { s4 } from './CommonJs.js';
12
+ import { loggerFactory } from './Logger.js';
13
+ import { SocketIo } from './SocketIo.js';
14
+ import { s } from './VanillaJs.js';
15
+
16
+ const logger = loggerFactory(import.meta);
17
+
18
+ /**
19
+ * @class SocketIoHandlerProvider
20
+ * @classdesc Provides a static factory method to create app-specific SocketIo event handlers
21
+ * from an {@link AppStore} instance. Handles common channel events (chat, email-confirmed)
22
+ * and wires connect/disconnect lifecycle.
23
+ * @memberof SocketIoHandlerProvider
24
+ */
25
+ class SocketIoHandlerProvider {
26
+ /**
27
+ * Creates a standard SocketIo event initialization object for an app module.
28
+ *
29
+ * @static
30
+ * @param {import('./AppStore.js').AppStore} appStore - The app-specific AppStore instance.
31
+ * @returns {{ Init: function(): Promise<void> }} An object with an `Init` method for SocketIo event registration.
32
+ */
33
+ static create(appStore) {
34
+ return {
35
+ Init() {
36
+ return new Promise((resolve) => {
37
+ for (const type of Object.keys(appStore.Data)) {
38
+ SocketIo.Event[type][s4()] = async (args) => {
39
+ args = JSON.parse(args[0]);
40
+ switch (type) {
41
+ case 'chat':
42
+ {
43
+ const idModal = 'modal-chat';
44
+ if (s(`.${idModal}-chat-box`)) Chat.appendChatBox({ idModal, ...args });
45
+ }
46
+ break;
47
+
48
+ default:
49
+ break;
50
+ }
51
+ const { status } = args;
52
+
53
+ switch (status) {
54
+ case 'email-confirmed': {
55
+ const newUser = { ...appStore.Data.user.main.model.user, emailConfirmed: true };
56
+ Account.renderVerifyEmailStatus(newUser);
57
+ Account.triggerUpdateEvent({ user: newUser });
58
+ break;
59
+ }
60
+
61
+ default:
62
+ break;
63
+ }
64
+ };
65
+ }
66
+ SocketIo.Event.connect[s4()] = async (reason) => {};
67
+ SocketIo.Event.disconnect[s4()] = async (reason) => {};
68
+ return resolve();
69
+ });
70
+ },
71
+ };
72
+ }
73
+ }
74
+
75
+ export { SocketIoHandlerProvider };