underpost 3.1.2 → 3.2.0

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 (98) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/ghpkg.ci.yml +4 -4
  3. package/.github/workflows/npmpkg.ci.yml +38 -7
  4. package/.github/workflows/pwa-microservices-template-page.cd.yml +3 -4
  5. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  6. package/.github/workflows/release.cd.yml +4 -4
  7. package/CHANGELOG.md +365 -1
  8. package/CLI-HELP.md +55 -3
  9. package/README.md +7 -3
  10. package/bin/build.js +18 -12
  11. package/bin/deploy.js +205 -225
  12. package/bin/file.js +3 -0
  13. package/conf.js +4 -10
  14. package/jsdoc.json +1 -1
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +72 -50
  19. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  20. package/manifests/deployment/playwright/deployment.yaml +1 -1
  21. package/nodemon.json +1 -1
  22. package/package.json +21 -14
  23. package/scripts/ports-ls.sh +2 -0
  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 +76 -242
  29. package/src/cli/deploy.js +104 -65
  30. package/src/cli/env.js +1 -0
  31. package/src/cli/fs.js +2 -1
  32. package/src/cli/index.js +50 -1
  33. package/src/cli/kubectl.js +211 -0
  34. package/src/cli/release.js +284 -0
  35. package/src/cli/repository.js +328 -112
  36. package/src/cli/run.js +283 -69
  37. package/src/cli/test.js +3 -3
  38. package/src/client/Default.index.js +3 -4
  39. package/src/client/components/core/Alert.js +2 -2
  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/Docs.js +9 -2
  43. package/src/client/components/core/DropDown.js +129 -17
  44. package/src/client/components/core/Keyboard.js +2 -2
  45. package/src/client/components/core/LogIn.js +2 -2
  46. package/src/client/components/core/LogOut.js +2 -2
  47. package/src/client/components/core/Modal.js +0 -1
  48. package/src/client/components/core/Panel.js +0 -1
  49. package/src/client/components/core/PanelForm.js +19 -19
  50. package/src/client/components/core/RichText.js +1 -2
  51. package/src/client/components/core/SocketIo.js +82 -29
  52. package/src/client/components/core/SocketIoHandler.js +75 -0
  53. package/src/client/components/core/Stream.js +143 -95
  54. package/src/client/components/core/Webhook.js +40 -7
  55. package/src/client/components/default/AppStoreDefault.js +5 -0
  56. package/src/client/components/default/LogInDefault.js +3 -3
  57. package/src/client/components/default/LogOutDefault.js +2 -2
  58. package/src/client/components/default/MenuDefault.js +5 -5
  59. package/src/client/components/default/SocketIoDefault.js +3 -51
  60. package/src/client/services/core/core.service.js +20 -8
  61. package/src/client/services/user/user.management.js +2 -2
  62. package/src/client/ssr/body/404.js +15 -11
  63. package/src/client/ssr/body/500.js +15 -11
  64. package/src/client/ssr/body/SwaggerDarkMode.js +285 -0
  65. package/src/client/ssr/offline/NoNetworkConnection.js +11 -10
  66. package/src/client/ssr/pages/Test.js +11 -10
  67. package/src/index.js +24 -1
  68. package/src/runtime/express/Express.js +26 -9
  69. package/src/runtime/lampp/Dockerfile +9 -2
  70. package/src/runtime/lampp/Lampp.js +4 -3
  71. package/src/runtime/wp/Dockerfile +64 -0
  72. package/src/runtime/wp/Wp.js +497 -0
  73. package/src/server/auth.js +30 -6
  74. package/src/server/backup.js +19 -1
  75. package/src/server/client-build-docs.js +51 -110
  76. package/src/server/client-build.js +55 -64
  77. package/src/server/client-formatted.js +109 -57
  78. package/src/server/conf.js +19 -15
  79. package/src/server/ipfs-client.js +24 -1
  80. package/src/server/peer.js +8 -0
  81. package/src/server/runtime.js +25 -1
  82. package/src/server/start.js +21 -8
  83. package/src/ws/IoInterface.js +1 -10
  84. package/src/ws/IoServer.js +14 -33
  85. package/src/ws/core/channels/core.ws.chat.js +65 -20
  86. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  87. package/src/ws/core/channels/core.ws.stream.js +90 -31
  88. package/src/ws/core/core.ws.connection.js +12 -33
  89. package/src/ws/core/core.ws.emit.js +10 -26
  90. package/src/ws/core/core.ws.server.js +25 -58
  91. package/src/ws/default/channels/default.ws.main.js +53 -12
  92. package/src/ws/default/default.ws.connection.js +26 -13
  93. package/src/ws/default/default.ws.server.js +30 -12
  94. package/src/client/components/default/ElementsDefault.js +0 -38
  95. package/src/ws/core/management/core.ws.chat.js +0 -8
  96. package/src/ws/core/management/core.ws.mailer.js +0 -16
  97. package/src/ws/core/management/core.ws.stream.js +0 -8
  98. package/src/ws/default/management/default.ws.main.js +0 -8
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Core per-app state store for WebSocket channel data.
3
+ *
4
+ * @module client/core/AppStore
5
+ * @namespace AppStore
6
+ */
7
+
8
+ /**
9
+ * @class AppStore
10
+ * @classdesc Per-app singleton state store for WebSocket channel data and authenticated user state.
11
+ *
12
+ * Usage: `AppStoreX.Data.user.main.model.user` — the authenticated user object.
13
+ * `AppStoreX.Data` keys (`chat`, `mailer`, `stream`, etc.) — channel definitions for `SocketIo.Init`.
14
+ * @memberof AppStore
15
+ */
16
+ class AppStore {
17
+ /**
18
+ * Channel data map, keyed by channel name (e.g. `user`, `chat`, `mailer`).
19
+ * The `user` channel always contains `{ main: { model: { user: { _id: '' } } } }`.
20
+ *
21
+ * @type {Object.<string, Object>}
22
+ */
23
+ Data;
24
+
25
+ /** @private @type {function(): Object} */
26
+ #initialStateFactory;
27
+
28
+ /**
29
+ * Creates a new AppStore instance.
30
+ *
31
+ * @param {function(): Object} initialStateFactory - Factory function returning the initial data shape.
32
+ * Must return at least `{ user: { main: { model: { user: { _id: '' } } } } }`.
33
+ */
34
+ constructor(initialStateFactory) {
35
+ this.#initialStateFactory = initialStateFactory;
36
+ this.Data = initialStateFactory();
37
+ }
38
+
39
+ /**
40
+ * Resets `Data` to its initial state.
41
+ *
42
+ * @returns {void}
43
+ */
44
+ reset() {
45
+ this.Data = this.#initialStateFactory();
46
+ }
47
+
48
+ /**
49
+ * Creates an AppStore with the standard channel layout.
50
+ * Always includes `user`, `chat`, and `mailer` channels.
51
+ *
52
+ * @static
53
+ * @param {...string} extraChannels - Additional channel names (e.g. `'stream'`).
54
+ * @returns {AppStore}
55
+ */
56
+ static create(...extraChannels) {
57
+ return new AppStore(() => {
58
+ const state = {
59
+ user: { main: { model: { user: { _id: '' } } } },
60
+ chat: {},
61
+ mailer: {},
62
+ };
63
+ for (const ch of extraChannels) state[ch] = {};
64
+ return state;
65
+ });
66
+ }
67
+ }
68
+
69
+ export { AppStore };
@@ -25,7 +25,7 @@ const eventDateFactory = (event) =>
25
25
  const CalendarCore = {
26
26
  RenderStyle: async function () {},
27
27
  Data: {},
28
- Render: async function (options = { idModal: '', Elements: {}, hiddenDates: [] }) {
28
+ Render: async function (options = { idModal: '', appStore: {}, hiddenDates: [] }) {
29
29
  this.Data[options.idModal] = {
30
30
  data: [],
31
31
  originData: [],
@@ -49,7 +49,7 @@ const CalendarCore = {
49
49
  this.Data[options.idModal].filesData = [];
50
50
  this.Data[options.idModal].originData = newInstance(resultData);
51
51
  this.Data[options.idModal].data = resultData.map((o) => {
52
- if (o.creatorUserId && options.Elements.Data.user.main.model.user._id === o.creatorUserId) o.tools = true;
52
+ if (o.creatorUserId && options.appStore.Data.user.main.model.user._id === o.creatorUserId) o.tools = true;
53
53
  o.id = o._id;
54
54
 
55
55
  this.Data[options.idModal].filesData.push({});
@@ -24,11 +24,18 @@ const Docs = {
24
24
  html: async () => {
25
25
  if (docData.renderHtml) return await docData.renderHtml();
26
26
  return html`
27
+ <style>
28
+ .iframe-${ModalId} {
29
+ width: 100%;
30
+ border: none;
31
+ background: white;
32
+ display: block;
33
+ }
34
+ </style>
27
35
  <iframe
28
36
  class="in iframe-${ModalId}"
29
- style="width: 100%; border: none; background: white; display: block"
30
37
  src="${docData.url()}"
31
- sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-popups-to-escape-sandbox"
38
+ sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-popups-to-escape-sandbox allow-top-navigation"
32
39
  >
33
40
  </iframe>
34
41
  `;
@@ -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,75 @@ 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
+ if (options.serviceProvider) {
79
+ DropDown.Tokens[id].oncheckvalues = {};
80
+ DropDown.Tokens[id].value = [];
81
+ htmls(`.dropdown-current-${id}`, '');
82
+ htmls(`.${id}-render-container`, '');
83
+ } else {
84
+ for (const opt of DropDown.Tokens[id].value) {
85
+ s(`.dropdown-option-${id}-${opt}`).click();
86
+ }
27
87
  }
28
- else this.Tokens[id].value = undefined;
88
+ } else this.Tokens[id].value = undefined;
29
89
  },
30
90
  });
31
91
 
@@ -52,7 +112,7 @@ const DropDown = {
52
112
  const i = index;
53
113
  const valueDisplay = optionData.value.trim().replaceAll(' ', '-');
54
114
  setTimeout(() => {
55
- const onclick = (e) => {
115
+ const onclick = async (e) => {
56
116
  if (options && options.lastSelectClass && s(`.dropdown-option-${this.Tokens[id].lastSelectValue}`)) {
57
117
  s(`.dropdown-option-${this.Tokens[id].lastSelectValue}`).classList.remove(options.lastSelectClass);
58
118
  }
@@ -72,22 +132,18 @@ const DropDown = {
72
132
  if (optionData.value !== 'close') {
73
133
  if (optionData.value !== 'reset') {
74
134
  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
- );
135
+ _renderSelectedBadges();
84
136
  } else {
85
137
  htmls(`.dropdown-current-${id}`, optionData.display);
86
138
  }
87
139
  } else htmls(`.dropdown-current-${id}`, '');
88
140
 
89
141
  this.Tokens[id].value =
90
- options.type === 'checkbox' ? data.filter((d) => d.checked).map((d) => d.data) : optionData.data;
142
+ options.type === 'checkbox'
143
+ ? options.serviceProvider
144
+ ? Object.values(DropDown.Tokens[id].oncheckvalues).map((v) => v.data)
145
+ : data.filter((d) => d.checked).map((d) => d.data)
146
+ : optionData.data;
91
147
 
92
148
  console.warn('current value dropdown id:' + id, this.Tokens[id].value);
93
149
 
@@ -126,7 +182,11 @@ const DropDown = {
126
182
  },
127
183
  checked: () => {
128
184
  optionData.checked = true;
129
- DropDown.Tokens[id].oncheckvalues[valueDisplay] = {};
185
+ DropDown.Tokens[id].oncheckvalues[valueDisplay] = {
186
+ data: optionData.data,
187
+ display: optionData.display,
188
+ value: optionData.value,
189
+ };
130
190
  },
131
191
  },
132
192
  })}
@@ -153,6 +213,10 @@ const DropDown = {
153
213
  s(`.dropdown-current-${id}`).onclick = switchOptionsPanel;
154
214
  if (options && options.open) switchOptionsPanel();
155
215
 
216
+ if (options.type === 'checkbox') {
217
+ ThemeEvents[`dropdown-badge-${id}`] = () => _renderSelectedBadges();
218
+ }
219
+
156
220
  const dropDownSearchHandle = async () => {
157
221
  const _data = [];
158
222
  if (!s(`.search-box-${id}`)) return;
@@ -160,6 +224,12 @@ const DropDown = {
160
224
  let _value = s(`.search-box-${id}`).value.toLowerCase();
161
225
 
162
226
  for (const objData of options.data) {
227
+ if (
228
+ options.excludeSelected &&
229
+ options.type === 'checkbox' &&
230
+ DropDown.Tokens[id].oncheckvalues[objData.value.trim().replaceAll(' ', '-')]
231
+ )
232
+ continue;
163
233
  const objValue = objData.value.toLowerCase();
164
234
  if (
165
235
  objValue.match(_value) ||
@@ -186,7 +256,49 @@ const DropDown = {
186
256
  }
187
257
  };
188
258
 
189
- s(`.search-box-${id}`).oninput = dropDownSearchHandle;
259
+ if (options.serviceProvider) {
260
+ let serviceSearchTimeout = null;
261
+ s(`.search-box-${id}`).oninput = () => {
262
+ clearTimeout(serviceSearchTimeout);
263
+ const q = s(`.search-box-${id}`).value.trim();
264
+ if (!q) {
265
+ htmls(`.${id}-render-container`, '');
266
+ return;
267
+ }
268
+ serviceSearchTimeout = setTimeout(async () => {
269
+ try {
270
+ let results = await options.serviceProvider(q);
271
+ if (options.type === 'checkbox') {
272
+ if (options.excludeSelected) {
273
+ const selectedKeys = Object.keys(DropDown.Tokens[id].oncheckvalues);
274
+ results = results.filter((item) => !selectedKeys.includes(item.value.trim().replaceAll(' ', '-')));
275
+ }
276
+ results = results.map((item) => {
277
+ const vd = item.value.trim().replaceAll(' ', '-');
278
+ return { ...item, checked: !!DropDown.Tokens[id].oncheckvalues[vd] };
279
+ });
280
+ }
281
+ const controlItems = options.data.filter((d) => d.value === 'reset' || d.value === 'close');
282
+ const allData = [...results, ...controlItems];
283
+ if (allData.length > controlItems.length) {
284
+ const { render } = await _render(allData);
285
+ htmls(`.${id}-render-container`, render);
286
+ } else {
287
+ htmls(
288
+ `.${id}-render-container`,
289
+ html` <div class="inl" style="padding: 10px; color: red">
290
+ <i class="fas fa-exclamation-circle"></i> ${Translate.Render('no-result-found')}
291
+ </div>`,
292
+ );
293
+ }
294
+ } catch (e) {
295
+ console.error('DropDown serviceProvider error:', e);
296
+ }
297
+ }, 200);
298
+ };
299
+ } else {
300
+ s(`.search-box-${id}`).oninput = dropDownSearchHandle;
301
+ }
190
302
 
191
303
  // Not use onblur generate bug on input toggle
192
304
  // 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
 
@@ -30,10 +30,9 @@ const RichText = {
30
30
  return html` <style>
31
31
  .md-container {
32
32
  background: white;
33
- color: black;
34
33
  }
35
34
  .md-container button {
36
- color: black !important;
35
+ color: black;
37
36
  }
38
37
  </style>
39
38
  <div class="in md-container"><textarea class="${id}"></textarea></div>`;
@@ -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 };