@raclettejs/core 0.1.19 → 0.1.21

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 (134) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CONTRIBUTING.md +23 -0
  3. package/bin/cli.js +1 -1
  4. package/dev/README.md +4 -0
  5. package/dist/cli.js +135 -124
  6. package/dist/cli.js.map +4 -4
  7. package/dist/index.js +9 -8
  8. package/dist/index.js.map +4 -4
  9. package/package.json +12 -6
  10. package/raclette.default.config.yaml +60 -0
  11. package/services/backend/delay-api-debugger.sh +0 -0
  12. package/services/backend/package.json +1 -0
  13. package/services/backend/src/app.ts +4 -0
  14. package/services/backend/src/core/config/configService.ts +10 -1
  15. package/services/backend/src/core/events/eventEmitter.ts +32 -7
  16. package/services/backend/src/core/http/request-params.plugin.ts +2 -2
  17. package/services/backend/src/core/pluginSystem/pluginFastifyInstance.ts +4 -1
  18. package/services/backend/src/core/pluginSystem/pluginTypes.ts +3 -1
  19. package/services/backend/src/core/sockets/createSystemEventHandlers.ts +7 -0
  20. package/services/backend/src/core/sockets/index.ts +61 -13
  21. package/services/backend/src/core/sockets/socketManager.ts +452 -105
  22. package/services/backend/src/core/sockets/socketTypes.ts +13 -1
  23. package/services/backend/src/core/sockets/workSessionJoinPayload.ts +106 -0
  24. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.get.ts +3 -1
  25. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.getAll.ts +3 -1
  26. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.hardDelete.ts +16 -19
  27. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.patch.ts +3 -1
  28. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.post.ts +3 -1
  29. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.remove.ts +3 -1
  30. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.service.ts +2 -3
  31. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.get.ts +3 -1
  32. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.getAll.ts +3 -1
  33. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.hardDelete.ts +16 -19
  34. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.patch.ts +3 -1
  35. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.bulk.ts +3 -1
  36. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.ts +3 -1
  37. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.remove.ts +3 -1
  38. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.restore.ts +12 -15
  39. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.service.ts +2 -3
  40. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.get.ts +3 -1
  41. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.getAll.ts +4 -2
  42. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.hardDelete.ts +12 -17
  43. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.patch.ts +3 -1
  44. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.bulk.ts +3 -1
  45. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.ts +3 -1
  46. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.remove.ts +3 -1
  47. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.restore.ts +12 -15
  48. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.service.ts +1 -0
  49. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.get.ts +3 -1
  50. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.getAll.ts +4 -2
  51. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.hardDelete.ts +3 -1
  52. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.patch.ts +3 -1
  53. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.post.ts +4 -2
  54. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.remove.ts +3 -1
  55. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.get.ts +1 -0
  56. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.getAll.ts +1 -0
  57. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.hardDelete.ts +10 -15
  58. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.patch.ts +3 -1
  59. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.bulk.ts +3 -1
  60. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.ts +3 -1
  61. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.remove.ts +3 -1
  62. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.restore.ts +12 -15
  63. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/tag.service.ts +1 -2
  64. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.get.ts +1 -0
  65. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.getAll.ts +1 -0
  66. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.hardDelete.ts +10 -15
  67. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patch.ts +4 -6
  68. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchLastProject.ts +4 -2
  69. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.post.ts +3 -1
  70. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.remove.ts +3 -4
  71. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.restore.ts +12 -15
  72. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/events/index.ts +45 -0
  73. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/helpers/crud.ts +12 -0
  74. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/helpers/payload.ts +21 -0
  75. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/index.ts +55 -0
  76. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.ts +19 -0
  77. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.get.ts +53 -0
  78. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.getAll.ts +39 -0
  79. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.hardDelete.ts +53 -0
  80. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.patch.ts +58 -0
  81. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.post.ts +53 -0
  82. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.remove.ts +52 -0
  83. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.model.ts +65 -0
  84. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.schema.ts +125 -0
  85. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.service.ts +351 -0
  86. package/services/backend/src/corePlugins/raclette__core/backend/index.ts +2 -0
  87. package/services/backend/src/corePlugins/raclette__core/frontend/generated-config.ts +159 -24
  88. package/services/backend/src/modules/authentication/index.ts +255 -4
  89. package/services/backend/src/modules/hooks/rate-limiter.ts +21 -0
  90. package/services/backend/src/types/custom-fastify.d.ts +17 -0
  91. package/services/backend/src/utils/request.utils.ts +4 -1
  92. package/services/backend/types/index.d.ts +15 -1
  93. package/services/backend/yarn.lock +13 -4
  94. package/services/frontend/index.html +1 -1
  95. package/services/frontend/provision/entrypoint.sh +0 -0
  96. package/services/frontend/src/core/lib/configService.ts +1 -1
  97. package/services/frontend/src/core/lib/dataHelper.ts +1 -1
  98. package/services/frontend/src/core/lib/eggs/snow.ts +49 -20
  99. package/services/frontend/src/core/lib/helpers/index.ts +1 -0
  100. package/services/frontend/src/core/lib/helpers/removeEmpty.ts +69 -0
  101. package/services/frontend/src/core/lib/httpClient.ts +19 -0
  102. package/services/frontend/src/core/main.ts +20 -10
  103. package/services/frontend/src/core/setup/socket.ts +4 -3
  104. package/services/frontend/src/core/setup/socketClient.ts +31 -3
  105. package/services/frontend/src/core/setup/socketEvents.ts +92 -8
  106. package/services/frontend/src/core/store/index.ts +5 -1
  107. package/services/frontend/src/core/store/reducers/compositions/index.ts +2 -2
  108. package/services/frontend/src/core/store/reducers/interactionLinks/index.ts +1 -1
  109. package/services/frontend/src/core/store/reducers/widgets/reducers.ts +1 -2
  110. package/services/frontend/src/core/store/types/index.ts +0 -1
  111. package/services/frontend/src/core/types/PluginRegistrar.ts +8 -6
  112. package/services/frontend/src/core/types/Store.ts +1 -0
  113. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +68 -8
  114. package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +17 -5
  115. package/services/frontend/src/orchestrator/components/welcomeScreen/AdminWelcomeScreen.vue +1 -1
  116. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  117. package/services/frontend/src/orchestrator/composables/usePluginApi.ts +0 -3
  118. package/services/frontend/src/orchestrator/composables/useRouteState.ts +23 -4
  119. package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +166 -0
  120. package/services/frontend/src/orchestrator/helpers/index.ts +1 -0
  121. package/services/frontend/src/orchestrator/i18n/de-DE.json +1 -1
  122. package/services/frontend/src/orchestrator/i18n/en-EU.json +1 -1
  123. package/services/frontend/src/orchestrator/i18n/sk.json +1 -1
  124. package/services/frontend/src/orchestrator/index.ts +34 -4
  125. package/services/frontend/src/orchestrator/router/routeParserHelper.ts +106 -1
  126. package/services/frontend/src/orchestrator/types/Widgets.ts +2 -0
  127. package/services/frontend/types.ts +1 -0
  128. package/src/README.md +404 -0
  129. package/src/types.ts +19 -5
  130. package/templates/README.md +0 -0
  131. package/types/index.ts +18 -4
  132. package/services/backend/.gitignore +0 -17
  133. package/services/frontend/.gitignore +0 -35
  134. package/yarn.lock +0 -2937
@@ -13,6 +13,7 @@ const axiosClient = axios.create({
13
13
  "Accept-Version": "*",
14
14
  },
15
15
  validateStatus: (status) => status >= 200 && status < 300,
16
+ withCredentials: true, // Required for CORS with credentials
16
17
  })
17
18
 
18
19
  // Fetch client configuration
@@ -370,6 +371,23 @@ const unsetHttpToken = () => {
370
371
  delete fetchClient.defaults.headers.common.Authorization
371
372
  }
372
373
 
374
+ const WORK_SESSION_HEADER = "x-work-session-id"
375
+
376
+ /**
377
+ * Set work session ID for unauthenticated shared sessions.
378
+ * When set, all API requests will include this header so backend can allow access via authenticatedOrWorksession.
379
+ * @param sessionId - Work session UUID or null to clear
380
+ */
381
+ const setWorkSessionId = (sessionId: string | null) => {
382
+ if (sessionId) {
383
+ axiosClient.defaults.headers.common[WORK_SESSION_HEADER] = sessionId
384
+ fetchClient.defaults.headers.common[WORK_SESSION_HEADER] = sessionId
385
+ } else {
386
+ delete axiosClient.defaults.headers.common[WORK_SESSION_HEADER]
387
+ delete fetchClient.defaults.headers.common[WORK_SESSION_HEADER]
388
+ }
389
+ }
390
+
373
391
  // Main export object with both clients
374
392
  const httpClient: {
375
393
  axios: typeof axiosClient
@@ -386,6 +404,7 @@ export {
386
404
  apiHealthcheck,
387
405
  setHttpToken,
388
406
  unsetHttpToken,
407
+ setWorkSessionId,
389
408
  configureHttpInterceptors,
390
409
  DEFAULT_TIMEOUT,
391
410
  }
@@ -6,6 +6,7 @@ import {
6
6
  configureHttpInterceptors,
7
7
  apiHealthcheck,
8
8
  setHttpToken,
9
+ setWorkSessionId,
9
10
  } from "@racletteCore/lib/httpClient"
10
11
 
11
12
  import { chadsayFE } from "@racletteCore"
@@ -66,13 +67,22 @@ const initPluginRegistration = async () => {
66
67
  }
67
68
  }
68
69
  const createRacletteApp = async (
69
- options = { requireAuth: true },
70
+ options: { requireAuth?: boolean; sessionId?: string } = {
71
+ requireAuth: true,
72
+ },
70
73
  ): Promise<CreateAppResponse> => {
71
74
  setupHttpClient()
75
+ if (options.sessionId) {
76
+ setWorkSessionId(options.sessionId)
77
+ } else {
78
+ setWorkSessionId(null)
79
+ }
72
80
 
73
81
  let user: User | null = null
74
82
  await configService.init()
75
- if (options.requireAuth) {
83
+
84
+ // If sessionId is provided, skip authentication
85
+ if (options.requireAuth && !options.sessionId) {
76
86
  const { authenticated, error, user: _user, code } = await checkAuthStatus()
77
87
 
78
88
  if (!authenticated) {
@@ -88,7 +98,7 @@ const createRacletteApp = async (
88
98
  user = _user!
89
99
  }
90
100
 
91
- await setupDependencies()
101
+ await setupDependencies(options.sessionId)
92
102
 
93
103
  store.dispatch({ type: "user/set", payload: user })
94
104
 
@@ -98,11 +108,11 @@ const createRacletteApp = async (
98
108
  return new Promise<CreateAppResponse>(async (resolve, reject) => {
99
109
  eventbus.on("ui/setupProject", (spaceId: string) => {
100
110
  resolve({
101
- authenticated: true,
111
+ authenticated: !!user || !!options.sessionId,
102
112
  setupComplete: true,
103
113
  spaceId,
104
114
  })
105
- console.log(chadsayFE("Raclette is up and running!"))
115
+ console.log(chadsayFE("racletteJS is up and running!"))
106
116
  })
107
117
  await awaitTime(2000)
108
118
  reject("timeouted")
@@ -182,7 +192,7 @@ const generateLoginCallback =
182
192
  return {
183
193
  success: false,
184
194
  // TODO: Constants would be nice here
185
- error: new Error("Invalid response: Token not found"),
195
+ error: "Invalid response: Token not found",
186
196
  message: "Authentication failed: Invalid server response",
187
197
  }
188
198
  }
@@ -201,7 +211,7 @@ const generateLoginCallback =
201
211
 
202
212
  return {
203
213
  success: false,
204
- error,
214
+ error: errorMessage,
205
215
  message: errorMessage,
206
216
  }
207
217
  }
@@ -210,7 +220,7 @@ const generateLoginCallback =
210
220
  const getCookie = (name: string): string | null => {
211
221
  const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"))
212
222
 
213
- return match ? match[2] : null
223
+ return match && match[2] ? match[2] : null
214
224
  }
215
225
 
216
226
  const checkAuthStatus = async (): Promise<{
@@ -246,7 +256,7 @@ const checkAuthStatus = async (): Promise<{
246
256
  }
247
257
  }
248
258
 
249
- const setupDependencies = async () => {
259
+ const setupDependencies = async (sessionId?: string) => {
250
260
  registerStoreEffects(store)
251
261
 
252
262
  // api client - remaining config
@@ -254,7 +264,7 @@ const setupDependencies = async () => {
254
264
  apiHealthcheck(store)
255
265
 
256
266
  // socket
257
- socket = createSocket(SOCKET_URL, appSessionId)
267
+ socket = createSocket(SOCKET_URL, appSessionId, { sessionId })
258
268
 
259
269
  registerSocketEvents(socket, store, eventbus)
260
270
  // listen to eventbus and send messages to the socket server
@@ -9,10 +9,10 @@ import log from "@racletteCore/lib/logger"
9
9
  * createSocket returns an instance of a custom SocketClient
10
10
  * @param {string} host - our socket endpoint
11
11
  * @param {string} appSessionId - The id for the current application - server session
12
- * @param {object} opts - socket.io options
12
+ * @param {object} opts - socket.io options (can include sessionId for work sessions)
13
13
  */
14
14
 
15
- const createSocket = (host, appSessionId, opts = {}) => {
15
+ const createSocket = (host, appSessionId, opts: { sessionId?: string } = {}) => {
16
16
  log.socket("createSocket", { host, appSessionId, opts })
17
17
  const token = getUserToken()
18
18
  const defaults = {
@@ -22,7 +22,8 @@ const createSocket = (host, appSessionId, opts = {}) => {
22
22
  transports: ["polling", "websocket"],
23
23
  withCredentials: true,
24
24
  auth: {
25
- token: token,
25
+ ...(token ? { token } : {}),
26
+ ...(opts.sessionId ? { sessionId: opts.sessionId } : {}),
26
27
  },
27
28
  extraHeaders: {
28
29
  "app-session-id": appSessionId,
@@ -5,6 +5,28 @@
5
5
  import log from "@racletteCore/lib/logger"
6
6
  import { getUserToken } from "./sessionHandler"
7
7
  import configService from "@racletteCore/lib/configService"
8
+ import { validate } from "uuid"
9
+
10
+ /**
11
+ * Extract sessionId from URL if present
12
+ * Supports racletteJS route format: /:workSession/id={uuid}
13
+ */
14
+ const extractSessionIdFromUrl = (): string | null => {
15
+ const path = window.location.pathname + (window.location.hash || "")
16
+ // Match /:workSession/id={uuid} pattern
17
+ const workSessionMatch = path.match(/\/:workSession\/id=([^/&#]+)/)
18
+
19
+ if (workSessionMatch && workSessionMatch[1]) {
20
+ const sessionId = workSessionMatch[1]
21
+ // Validate UUID format
22
+ if (validate(sessionId)) {
23
+ return sessionId
24
+ }
25
+ }
26
+
27
+ return null
28
+ }
29
+
8
30
  const registerSocketClient = ($socket, $eventbus) => {
9
31
  $eventbus.on("socket/join/room", (roomId?: string) => {
10
32
  const { requireAuthentication } = configService.getConfig()?.global ?? {
@@ -12,16 +34,22 @@ const registerSocketClient = ($socket, $eventbus) => {
12
34
  }
13
35
 
14
36
  let token: string | null = null
37
+ const sessionId = extractSessionIdFromUrl()
38
+
39
+ // Check if we're in a work session context (sessionId in URL indicates work session)
40
+ const isWorkSession = !!sessionId
15
41
 
16
42
  if (requireAuthentication) {
17
43
  token = getUserToken()
18
44
 
19
- if (!token) {
20
- return log.socket("emitJoin", "no token")
45
+ // For work sessions, we don't need a token - the sessionId is used for authentication
46
+ // The sessionId was already sent during socket handshake in the auth object
47
+ if (!token && !isWorkSession) {
48
+ return log.socket("emitJoin", "no token and not a work session")
21
49
  }
22
50
  }
23
51
 
24
- log.socket("emitJoin", { roomId, token })
52
+ log.socket("emitJoin", { roomId, token, sessionId, isWorkSession })
25
53
  $socket.emit("join", { roomId, token })
26
54
  })
27
55
 
@@ -12,6 +12,29 @@ import { setupSpace } from "@racletteCore/lib/projectDataHelper"
12
12
  import { handleServerItem } from "@racletteCore/lib/data/dataApi"
13
13
  import * as userNotifier from "@racletteCore/lib/userNotifier"
14
14
  import type { ProjectConfiguration } from "../store/types"
15
+ import { validate } from "uuid"
16
+ import { setHttpToken, setWorkSessionId } from "@racletteCore/lib/httpClient"
17
+
18
+ /**
19
+ * Extract sessionId from URL if present
20
+ * Supports racletteJS route format: /:workSession/id={uuid}
21
+ */
22
+ const extractSessionIdFromUrl = (): string | null => {
23
+ const path = window.location.pathname + (window.location.hash || "")
24
+ // Match /:workSession/id={uuid} pattern
25
+ const workSessionMatch = path.match(/\/:workSession\/id=([^/&#]+)/)
26
+
27
+ if (workSessionMatch && workSessionMatch[1]) {
28
+ const sessionId = workSessionMatch[1]
29
+ // Validate UUID format
30
+ if (validate(sessionId)) {
31
+ return sessionId
32
+ }
33
+ }
34
+
35
+ return null
36
+ }
37
+
15
38
  const registerSocketEvents = ($socket, $store, $eventbus) => {
16
39
  /**
17
40
  * connect
@@ -171,24 +194,85 @@ const registerSocketEvents = ($socket, $store, $eventbus) => {
171
194
 
172
195
  /**
173
196
  * on joinConfirmation this means that we entered a new project with a new scope!
197
+ * For work sessions, the payload structure is different: { session, composition }
174
198
  */
175
199
  $socket.on(
176
200
  "joinConfirmation",
177
201
  (payload: {
178
- project: { _id: string; account: string }
179
- projectConfig: ProjectConfiguration
180
- compositions: Composition[]
181
- interactionLinks: InteractionLink[]
202
+ project?: { _id: string; account: string }
203
+ projectConfig?: ProjectConfiguration
204
+ compositions?: Composition[]
205
+ interactionLinks?: InteractionLink[]
206
+ token?: string // Work session JWT token
182
207
  }) => {
183
- const { project, projectConfig, compositions, interactionLinks } = payload
208
+ const { project, projectConfig, compositions, interactionLinks, token } = payload
209
+
210
+ // Check if this is a work session (no project but has compositions/interactionLinks)
211
+ const isWorkSession = !project && compositions && compositions.length > 0
212
+
213
+ if (isWorkSession) {
214
+ // Work session: use session ID as room identifier
215
+ const sessionId = extractSessionIdFromUrl() || "work_session"
216
+ const resolveId = "join_session_" + sessionId
217
+
218
+ log.socket("onJoinConfirmation", "Work session join", payload)
219
+
220
+ // If token is provided, use it for authentication (replaces x-work-session-id header)
221
+ if (token) {
222
+ setHttpToken(token)
223
+ // Clear the x-work-session-id header since we're using token now
224
+ setWorkSessionId(null)
225
+ log.socket("onJoinConfirmation", "Work session token set", { hasToken: !!token })
226
+ }
227
+
228
+ $eventbus.once("store_actionResolved_" + resolveId, async (action) => {
229
+ $store.dispatch({
230
+ type: "ui/update",
231
+ payload: { loading: true },
232
+ })
233
+
234
+ // Use the same setupSpace function as logged-in users
235
+ // This ensures all normal processing happens (landing page detection, etc.)
236
+ await setupSpace(compositions || [], interactionLinks || [], projectConfig || {})
184
237
 
185
- const roomId = project._id
238
+ // Don't navigate - let the default composition loading logic handle it
239
+ // The URL stays as /:workSession/id={uuid} and the router will match
240
+ // the composition from the store based on the default logic
241
+
242
+ // Enable fullscreen mode (hide nav bars) for work sessions
243
+ $store.dispatch({
244
+ type: "ui/update",
245
+ payload: {
246
+ availableInterfaces: [], // Hide all UI interfaces for fullscreen
247
+ navigationOpen: false,
248
+ },
249
+ })
250
+
251
+ $store.dispatch({
252
+ type: "ui/update",
253
+ payload: { loading: false },
254
+ })
255
+
256
+ $eventbus.emit("ui/setupProject", sessionId)
257
+ })
258
+
259
+ // Reset store states for work session
260
+ $store.dispatch({
261
+ type: "projectInit",
262
+ resolveId,
263
+ })
264
+
265
+ return
266
+ }
267
+
268
+ // Regular project join confirmation (existing flow)
269
+ const roomId = project?._id
186
270
 
187
271
  if (!project) {
188
272
  log.socket("onJoinConfirmation", "no Room")
189
-
190
273
  return
191
274
  }
275
+
192
276
  log.socket("onJoinConfirmation", payload)
193
277
 
194
278
  const resolveId = "join_" + roomId
@@ -204,7 +288,7 @@ const registerSocketEvents = ($socket, $store, $eventbus) => {
204
288
  type: "ui/update",
205
289
  payload: { loading: true },
206
290
  })
207
- await setupSpace(compositions, interactionLinks, projectConfig)
291
+ await setupSpace(compositions || [], interactionLinks || [], projectConfig || {})
208
292
 
209
293
  $eventbus.emit("ui/setupProject", roomId)
210
294
  })
@@ -37,6 +37,7 @@ import {
37
37
  import {
38
38
  compositionsReducers,
39
39
  compositionsEffects,
40
+ compositionsSelectors,
40
41
  } from "./reducers/compositions"
41
42
  import backendReducers from "./reducers/backend"
42
43
  import { queriesEffects, queriesReducers } from "./reducers/queries"
@@ -51,7 +52,7 @@ const extensions =
51
52
  ? [
52
53
  new LoggerExtension(),
53
54
  new ReduxDevtoolsExtension({
54
- name: "Raclette",
55
+ name: "racletteJS",
55
56
  trace: true,
56
57
  traceLimit: 25,
57
58
  }),
@@ -129,6 +130,9 @@ const createStore = (eventbus, appSessionId: string): RacletteStore => {
129
130
  appSessionId,
130
131
  hasChanges: (oldVal: string, newVal: string): boolean =>
131
132
  JSON.stringify(oldVal) !== JSON.stringify(newVal),
133
+ selectors: {
134
+ compositions: compositionsSelectors,
135
+ },
132
136
  getState: (params: string | any[] | StoreHelperGetState = {}): object => {
133
137
  // Default values
134
138
  let stateKey = ""
@@ -11,7 +11,7 @@ import type { WidgetBase } from "../../types"
11
11
  import { v4 as uuidv4 } from "uuid"
12
12
  import { $store } from "@racletteCore"
13
13
  import log from "@racletteCore/lib/logger"
14
-
14
+ import * as compositionsSelectors from "./selectors"
15
15
  const reducerMap = {
16
16
  init: () => applicationState.pages,
17
17
  projectInit: reducer.projectInit,
@@ -82,4 +82,4 @@ const compositionsEffects = (store) => {
82
82
  }
83
83
  const compositionsReducers = reducerFromMap(reducerMap)
84
84
 
85
- export { compositionsEffects, compositionsReducers }
85
+ export { compositionsEffects, compositionsReducers, compositionsSelectors }
@@ -6,7 +6,7 @@ import reducerFromMap from "@racletteCore/lib/reducerFromMap"
6
6
  import { applicationState } from "@racletteCore/store/state"
7
7
  import interactionLinksEffects from "./effects"
8
8
  const reducerMap = {
9
- init: () => applicationState.pages,
9
+ init: () => applicationState.interactionLinks,
10
10
  projectInit: reducer.projectInit,
11
11
  "interactionLinks/add": reducer.interactionLinksAdd,
12
12
  "interactionLinks/set": reducer.interactionLinksSet,
@@ -3,9 +3,8 @@ import { applicationState, storePatterns } from "@racletteCore/store/state"
3
3
 
4
4
  const cleanWidgetState = (widget) => {
5
5
  const cleanWidget = clone(widget)
6
-
7
6
  delete cleanWidget.type
8
-
7
+ // TODO implement empty state attribute deletion here, using removeEmpty
9
8
  return cleanWidget
10
9
  }
11
10
 
@@ -90,7 +90,6 @@ export type NotificationsState = {
90
90
  history: []
91
91
  items: { [key: string]: Notification }
92
92
  }
93
-
94
93
  export type ApplicationState = {
95
94
  widgets: {
96
95
  [key: string]: WidgetBase
@@ -12,6 +12,8 @@ import type {
12
12
  PluginApi,
13
13
  } from "@racletteOrchestrator"
14
14
  import type { DefineComponent } from "vue"
15
+ import type { UIState, RouteState, WidgetBase } from "@racletteCore/store/types"
16
+ import type { Query } from "@racletteCore/store/reducers/queries/reducers"
15
17
  export type BroadcastChannelDefinition = {
16
18
  channel: string
17
19
  channelKey: string
@@ -77,12 +79,12 @@ export type PluginFrontendDefinition = {
77
79
  type StorePayloadObject = Record<string, any>
78
80
 
79
81
  export type PluginStoreApi = {
80
- getUiState<T = any>(): T
81
- setUiState: (payload: StorePayloadObject) => void
82
- getWidgetState<T = any>(widgetId: string): T
83
- updateWidgetState: (payload: StorePayloadObject, widgetId: string) => void
84
- setWidgetState: (payload: StorePayloadObject, widgetId: string) => void
85
- getQueryState<T = any>(actionId: actionId): T
82
+ getUiState(): UIState
83
+ setUiState: (payload: Partial<UIState>) => void
84
+ getWidgetState(widgetId: string): WidgetBase | Record<string, never>
85
+ updateWidgetState: (payload: Partial<WidgetBase>, widgetId: string) => void
86
+ setWidgetState: (payload: Partial<WidgetBase>, widgetId: string) => void
87
+ getQueryState(actionId: actionId): Query | Record<string, never>
86
88
  getQueryData<T = any>(actionId: actionId): T
87
89
  get<T = any>(pathArray: string[], filter: Record<string, any>): T
88
90
  }
@@ -13,5 +13,6 @@ export type StoreHelperGetState = {
13
13
  export type RacletteStore = ReduxStore & {
14
14
  hasChanges: (oldVal: string, newVal: string) => boolean
15
15
  getState: (params: string | any[] | StoreHelperGetState) => any
16
+ selectors: Record<string, any>
16
17
  appSessionId?: String
17
18
  }
@@ -87,11 +87,20 @@
87
87
  <script setup lang="ts">
88
88
  import type { Composition } from "@shared/types/core/Composition.types"
89
89
  import type { WidgetBase } from "@racletteCore/store/types"
90
- import { watch, ref, h, computed, type Component } from "vue"
90
+ import {
91
+ watch,
92
+ ref,
93
+ h,
94
+ computed,
95
+ onUnmounted,
96
+ nextTick,
97
+ type Component,
98
+ } from "vue"
91
99
  import { widgetLoader } from "@racletteOrchestrator/helpers/widgetLoader"
92
100
  import { useDisplay } from "vuetify"
93
101
  import useStateSubscriber from "@racletteOrchestrator/composables/useStateSubscriber"
94
102
  import useRouteState from "@racletteOrchestrator/composables/useRouteState"
103
+ import { createWidgetLifecycleManager } from "@racletteOrchestrator/composables/useWidgetLifecycle"
95
104
 
96
105
  const props = withDefaults(
97
106
  defineProps<{
@@ -104,6 +113,7 @@ const props = withDefaults(
104
113
  maxHeight: 0,
105
114
  },
106
115
  )
116
+
107
117
  const { compositionSlots } = useRouteState()
108
118
 
109
119
  const currentRouteConfig = computed(() =>
@@ -156,12 +166,18 @@ const gridKey = ref(0)
156
166
 
157
167
  // Calculate max height per widget based on grid layout
158
168
  const maxWidgetHeight = computed(() => {
159
- // Always set max height to the calculated available screen height (with buffer)
160
- // Later we should determine a max value based on other widgets in each row, so this value will be different across rows and columns
161
-
162
169
  return Math.min(props.maxHeight, 2400)
163
170
  })
164
171
 
172
+ // Calculate total number of widgets
173
+ const totalWidgets = computed(() => {
174
+ return props.widgetsLayout.reduce((sum, column) => sum + column.length, 0)
175
+ })
176
+
177
+ // Widget lifecycle manager
178
+ const lifecycleManager = ref(createWidgetLifecycleManager(totalWidgets.value))
179
+
180
+ // Widget component cache
165
181
  const widgetComponentCache = new Map<string, Component>()
166
182
 
167
183
  const getSlotWidget = (widget: WidgetBase) => {
@@ -169,11 +185,45 @@ const getSlotWidget = (widget: WidgetBase) => {
169
185
 
170
186
  if (!widgetComponentCache.has(cacheKey)) {
171
187
  try {
172
- const component = widgetLoader(widget)
173
- widgetComponentCache.set(cacheKey, component)
188
+ const originalComponent = widgetLoader(widget)
189
+
190
+ // Wrap the component to inject lifecycle hooks
191
+ const wrappedComponent = {
192
+ name: `${originalComponent.name || "Widget"}Wrapper`,
193
+ setup(props, context) {
194
+ const { onAllWidgetsReady, onWidgetRendered, onWidgetUnmounted } =
195
+ lifecycleManager.value.createLifecycleHooks(widget.uuid)
196
+
197
+ onUnmounted(() => {
198
+ onWidgetUnmounted()
199
+ })
200
+
201
+ // Pass lifecycle hook to the component as a prop
202
+ const enhancedProps = {
203
+ ...props,
204
+ onAllWidgetsReady,
205
+ ref: (el: any) => {
206
+ if (el) {
207
+ // Element is mounted, observe it
208
+ nextTick(() => {
209
+ // Get the actual DOM element
210
+ const element = el.$el || el
211
+ if (element instanceof HTMLElement) {
212
+ onWidgetRendered(element)
213
+ }
214
+ })
215
+ }
216
+ },
217
+ }
218
+
219
+ return () => h(originalComponent, enhancedProps, context.slots)
220
+ },
221
+ }
222
+
223
+ widgetComponentCache.set(cacheKey, wrappedComponent)
174
224
  } catch (error) {
175
225
  console.error("🚨 Error creating widget component:", error)
176
- // cache error component too
226
+ // Cache error component too
177
227
  widgetComponentCache.set(cacheKey, {
178
228
  name: "ComponentCreationError",
179
229
  setup() {
@@ -195,10 +245,20 @@ const getGridCols = (rowLength) => (mdAndDown.value ? 12 : 12 / rowLength)
195
245
 
196
246
  const getSlotCols = (slot) => (mdAndDown.value ? 12 : slot.column)
197
247
 
198
- // Watch for changes in widgetsLayout to trigger transition
248
+ // Watch for changes in widgetsLayout to trigger transition and reset lifecycle
199
249
  watch(
200
250
  () => props.widgetsLayout,
201
251
  () => {
252
+ // Reset lifecycle manager
253
+ lifecycleManager.value.reset()
254
+
255
+ // Clear component cache to force re-wrapping with new lifecycle manager
256
+ widgetComponentCache.clear()
257
+
258
+ // Create new lifecycle manager with updated widget count
259
+ lifecycleManager.value = createWidgetLifecycleManager(totalWidgets.value)
260
+
261
+ // Trigger grid transition
202
262
  gridKey.value++
203
263
  },
204
264
  { deep: true },
@@ -38,7 +38,7 @@
38
38
  </template>
39
39
  <v-list-item-title>{{ $t("core.toggle_theme") }}</v-list-item-title>
40
40
  </v-list-item>
41
- <v-list-item v-if="isDecember()" @click="eggMe('xmas')">
41
+ <v-list-item v-if="isWinter()" @click="toggleSnow">
42
42
  <template #prepend>
43
43
  <v-icon>mdi-snowflake</v-icon>
44
44
  </template>
@@ -145,13 +145,20 @@ const { getCompositionLinkForSlotName } = useRouteState()
145
145
 
146
146
  const userLanguage = useLocalStorage<string>("user-language", null)
147
147
  const userTheme = useLocalStorage<string>("user-theme", null)
148
+ const showSnow = useLocalStorage<boolean>("show-snow", true)
148
149
  const { state: pageNavigation } = usePageNavigation()
149
150
 
150
151
  const isOpen = ref(false)
151
152
  const canRefresh = ref(true)
152
153
 
153
154
  const requireAuthentication = configService.getRequireAuthentication()
154
- const isDecember = () => new Date().getMonth() === 11
155
+ const isWinter = () => {
156
+ const month = new Date().getMonth()
157
+ if (month === 11 || month === 0) {
158
+ return true
159
+ }
160
+ return false
161
+ }
155
162
  const currentPageRoute = getCompositionLinkForSlotName(DEFAULT_SLOT_NAME)
156
163
 
157
164
  const activeRouteName = computed(() => {
@@ -214,11 +221,16 @@ const openEgg = () =>
214
221
  isOpen.value = false
215
222
  eggMe("asteroids")
216
223
  })
217
-
224
+ const toggleSnow = () => {
225
+ showSnow.value = !showSnow.value
226
+ eggMe("xmas")
227
+ }
218
228
  onMounted(async () => {
219
- if (isDecember()) {
229
+ if (isWinter()) {
220
230
  await awaitTime(600)
221
- eggMe("xmas")
231
+ if (showSnow.value) {
232
+ eggMe("xmas")
233
+ }
222
234
  }
223
235
  })
224
236
  </script>
@@ -112,7 +112,7 @@ const configurations = computed(() => [
112
112
  // Methods
113
113
  const handleWorkbenchClick = () => {
114
114
  // Navigate to workbench or emit event
115
- console.log("Navigate to Raclette Workbench")
115
+ console.log("Navigate to racletteJS Workbench")
116
116
  // router.push('/workbench') // Uncomment when route is available
117
117
  }
118
118
  </script>
@@ -3,3 +3,4 @@ export * from "./useRouteState"
3
3
  export * from "./useCurrentComposition"
4
4
  export * from "./useWidgets"
5
5
  export * from "./useInteractionLinks"
6
+ export * from "./useWidgetLifecycle"
@@ -169,7 +169,6 @@ export const usePluginApi = (
169
169
  }
170
170
  return getResult()
171
171
  },
172
-
173
172
  getQueryState: (actionId) => {
174
173
  if (!actionId) return {}
175
174
  const getResult = () => {
@@ -185,7 +184,6 @@ export const usePluginApi = (
185
184
  console.error("not implemented yet")
186
185
  },
187
186
  get: (pathArray, filter) => {
188
- const cacheString = createStableHash({ pathArray, filter })
189
187
  const getResult = () => {
190
188
  const { state } = getState(pathArray, filter)
191
189
  return state
@@ -206,7 +204,6 @@ export const usePluginApi = (
206
204
  payload,
207
205
  })
208
206
  },
209
-
210
207
  updateWidgetState: (
211
208
  payload: object,
212
209
  widgetId: string = getWidgetId(),