@raclettejs/core 0.1.35 → 0.1.36-canary.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 (101) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/dist/cli.js +86 -82
  3. package/dist/cli.js.map +3 -3
  4. package/package.json +5 -5
  5. package/services/.dockerignore +2 -0
  6. package/services/backend/.dockerignore +3 -1
  7. package/services/backend/src/core/payload/payloadTypes.ts +1 -0
  8. package/services/backend/src/core/pluginSystem/configGenerator/index.ts +12 -11
  9. package/services/backend/src/core/pluginSystem/pluginFastifyInstance.ts +28 -3
  10. package/services/backend/src/core/pluginSystem/pluginLoader.ts +11 -2
  11. package/services/backend/src/core/pluginSystem/pluginTypes.ts +39 -1
  12. package/services/backend/src/core/sockets/createSystemEventHandlers.ts +4 -4
  13. package/services/backend/src/core/sockets/socketManager.ts +80 -32
  14. package/services/backend/src/core/sockets/socketTypes.ts +6 -0
  15. package/services/backend/src/core/validation/schemaGenerator.ts +10 -3
  16. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.service.ts +1 -1
  17. package/services/backend/src/corePlugins/raclette__core/backend/index.ts +0 -2
  18. package/services/backend/src/modules/authentication/index.ts +15 -23
  19. package/services/backend/src/shared/helper/adminTag.ts +34 -0
  20. package/services/backend/src/shared/types/plugins/index.ts +1 -9
  21. package/services/backend/src/utils/request.utils.ts +1 -1
  22. package/services/frontend/.dockerignore +3 -1
  23. package/services/frontend/src/core/lib/data/dataApi.ts +27 -16
  24. package/services/frontend/src/core/lib/data/queryApi.ts +17 -8
  25. package/services/frontend/src/core/lib/data/responseTypeHandler.ts +17 -8
  26. package/services/frontend/src/core/lib/data/writeDataHandler.ts +3 -1
  27. package/services/frontend/src/core/lib/dataHelper.ts +51 -12
  28. package/services/frontend/src/core/lib/eventWhitelist.ts +2 -0
  29. package/services/frontend/src/core/lib/httpClient.ts +16 -2
  30. package/services/frontend/src/core/lib/userNotifier.ts +20 -0
  31. package/services/frontend/src/core/lib/workSessionUrl.ts +16 -0
  32. package/services/frontend/src/core/main.ts +82 -15
  33. package/services/frontend/src/core/setup/socketBootstrap.ts +83 -0
  34. package/services/frontend/src/core/setup/socketClient.ts +39 -39
  35. package/services/frontend/src/core/setup/socketEvents.ts +152 -158
  36. package/services/frontend/src/core/setup/socketRegistry.ts +19 -0
  37. package/services/frontend/src/core/types/DataApi.ts +10 -2
  38. package/services/frontend/src/core/types/PluginRegistrar.ts +1 -1
  39. package/services/frontend/src/orchestrator/ProductOrchestrator.vue +14 -0
  40. package/services/frontend/src/orchestrator/assets/styles/layers.css +4 -1
  41. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +267 -35
  42. package/services/frontend/src/orchestrator/components/menu/ServerStatus.vue +11 -2
  43. package/services/frontend/src/orchestrator/composables/useVueQueryObservableHelper.ts +12 -7
  44. package/services/frontend/src/orchestrator/composables/useVueWriteOperationHelper.ts +12 -6
  45. package/services/frontend/src/orchestrator/i18n/de-DE.json +1 -0
  46. package/services/frontend/src/orchestrator/i18n/en-EU.json +1 -0
  47. package/services/frontend/src/orchestrator/i18n/sk.json +1 -0
  48. package/services/frontend/src/orchestrator/i18n/tl-TL.json +1 -0
  49. package/services/frontend/src/orchestrator/index.ts +7 -24
  50. package/services/frontend/src/orchestrator/lib/queryParamsSource.ts +48 -0
  51. package/services/frontend/src/orchestrator/main.ts +2 -2
  52. package/services/frontend/src/orchestrator/router/index.ts +0 -19
  53. package/services/frontend/src/orchestrator/router/registerEventHandlers.ts +29 -0
  54. package/services/frontend/src/orchestrator/router/routeStore.ts +92 -17
  55. package/services/frontend/src/orchestrator/router/routes.ts +5 -0
  56. package/services/frontend/src/orchestrator/setup/application.ts +11 -2
  57. package/services/frontend/src/orchestrator/types/DataApi.ts +19 -0
  58. package/services/frontend/src/orchestrator/types/PluginApi.ts +4 -7
  59. package/services/frontend/src/orchestrator/types/Widgets.ts +2 -0
  60. package/services/frontend/src/orchestrator/types/index.ts +1 -0
  61. package/templates/fs.yml +1 -1
  62. package/package/dist/index.js +0 -452
  63. package/services/backend/.yarn/install-state.gz +0 -0
  64. package/services/backend/dist/core/eventBus/index.js +0 -7
  65. package/services/backend/dist/core/pluginSystem/configGenerator/index.js +0 -346
  66. package/services/backend/dist/core/sockets/index.js +0 -95
  67. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/events/index.js +0 -31
  68. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/index.js +0 -48
  69. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/routes/index.js +0 -15
  70. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/events/index.js +0 -22
  71. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/index.js +0 -51
  72. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/routes/index.js +0 -19
  73. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/events/index.js +0 -22
  74. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/index.js +0 -48
  75. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.js +0 -19
  76. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/events/index.js +0 -31
  77. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/index.js +0 -49
  78. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/routes/index.js +0 -16
  79. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/events/index.js +0 -39
  80. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/index.js +0 -50
  81. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/routes/index.js +0 -19
  82. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/events/index.js +0 -31
  83. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/index.js +0 -51
  84. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/routes/index.js +0 -21
  85. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/events/index.js +0 -31
  86. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/index.js +0 -48
  87. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.js +0 -15
  88. package/services/backend/dist/corePlugins/raclette__core/backend/index.js +0 -34
  89. package/services/backend/dist/domains/index.js +0 -11
  90. package/services/backend/dist/domains/system/index.js +0 -11
  91. package/services/backend/dist/domains/system/routes/index.js +0 -17
  92. package/services/backend/dist/helpers/index.js +0 -14
  93. package/services/backend/dist/index.js +0 -3
  94. package/services/backend/dist/modules/authentication/index.js +0 -253
  95. package/services/backend/dist/shared/types/core/index.js +0 -8
  96. package/services/backend/dist/shared/types/dataTypes/index.js +0 -6
  97. package/services/backend/dist/shared/types/index.js +0 -8
  98. package/services/backend/dist/shared/types/plugins/index.js +0 -8
  99. package/services/backend/dist/types/index.js +0 -12
  100. package/services/backend/dist/utils/index.js +0 -2
  101. package/services/frontend/.yarn/install-state.gz +0 -0
@@ -10,4 +10,6 @@ package.json
10
10
  yarn.lock
11
11
  devTools
12
12
  provision
13
- .DS_Store
13
+ .DS_Store
14
+ .yarnrc.yml
15
+ .yarn
@@ -5,16 +5,19 @@ import getData from "@racletteCore/lib/data/fetchDataHandler"
5
5
  import handleServerItem from "./responseTypeHandler"
6
6
  import $queryApi from "./queryApi"
7
7
  import writeData from "./writeDataHandler"
8
- import type { CustomStoreActionsPayload } from "@racletteCore/types"
8
+ import type {
9
+ CustomStoreActionsPayload,
10
+ QueryParamsInput,
11
+ } from "@racletteCore/types"
12
+ import { resolveQueryParams } from "@racletteCore/lib/dataHelper"
9
13
  import { BehaviorSubject } from "rxjs"
10
- import { clone, mergeDeepRight } from "ramda"
14
+ import { mergeDeepRight } from "ramda"
11
15
 
12
16
  const $writeDataApi = (
13
17
  actionId: String,
14
18
  operationDefinition: object,
15
19
  { params = {}, options = {} }: CustomStoreActionsPayload = {},
16
20
  ) => {
17
- const _params = clone(params)
18
21
  // Create BehaviorSubjects for state management
19
22
  const isLoadingSubject$ = new BehaviorSubject<boolean>(false)
20
23
  const dataSubject$ = new BehaviorSubject([])
@@ -25,18 +28,26 @@ const $writeDataApi = (
25
28
  data$: dataSubject$.asObservable(),
26
29
  error$: errorSubject$.asObservable(),
27
30
  response$: responseSubject$.asObservable(),
28
- execute: async (executeParams = {}) => {
29
- const _executeParams = clone(executeParams)
31
+ execute: async (executeParams?: QueryParamsInput | FormData | unknown[]) => {
32
+ const isFormData =
33
+ typeof FormData !== "undefined" && executeParams instanceof FormData
30
34
  errorSubject$.next(null)
31
35
  responseSubject$.next({})
32
36
  let queryParams
33
- if (Array.isArray(_executeParams)) {
34
- if (Array.isArray(_params)) {
35
- queryParams = _params.concat(_executeParams)
36
- }
37
- queryParams = _executeParams
37
+ if (isFormData) {
38
+ queryParams = executeParams
39
+ } else if (Array.isArray(executeParams)) {
40
+ const baseParams = resolveQueryParams(params)
41
+ queryParams = Array.isArray(baseParams)
42
+ ? baseParams.concat(executeParams)
43
+ : executeParams
44
+ } else if (executeParams === undefined) {
45
+ queryParams = resolveQueryParams(params)
38
46
  } else {
39
- queryParams = mergeDeepRight(_params, _executeParams)
47
+ queryParams = mergeDeepRight(
48
+ resolveQueryParams(params),
49
+ resolveQueryParams(executeParams as QueryParamsInput),
50
+ )
40
51
  }
41
52
 
42
53
  if (options.responseType === "stream") {
@@ -53,16 +64,16 @@ const $writeDataApi = (
53
64
  errorSubject$.next(e.response)
54
65
  isLoadingSubject$.next(false)
55
66
  })
56
- responseSubject$.next(resultData?.response)
57
67
 
58
68
  // Update both boolean and observable on success
69
+ responseSubject$.next(resultData?.response)
59
70
  isLoadingSubject$.next(false)
60
71
 
61
72
  // Update data if resultData is available
62
- if (resultData?.result) {
63
- const dataArray = Array.isArray(resultData.result)
64
- ? resultData.result
65
- : [resultData.result]
73
+ if (resultData?.response) {
74
+ const dataArray = Array.isArray(resultData.response)
75
+ ? resultData.response
76
+ : [resultData.response]
66
77
  dataSubject$.next(dataArray)
67
78
  }
68
79
 
@@ -2,11 +2,14 @@
2
2
  * data api for the store
3
3
  */
4
4
  import { getData } from "@racletteCore/lib/data/dataApi"
5
- import { fixQueryParamTypes } from "@racletteCore/lib/dataHelper"
5
+ import { resolveQueryParams } from "@racletteCore/lib/dataHelper"
6
6
  import { $store, $eventbus } from "@racletteCore"
7
- import type { CustomStoreActionsPayload } from "@racletteCore/types"
7
+ import type {
8
+ CustomStoreActionsPayload,
9
+ QueryParamsInput,
10
+ } from "@racletteCore/types"
8
11
  import { BehaviorSubject } from "rxjs"
9
- import { mergeDeepRight, clone } from "ramda"
12
+ import { mergeDeepRight } from "ramda"
10
13
 
11
14
  const callbacks = {}
12
15
  const queryApis = {}
@@ -22,7 +25,6 @@ export const $queryApi = (
22
25
  }
23
26
  const errorSubject$ = new BehaviorSubject(null)
24
27
  const responseSubject$ = new BehaviorSubject({})
25
- const cleanedParams = fixQueryParamTypes(params)
26
28
  // check if we have a query fullfilling the needs of the one requested
27
29
  // here. If not we will update the one with this actionId
28
30
  let actionType = "queries/add"
@@ -39,7 +41,10 @@ export const $queryApi = (
39
41
  payload: {
40
42
  actionId,
41
43
  target,
42
- params: cleanedParams,
44
+ // TODO: subscriber params are re-resolved on execute(), but this registration
45
+ // snapshot may leave store query params stale until the next execute()
46
+ // we should check the query behaviour for the store and how to deal with changing queries
47
+ params: resolveQueryParams(params),
43
48
  options,
44
49
  config,
45
50
  },
@@ -66,11 +71,15 @@ export const $queryApi = (
66
71
  responseSubject$.complete()
67
72
  errorSubject$.complete()
68
73
  },
69
- execute: async (executeParams = {}) => {
70
- const _executeParams = clone(executeParams)
74
+ execute: async (executeParams?: QueryParamsInput) => {
71
75
  errorSubject$.next(null)
72
76
  responseSubject$.next({})
73
- const queryParams = mergeDeepRight(cleanedParams, _executeParams)
77
+ // `params` may be a subscriber () => {...} from the orchestrator — always resolve here.
78
+ const baseParams = resolveQueryParams(params)
79
+ const queryParams =
80
+ executeParams === undefined
81
+ ? baseParams
82
+ : mergeDeepRight(baseParams, resolveQueryParams(executeParams))
74
83
  await $store.dispatch({
75
84
  type: "queries/update",
76
85
  payload: {
@@ -7,6 +7,15 @@ import * as userNotifier from "@racletteCore/lib/userNotifier"
7
7
  import { $store, markDataAsModified } from "@racletteCore"
8
8
 
9
9
  const handledRequests = {}
10
+
11
+ const resolveDeletedIds = (data) => {
12
+ if (data.deleted?.length) {
13
+ return data.deleted
14
+ }
15
+ return (data.items || [])
16
+ .map((item) => (typeof item === "string" ? item : item?._id))
17
+ .filter(Boolean)
18
+ }
10
19
  const hasBeenHandled = (requestId, appRequestCode) => {
11
20
  if (requestId === "system") {
12
21
  return false
@@ -137,13 +146,13 @@ const handler = {
137
146
  requestId,
138
147
  data,
139
148
  })
140
- const deleted = data.items || []
141
- $store.dispatch({ type: "data/hardDelete", payload: deleted })
149
+ const deletedIds = resolveDeletedIds(data)
150
+ $store.dispatch({ type: "data/hardDelete", payload: deletedIds })
142
151
  if (notify) {
143
- userNotifier.dataDelete(isOwnRequest, isSystemAction, deleted)
152
+ userNotifier.dataDelete(isOwnRequest, isSystemAction, data.items || [])
144
153
  }
145
154
 
146
- return deleted
155
+ return deletedIds
147
156
  },
148
157
  dataHardDeleted: (
149
158
  data,
@@ -162,12 +171,12 @@ const handler = {
162
171
  requestId,
163
172
  data,
164
173
  })
165
- const deleted = data.items || []
166
- $store.dispatch({ type: "data/hardDelete", payload: deleted })
174
+ const deletedIds = resolveDeletedIds(data)
175
+ $store.dispatch({ type: "data/hardDelete", payload: deletedIds })
167
176
  if (notify) {
168
- userNotifier.dataDelete(isOwnRequest, isSystemAction, deleted)
177
+ userNotifier.dataDelete(isOwnRequest, isSystemAction, data.items || [])
169
178
  }
170
- return deleted
179
+ return deletedIds
171
180
  },
172
181
  dataMove: (
173
182
  data,
@@ -73,7 +73,9 @@ const storeActions = {
73
73
  }
74
74
  const getAxiosPromise = async (method, target, source, config) => {
75
75
  if (method === "delete") {
76
- return await $api.axios.delete(target, config, { data: { source } })
76
+ const deleteConfig =
77
+ source != null ? { ...config, data: source } : config
78
+ return await $api.axios.delete(target, deleteConfig)
77
79
  }
78
80
  return await $api.axios[method](target, source, config)
79
81
  }
@@ -3,6 +3,8 @@
3
3
  *
4
4
  */
5
5
  import { clone, path } from "ramda"
6
+ import type { QueryParamsInput } from "@racletteCore/types"
7
+ import { isDevelopmentMode } from "@racletteCore/helpers/devMode"
6
8
  import log from "@racletteCore/lib/logger"
7
9
  import { $eventbus } from "@racletteCore/main"
8
10
  import { $store } from "@racletteCore/main"
@@ -225,6 +227,10 @@ const replaceLast = (str, pattern, replacement) => {
225
227
  *
226
228
  */
227
229
  const removeCacheAttributes = (item) => {
230
+ if (typeof FormData !== "undefined" && item instanceof FormData) {
231
+ return item
232
+ }
233
+
228
234
  const _item = clone(item)
229
235
 
230
236
  if (_item.__requestedFor) {
@@ -417,25 +423,57 @@ const getValueFromNestedAttributeString = (attrString, item) => {
417
423
  const isAnswerToOwnRequest = (appSessionId) =>
418
424
  appSessionId == $store.appSessionId
419
425
 
420
- const fixQueryParamTypes = (params) => {
421
- const fixedParams = clone(params)
426
+ const NUMERIC_QUERY_OPTIONS = ["from", "skip", "size", "limit"] as const
422
427
 
423
- if (params?.options?.from) {
424
- fixedParams.options.from = parseInt(params.options.from)
425
- }
426
- if (params?.options?.skip) {
427
- fixedParams.options.skip = parseInt(params.options.skip)
428
- }
429
- if (params?.options?.size) {
430
- fixedParams.options.size = parseInt(params.options.size)
428
+ /** Clone query params and coerce `options.from|skip|size|limit` to integers (often strings from URLs/forms). */
429
+ const fixQueryParamTypes = (params: Record<string, unknown> = {}) => {
430
+ const fixedParams = clone(params) as Record<string, unknown>
431
+ const options = fixedParams.options
432
+
433
+ if (options == null || typeof options !== "object" || Array.isArray(options)) {
434
+ return fixedParams
431
435
  }
432
- if (params?.options?.limit) {
433
- fixedParams.options.limit = parseInt(params.options.limit)
436
+
437
+ const opts = options as Record<string, unknown>
438
+ for (const key of NUMERIC_QUERY_OPTIONS) {
439
+ if (opts[key] != null) {
440
+ opts[key] = parseInt(String(opts[key]), 10)
441
+ }
434
442
  }
435
443
 
436
444
  return fixedParams
437
445
  }
438
446
 
447
+ const isPlainParamsRecord = (
448
+ value: unknown,
449
+ ): value is Record<string, unknown> =>
450
+ value != null && typeof value === "object" && !Array.isArray(value)
451
+
452
+ const warnInvalidResolvedQueryParams = (value: unknown): void => {
453
+ if (!isDevelopmentMode()) return
454
+ const kind =
455
+ value == null ? String(value) : Array.isArray(value) ? "array" : typeof value
456
+ log.warning(
457
+ "Invalid query params",
458
+ `Params subscriber resolved to ${kind}, expected a plain object. Using {}.`,
459
+ )
460
+ }
461
+
462
+ /** Resolve plain params or subscriber, then normalize query param types. */
463
+ const resolveQueryParams = (
464
+ params?: QueryParamsInput,
465
+ ): Record<string, unknown> => {
466
+ let resolved: unknown = params
467
+ if (typeof resolved === "function") {
468
+ resolved = resolved()
469
+ }
470
+ if (!isPlainParamsRecord(resolved)) {
471
+ warnInvalidResolvedQueryParams(resolved)
472
+ return fixQueryParamTypes({})
473
+ }
474
+ return fixQueryParamTypes(resolved)
475
+ }
476
+
439
477
  const promisifyActionResolve = (resolveId: string, callback?: Function) => {
440
478
  const handleCleanup = (result = {}) => {
441
479
  if (typeof callback === "function") {
@@ -473,6 +511,7 @@ export {
473
511
  debounceHelper,
474
512
  isAnswerToOwnRequest,
475
513
  fixQueryParamTypes,
514
+ resolveQueryParams,
476
515
  fixDataTypesFromObject,
477
516
  getItemsFromLookup,
478
517
  getItemFromLookup,
@@ -14,6 +14,8 @@ const eventWhitelist = [
14
14
  "widgets/reset/workingState",
15
15
  "ui/addToSnackbar",
16
16
  "ui/setupProject",
17
+ "ui/bootstrapFailed",
18
+ "ui/syncRouteCompositions",
17
19
  "socket/data/update",
18
20
  "socket/join/room",
19
21
  "socket/leave/room",
@@ -56,7 +56,7 @@ interface FetchClientInterface {
56
56
  post(url: string, data?: any, config?: RequestInit): Promise<Response>
57
57
  put(url: string, data?: any, config?: RequestInit): Promise<Response>
58
58
  patch(url: string, data?: any, config?: RequestInit): Promise<Response>
59
- delete(url: string, config?: RequestInit): Promise<Response>
59
+ delete(url: string, data?: any, config?: RequestInit): Promise<Response>
60
60
  }
61
61
 
62
62
  export type { FetchClientInterface }
@@ -223,9 +223,23 @@ class FetchClient implements FetchClientInterface {
223
223
 
224
224
  async delete(
225
225
  url: string,
226
+ data?: any,
226
227
  config?: RequestInit & { responseType?: string },
227
228
  ): Promise<Response> {
228
- return this.request(url, { ...config, method: "DELETE" })
229
+ const isFormData =
230
+ typeof FormData !== "undefined" && data instanceof FormData
231
+ const body = data
232
+ ? isFormData
233
+ ? data
234
+ : JSON.stringify(data)
235
+ : undefined
236
+ const headers = isFormData
237
+ ? { ...config?.headers }
238
+ : {
239
+ "Content-Type": "application/json",
240
+ ...config?.headers,
241
+ }
242
+ return this.request(url, { ...config, method: "DELETE", body, headers })
229
243
  }
230
244
 
231
245
  // Interceptor management (similar to axios)
@@ -32,6 +32,24 @@ const setServerDisconnected = () => {
32
32
  })
33
33
  }
34
34
 
35
+ const setServerConnecting = () => {
36
+ $store.dispatch({
37
+ type: "backend/socket/status",
38
+ payload: { status: 2 },
39
+ })
40
+ }
41
+
42
+ const setServerConnected = () => {
43
+ $store.dispatch({
44
+ type: "backend/socket/status",
45
+ payload: { status: 1, health: 1 },
46
+ })
47
+ $store.dispatch({
48
+ type: "backend/search/status",
49
+ payload: { status: 1 },
50
+ })
51
+ }
52
+
35
53
  const dataCreate = (isAnswerToOwnRequest, isSystemAction, items) => {
36
54
  items.forEach((item) => {
37
55
  if (isAnswerToOwnRequest) {
@@ -348,5 +366,7 @@ export {
348
366
  userConfigUpdate,
349
367
  newVersionAvailable,
350
368
  setServerDisconnected,
369
+ setServerConnecting,
370
+ setServerConnected,
351
371
  checkToken,
352
372
  }
@@ -0,0 +1,16 @@
1
+ import { validate } from "uuid"
2
+
3
+ /**
4
+ * Extract work-session id from the current URL.
5
+ * Supports racletteJS route format: /:workSession/id={uuid}
6
+ */
7
+ export const extractSessionIdFromUrl = (): string | null => {
8
+ const path = window.location.pathname + (window.location.hash || "")
9
+ const workSessionMatch = path.match(/\/:workSession\/id=([^/&#]+)/)
10
+
11
+ if (workSessionMatch?.[1] && validate(workSessionMatch[1])) {
12
+ return workSessionMatch[1]
13
+ }
14
+
15
+ return null
16
+ }
@@ -26,8 +26,16 @@ import { AUTH_TOKEN_KEY } from "@racletteCore/setup/sessionHandler"
26
26
  import $log from "@racletteCore/lib/logger"
27
27
  import { awaitTime } from "@racletteCore/lib/dataHelper"
28
28
  import type { User } from "@shared/types/core/User.types"
29
- import { checkToken } from "@racletteCore/lib/userNotifier"
29
+ import {
30
+ checkToken,
31
+ setServerConnecting,
32
+ } from "@racletteCore/lib/userNotifier"
30
33
  import configService from "@racletteCore/lib/configService"
34
+ import { isDevelopmentMode } from "@racletteCore/helpers/devMode"
35
+ import {
36
+ setActiveSocket,
37
+ teardownSocket,
38
+ } from "@racletteCore/setup/socketRegistry"
31
39
  import {
32
40
  registerPlugins,
33
41
  registeredPlugins,
@@ -36,6 +44,12 @@ import {
36
44
  const SOCKET_URL = getEnvVariable("RACLETTE_SOCKET_URL")
37
45
  const API_BASE_URL = getEnvVariable("RACLETTE_SERVER_BASE_URL")
38
46
 
47
+ /** First warning while waiting for socket bootstrap to finish. */
48
+ const SETUP_PROJECT_WARN_MS = 5_000
49
+ /** Hard timeout in production (dev mode never rejects — easier backend debugging). */
50
+ const SETUP_PROJECT_TIMEOUT_MS = 60_000
51
+ const SETUP_PROJECT_DEV_WARN_INTERVAL_MS = 10_000
52
+
39
53
  export type CreateAppResponse = {
40
54
  authenticated: boolean
41
55
  setupComplete: boolean
@@ -81,7 +95,6 @@ const createRacletteApp = async (
81
95
  let user: User | null = null
82
96
  await configService.init()
83
97
 
84
- // If sessionId is provided, skip authentication
85
98
  if (options.requireAuth && !options.sessionId) {
86
99
  const { authenticated, error, user: _user, code } = await checkAuthStatus()
87
100
 
@@ -102,20 +115,65 @@ const createRacletteApp = async (
102
115
 
103
116
  store.dispatch({ type: "user/set", payload: user })
104
117
 
105
- // does the core really need to communicate with itself via eventbus?
106
- eventbus.emit("socket/join/room")
118
+ setServerConnecting()
119
+
120
+ return new Promise<CreateAppResponse>((resolve, reject) => {
121
+ let settled = false
107
122
 
108
- return new Promise<CreateAppResponse>(async (resolve, reject) => {
109
- eventbus.on("ui/setupProject", (spaceId: string) => {
123
+ const onSetupProject = (spaceId: string) => {
124
+ if (settled) {
125
+ return
126
+ }
127
+
128
+ settled = true
129
+ eventbus.removeListener("ui/setupProject", onSetupProject)
110
130
  resolve({
111
131
  authenticated: !!user || !!options.sessionId,
112
132
  setupComplete: true,
113
133
  spaceId,
114
134
  })
115
135
  console.log(chadsayFE("racletteJS is up and running!"))
116
- })
117
- await awaitTime(2000)
118
- reject("timeouted")
136
+ }
137
+
138
+ eventbus.on("ui/setupProject", onSetupProject)
139
+
140
+ void (async () => {
141
+ await awaitTime(SETUP_PROJECT_WARN_MS)
142
+ if (settled) {
143
+ return
144
+ }
145
+
146
+ console.warn(
147
+ "[bootstrap] Still waiting for ui/setupProject (socket bootstrap). " +
148
+ "Check server joinConfirmation / projectInit in the network tab.",
149
+ )
150
+
151
+ if (isDevelopmentMode()) {
152
+ while (!settled) {
153
+ await awaitTime(SETUP_PROJECT_DEV_WARN_INTERVAL_MS)
154
+ if (settled) {
155
+ return
156
+ }
157
+ console.warn(
158
+ "[bootstrap] Bootstrap still pending (dev mode — no timeout).",
159
+ )
160
+ }
161
+ return
162
+ }
163
+
164
+ await awaitTime(SETUP_PROJECT_TIMEOUT_MS - SETUP_PROJECT_WARN_MS)
165
+ if (settled) {
166
+ return
167
+ }
168
+
169
+ settled = true
170
+ eventbus.removeListener("ui/setupProject", onSetupProject)
171
+ reject(
172
+ new Error(
173
+ `createRacletteApp timed out after ${SETUP_PROJECT_TIMEOUT_MS / 1000}s waiting for ui/setupProject`,
174
+ ),
175
+ )
176
+ })()
119
177
  })
120
178
  }
121
179
 
@@ -162,8 +220,6 @@ const generateLoginCallback =
162
220
 
163
221
  store.dispatch({ type: "user/set", payload: response.data.user })
164
222
 
165
- eventbus.emit("socket/join/room")
166
-
167
223
  // Return success with token & redirect function
168
224
  return {
169
225
  success: true,
@@ -263,12 +319,23 @@ const setupDependencies = async (sessionId?: string) => {
263
319
  configureHttpInterceptors(store)
264
320
  apiHealthcheck(store)
265
321
 
266
- // socket
267
- socket = createSocket(SOCKET_URL, appSessionId, { sessionId })
322
+ // socket — replace connection; detach old listeners before disconnect to avoid
323
+ // a stale disconnect handler marking the server offline after the new socket connects
324
+ const previousSocket = socket
268
325
 
269
- registerSocketEvents(socket, store, eventbus)
270
- // listen to eventbus and send messages to the socket server
326
+ socket = createSocket(SOCKET_URL, appSessionId, { sessionId })
327
+ setActiveSocket(socket)
328
+ setServerConnecting()
271
329
  registerSocketClient(socket, eventbus)
330
+ registerSocketEvents(socket, store, eventbus)
331
+
332
+ if (previousSocket) {
333
+ teardownSocket(previousSocket)
334
+ }
335
+
336
+ if (socket.connected) {
337
+ eventbus.emit("socket/join/room")
338
+ }
272
339
 
273
340
  // TODO: improve this code
274
341
  corePluginApi = {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Socket project bootstrap
3
+ *
4
+ * Flow:
5
+ * 1. Server emits joinConfirmation → client dispatches projectInit (resets project-scoped store slices)
6
+ * 2. projectInit completes → store emits store_actionResolved_{resolveId}
7
+ * 3. onProjectInitResolved runs setupSpace, syncs route compositions, emits ui/setupProject
8
+ */
9
+ import type { Composition } from "@shared/types/core/Composition.types"
10
+ import type { InteractionLink } from "@shared/types/core/InteractionLink.types"
11
+ import { setupSpace } from "@racletteCore/lib/projectDataHelper"
12
+ import type { ProjectConfiguration } from "../store/types"
13
+ import type EventEmitter from "@racletteCore/lib/eventEmitter"
14
+ import type { RacletteStore } from "@racletteCore"
15
+
16
+ export type ProjectBootstrapPayload = {
17
+ compositions?: Composition[]
18
+ interactionLinks?: InteractionLink[]
19
+ projectConfig?: ProjectConfiguration
20
+ project?: { _id: string; account: string }
21
+ roomId: string
22
+ /** Runs after setupSpace (e.g. work-session fullscreen UI). */
23
+ afterSetup?: () => void | Promise<void>
24
+ }
25
+
26
+ type BootstrapDeps = {
27
+ store: RacletteStore
28
+ eventbus: EventEmitter
29
+ }
30
+
31
+ /**
32
+ * Registers the post-projectInit handler, then resets project-scoped store state.
33
+ */
34
+ export const startProjectBootstrap = (
35
+ { store, eventbus }: BootstrapDeps,
36
+ resolveId: string,
37
+ payload: ProjectBootstrapPayload,
38
+ ) => {
39
+ const {
40
+ compositions,
41
+ interactionLinks,
42
+ projectConfig,
43
+ project,
44
+ roomId,
45
+ afterSetup,
46
+ } = payload
47
+
48
+ eventbus.once(`store_actionResolved_${resolveId}`, async () => {
49
+ if (project) {
50
+ store.dispatch({
51
+ type: "project/set",
52
+ payload: project,
53
+ })
54
+ }
55
+
56
+ store.dispatch({
57
+ type: "ui/update",
58
+ payload: { loading: true },
59
+ })
60
+
61
+ // Same setupSpace as logged-in users (landing page detection, etc.)
62
+ await setupSpace(
63
+ compositions || [],
64
+ interactionLinks || [],
65
+ projectConfig || {},
66
+ )
67
+ // Re-apply route composition slots after projectInit cleared ui.page
68
+ eventbus.emit("ui/syncRouteCompositions")
69
+ await afterSetup?.()
70
+
71
+ store.dispatch({
72
+ type: "ui/update",
73
+ payload: { loading: false },
74
+ })
75
+
76
+ eventbus.emit("ui/setupProject", roomId)
77
+ })
78
+
79
+ store.dispatch({
80
+ type: "projectInit",
81
+ resolveId,
82
+ })
83
+ }