@kumori/aurora-backend-handler 1.1.43 → 1.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # CHANGELOG 1.0.0 (2026-04-28)
2
+
3
+ ## Added (2 change)
4
+
5
+ * More tests to improve coverage.
6
+ * CHANGELOG.md file to follow the new reposiotry structure.
7
+
8
+ ## Changes (1 change)
9
+
10
+ * Updated the DSL library to ["@kumori/kumori-dsl-generator": "1.0.6"]
@@ -1498,7 +1498,6 @@ async function generateServiceSpecDSL(
1498
1498
  } = {
1499
1499
  simple: {},
1500
1500
  };
1501
-
1502
1501
  scaling.simple[form.serviceId] = {
1503
1502
  scale_up: {
1504
1503
  cpu: parseInt(form.scaling?.cpu.up || "") || 0,
@@ -1512,7 +1511,7 @@ async function generateServiceSpecDSL(
1512
1511
  min_replicas: form.scaling?.instances.min || 0,
1513
1512
  max_replicas: form.scaling?.instances.max || 0,
1514
1513
  };
1515
-
1514
+ const autoscalingDefined = form.scaling;
1516
1515
  const hasMarketplacePackage = marketplaceItem?.package;
1517
1516
 
1518
1517
  const serviceSpec: ServiceSpecDSLWithLocalComponent = {
@@ -1615,6 +1614,7 @@ async function generateServiceSpecDSL(
1615
1614
  deploymentMeta: {
1616
1615
  environment: form.environment || [],
1617
1616
  team: "development",
1617
+ scaling: autoscalingDefined ? scaling : {}
1618
1618
  },
1619
1619
  },
1620
1620
  },
package/developer.md ADDED
@@ -0,0 +1,998 @@
1
+ # Aurora BackendHandler — Developer Reference
2
+
3
+ This document is the authoritative developer guide for the `event-handler` library. It is written for a developer with zero prior context on this codebase. After reading it you will understand the architecture, every major module, and exactly how data flows from a UI action to a backend response and back.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [What this library does](#1-what-this-library-does)
10
+ 2. [High-level architecture](#2-high-level-architecture)
11
+ 3. [Entry point — `BackendHandler`](#3-entry-point--backendhandler)
12
+ 4. [Event bus — `EventHelper`](#4-event-bus--eventhelper)
13
+ 5. [WebSocket layer — `websocket-manager.ts`](#5-websocket-layer--websocket-managerts) ← main focus
14
+ 6. [API service modules](#6-api-service-modules)
15
+ 7. [Helper modules](#7-helper-modules)
16
+ 8. [Utilities](#8-utilities)
17
+ 9. [Configuration](#9-configuration)
18
+ 10. [End-to-end data flow examples](#10-end-to-end-data-flow-examples)
19
+ 11. [Adding a new feature — checklist](#11-adding-a-new-feature--checklist)
20
+
21
+ ---
22
+
23
+ ## 1. What this library does
24
+
25
+ The Aurora WUI (web frontend) never calls backend HTTP or WebSocket endpoints directly. Instead it fires **named events** into a global event bus. This library sits between that event bus and the backend, listening for those events, calling the correct API, and publishing the result back onto the bus so the UI can react.
26
+
27
+ ```
28
+ ┌──────────────────────────────────────────────┐
29
+ │ WUI Frontend │
30
+ │ publishes "create-tenant", listens for │
31
+ │ "tenant-created" / "tenant-creation-error" │
32
+ └────────────────────┬─────────────────────────┘
33
+ │ global event bus
34
+ ┌────────────────────▼─────────────────────────┐
35
+ │ event-handler library │
36
+ │ BackendHandler → EventHelper → API Services │
37
+ │ ↕ │
38
+ │ websocket-manager.ts │
39
+ │ (single WebSocket connection to backend) │
40
+ └────────────────────┬─────────────────────────┘
41
+ │ WebSocket / HTTP
42
+ ┌────────────────────▼─────────────────────────┐
43
+ │ Aurora Backend API │
44
+ └──────────────────────────────────────────────┘
45
+ ```
46
+
47
+ Key design decisions:
48
+ - **WebSocket-first** — almost all state mutations go through the WebSocket. HTTP is only used for the initial user load and a handful of multipart (file upload) operations.
49
+ - **Single connection** — one shared `WebSocket` instance for the whole application lifetime.
50
+ - **Request/response correlation** — every outgoing WS message gets a UUID (`messageId`). Incoming responses are matched by that ID to resolve the correct `Promise`.
51
+ - **Event-driven state** — the backend also pushes unsolicited "event" messages when the state of a resource changes (another user creates something, a deployment finishes, etc.). These are handled and merged into the local in-memory caches.
52
+
53
+ ---
54
+
55
+ ## 2. High-level architecture
56
+
57
+ ```
58
+ backend-handler.ts ← instantiated once by the consumer app
59
+
60
+ ├── EventHelper ← wraps the global event bus
61
+ │ └── createEvent ← factory: returns { publish, subscribe, unsubscribe }
62
+
63
+ ├── API Services (api/)
64
+ │ ├── user-api-service.ts
65
+ │ ├── tenant-api-service.ts
66
+ │ ├── account-api-service.ts
67
+ │ ├── environment-api-service.ts
68
+ │ ├── service-api-service.ts
69
+ │ ├── marketplace-api-service.ts
70
+ │ ├── resources-api-service.ts
71
+ │ ├── planProvider-api-service.ts
72
+ │ ├── reporting-api-service.ts
73
+ │ └── organizations-api-service.ts
74
+
75
+ ├── websocket-manager.ts ← owns the WebSocket connection + all in-memory maps
76
+
77
+ ├── helpers/ ← one file per entity domain
78
+ │ ├── user-helper.ts
79
+ │ ├── tenant-helper.ts
80
+ │ ├── service-helper.ts
81
+ │ ├── account-helper.ts
82
+ │ ├── environment-helper.ts
83
+ │ ├── resource-helper.ts
84
+ │ ├── revision-helper.ts
85
+ │ ├── plan-helper.ts
86
+ │ ├── registry-helper.ts
87
+ │ ├── token-helper.ts
88
+ │ ├── link-helper.ts
89
+ │ └── marketplace-helper.ts
90
+
91
+ └── utils/utils.ts ← pure utility functions
92
+ ```
93
+
94
+ **Module responsibility summary**
95
+
96
+ | Module | Responsibility |
97
+ |--------|---------------|
98
+ | `backend-handler.ts` | Wires subscriptions, owns subscription lifecycle, exports `eventHelper` singleton |
99
+ | `event-helper.ts` | Type-safe façade over the raw global event bus |
100
+ | `websocket-manager.ts` | WebSocket connection, pending-request registry, incoming message routing, in-memory entity caches |
101
+ | `api/*.ts` | Translate domain operations into WS or HTTP calls |
102
+ | `helpers/*.ts` | Process raw WS response payloads into domain objects |
103
+ | `utils/utils.ts` | Shared pure functions |
104
+
105
+ ---
106
+
107
+ ## 3. Entry point — `BackendHandler`
108
+
109
+ **File:** [backend-handler.ts](backend-handler.ts)
110
+
111
+ ### Construction
112
+
113
+ ```typescript
114
+ new BackendHandler(route, globalEventHandler, baseUrl, apiVersion)
115
+ ```
116
+
117
+ | Parameter | Type | Description |
118
+ |-----------|------|-------------|
119
+ | `route` | `string` | Current UI route (used to subscribe the right events) |
120
+ | `globalEventHandler` | `any` | Must expose `.publish()`, `.subscribe()`, `.unsubscribe()` |
121
+ | `baseUrl` | `string` | Backend base URL, e.g. `https://api.example.com` |
122
+ | `apiVersion` | `string` | API version string, e.g. `v1` |
123
+
124
+ **What happens in the constructor:**
125
+
126
+ 1. Validates that `globalEventHandler` has a `.subscribe` method — throws if not.
127
+ 2. Creates the `EventHelper` singleton (exported as `eventHelper`).
128
+ 3. Writes `baseUrl` and `apiVersion` into the shared `environment` object so all API services can read them.
129
+ 4. Calls `getUserHTTP()` — an HTTP GET that loads basic user info without a token.
130
+ 5. Inside the `.then()` callback it:
131
+ a. Subscribes to `user.load` → calls `initializeGlobalWebSocketClient` (opens the WS).
132
+ b. Subscribes to `user.loaded` → calls `updateUserComplete` (syncs in-memory state).
133
+ c. Calls `changeRoute(route)` → subscribes all entity event listeners.
134
+ d. Publishes `user.load` to kick off the WS init immediately.
135
+
136
+ ### Route management
137
+
138
+ ```typescript
139
+ handler.changeRoute(newRoute: string): void
140
+ ```
141
+
142
+ Unsubscribes all current listeners, then re-subscribes them. In the current implementation all entity groups are always subscribed (the commented-out route-filtering code shows the intended future behaviour). This is safe because the underlying `createEvent` factory creates fresh `publish/subscribe/unsubscribe` closures each time.
143
+
144
+ ```typescript
145
+ handler.subscribeForRoute(route: string): void
146
+ ```
147
+
148
+ Calls the nine private `subscribe*Events()` methods:
149
+ - `subscribeTenantEvents()`
150
+ - `subscribeUserEvents()`
151
+ - `subscribeAccountEvents()`
152
+ - `subscribePlanEvents()`
153
+ - `subscribeEnvironmentEvents()`
154
+ - `subscribeServiceEvents()`
155
+ - `subscribeMarketplaceEvents()`
156
+ - `subscribeResourceEvents()`
157
+ - `subscribeOrganizationEvents()`
158
+
159
+ Each method registers a callback on the event bus for each action event (e.g. `tenant.creation`, `service.deploy`, …) that calls the matching API service function, and pushes an unsubscribe handle into `this.unsubscribeFunctions` for clean teardown.
160
+
161
+ ```typescript
162
+ handler.isUserLoggedIn(): Promise<boolean>
163
+ ```
164
+
165
+ Quick auth check — calls `isUserLogged()` over HTTP.
166
+
167
+ ### Exported singleton
168
+
169
+ ```typescript
170
+ export let eventHelper: EventHelper;
171
+ ```
172
+
173
+ This is the single `EventHelper` instance used by the whole library. API services and `websocket-manager` import it from `backend-handler.ts` to publish results back to the UI.
174
+
175
+ ---
176
+
177
+ ## 4. Event bus — `EventHelper`
178
+
179
+ **File:** [event-helper.ts](event-helper.ts)
180
+
181
+ `EventHelper` is a typed façade over whatever global event bus the consumer application provides. It does **not** implement pub/sub itself — it delegates to `globalEventHandler.publish / subscribe / unsubscribe`.
182
+
183
+ ### `createEvent<T>(eventName: string)`
184
+
185
+ Private factory method. Returns:
186
+
187
+ ```typescript
188
+ {
189
+ publish: (data: T) => void,
190
+ subscribe: (callback: (payload: T) => void) => void,
191
+ unsubscribe: (callback: (payload: T) => void) => void,
192
+ }
193
+ ```
194
+
195
+ All event names come from the `EventNames` enum in `event-names.ts`, ensuring there are no typos or name clashes.
196
+
197
+ ### Entity groups
198
+
199
+ Each entity domain is exposed as a getter that returns `{ publish, subscribe, unsubscribe }` objects with type-safe methods:
200
+
201
+ | Getter | Entity type | Example events |
202
+ |--------|-------------|---------------|
203
+ | `eventHelper.tenant` | `Tenant` | `creation`, `created`, `creationError`, `update`, `updated`, `delete`, `deleted`, `createRegistry`, `inviteUser`, `createToken`, … |
204
+ | `eventHelper.user` | `UserData` | `creation`, `load`, `loaded`, `update`, `delete`, `authError` |
205
+ | `eventHelper.account` | `Account` | `creation`, `update`, `delete`, `clean` + result variants |
206
+ | `eventHelper.environment` | `Environment` | `creation`, `update`, `delete`, `clean`, `scale` + result variants |
207
+ | `eventHelper.service` | `Service` | `deploy`, `deployed`, `update`, `delete`, `restart`, `changeRevision`, `restartInstance`, `updateServiceLinks`, `requestRevisionData` + error variants |
208
+ | `eventHelper.marketplace` | `MarketplaceItem` | `deployItem`, `itemDeployed`, `loadItems`, `itemsLoaded`, `loadSchema`, `schemaLoaded`, `schemaLoadError` |
209
+ | `eventHelper.resource` | `Resource` | `creation`, `update`, `delete` + result variants |
210
+ | `eventHelper.plan` | `string` | `upgrade`, `upgraded`, `upgradeError`, `downgrade`, `downgraded`, `downgradeError` |
211
+ | `eventHelper.organization` | `Organization` | `creation`, `update`, `delete` + result variants |
212
+ | `eventHelper.notification` | `Notification` | `creation`, `deletion`, `read` |
213
+ | `eventHelper.planProviders` | `PlanProvider[]` | `loadPlans`, `plansLoaded`, `loadError` |
214
+
215
+ **Convention:** every action has three events:
216
+ - `<action>` — intent (UI fires this to trigger an operation)
217
+ - `<action>d` / `<entity>Created` / `<entity>Updated` … — success (library fires this after backend confirms)
218
+ - `<action>Error` — failure (library fires this if backend returns an error)
219
+
220
+ ---
221
+
222
+ ## 5. WebSocket layer — `websocket-manager.ts`
223
+
224
+ **File:** [websocket-manager.ts](websocket-manager.ts)
225
+
226
+ This is the most complex and important file in the library. It owns:
227
+ - The single WebSocket connection
228
+ - The pending-request registry (request/response correlation)
229
+ - All in-memory entity caches (Maps)
230
+ - Routing of incoming server messages to the correct handler
231
+ - Publishing results back through `eventHelper`
232
+
233
+ ### 5.1 Global state variables
234
+
235
+ ```typescript
236
+ let wsConnection: WebSocket | null // the live socket
237
+ let pendingRequests: Map<string, PendingOperation> // messageId → pending promise
238
+ let currentToken: string | null // token used to open current connection
239
+ let connectionPromise: Promise<WebSocket> | null // in-flight connect promise (prevents double-connect)
240
+ let userData: UserData // canonical user state
241
+ let user: User // User domain object rebuilt from userData
242
+
243
+ // Entity caches — keyed by entity ID
244
+ let tenantsMap: Map<string, Tenant>
245
+ let accountsMap: Map<string, Account>
246
+ let environmentsMap: Map<string, Environment>
247
+ let servicesMap: Map<string, Service>
248
+ let revisionsMap: Map<string, Revision>
249
+ let organizationsMap: Map<string, Organization>
250
+ let clusterTokensMap: Map<string, ClusterToken>
251
+ let tokenMap: Map<string, Token>
252
+ let roleMap: Map<string, Role[]>
253
+ let plansMap: Map<string, Plan>
254
+ let secretsMap: Map<string, any>
255
+
256
+ // Platform capability info (loaded once, cached)
257
+ let platformInfo: Platform | null
258
+ let isPlatformInfoLoading: boolean
259
+ let isPlatformInfoLoaded: boolean
260
+
261
+ // Misc deferred/pending state
262
+ let pendingDomains: Resource[]
263
+ let pendingEnvironments: Array<{ tenant: string; env: Environment }>
264
+ let pendingRevisionErrors: Array<{ service: string; revision: Revision }>
265
+ let pendingCloudProviderUpdates: Array<{ environmentId: string; accountId: string }>
266
+ let pendingRegistries: Array<{ tenant: string; registry: Registry }>
267
+ let pendingProjects: Array<{ tenant: string; project: string }>
268
+ let pendingTenantStatus: Map<string, string>
269
+
270
+ // Reporting (usage metrics)
271
+ let isLoadingReporting: boolean
272
+ let hasLoadedReportingOnce: boolean
273
+ const REPORTING_ITERATIONS = 5
274
+ const REPORTING_INTERVAL = 1000 // ms
275
+
276
+ let recentlyUpdatedServices: Map<string, Service>
277
+ ```
278
+
279
+ ### 5.2 Core interfaces
280
+
281
+ #### `WSMessage`
282
+
283
+ Every message sent or received on the WebSocket conforms to this shape:
284
+
285
+ ```typescript
286
+ interface WSMessage {
287
+ messageId: string; // UUID v7 — used to correlate requests to responses
288
+ payload: any; // domain data
289
+ topic: string; // "group:method", e.g. "tenant:create_tenant"
290
+ type: "request" | "multipartrequest" | "success" | "error" | "event";
291
+ error?: {
292
+ code?: string;
293
+ message?: string;
294
+ content?: string;
295
+ };
296
+ }
297
+ ```
298
+
299
+ #### `PendingOperation`
300
+
301
+ Stored in `pendingRequests` while waiting for a server response:
302
+
303
+ ```typescript
304
+ interface PendingOperation {
305
+ resolve: Function; // resolves the Promise returned to the caller
306
+ reject: Function; // rejects the Promise on error/timeout
307
+ action: string; // e.g. "CREATE", "UPDATE", "DELETE", "GET_CHANNELS"
308
+ entityType: string; // e.g. "tenant", "service", "account"
309
+ entityName: string; // entity identifier
310
+ originalData?: any; // the data sent in the request (for error context)
311
+ responsePayload?: any; // set when success response arrives
312
+ }
313
+ ```
314
+
315
+ ### 5.3 Exported functions
316
+
317
+ ---
318
+
319
+ #### `initializeGlobalWebSocketClient`
320
+
321
+ ```typescript
322
+ export const initializeGlobalWebSocketClient = async (
323
+ token: string,
324
+ type?: string,
325
+ name?: string,
326
+ previousData?: UserData,
327
+ ): Promise<WebSocket>
328
+ ```
329
+
330
+ Opens (or reuses) the global WebSocket connection.
331
+
332
+ **Logic:**
333
+
334
+ 1. Copies `previousData.notifications` into local `userData` (preserves notification list across reconnects).
335
+ 2. Stores `name` in `globalEntityName` (used later when processing the first data load).
336
+ 3. **If already connected with the same token** — returns the existing socket immediately. No duplicate connections.
337
+ 4. **If a connection is already in progress** (`connectionPromise` is set) — awaits it. This prevents a race condition when multiple callers try to connect simultaneously.
338
+ 5. **If connected with a different token** — closes the old connection and resets `platformInfo` flags.
339
+ 6. Constructs the WebSocket URL by replacing `http` with `ws` in `baseUrl`: `ws://host/api/<version>/ws`.
340
+ 7. **Browser vs. Node.js** — uses the native `WebSocket` in browsers; dynamically `require('ws')` in Node.js (detected via `typeof window === "undefined"`).
341
+ 8. Sets up three event listeners:
342
+
343
+ **`open`** — sets `currentToken`, clears `connectionPromise`, resolves the outer Promise.
344
+
345
+ **`message`** — the main message dispatcher:
346
+ ```
347
+ incoming message
348
+
349
+ ├── type === "success" or "error"
350
+ │ look up pendingRequests by messageId
351
+ │ → handleOperationSuccess(pending, message) OR
352
+ │ → handleOperationError(pending, message.error)
353
+ │ then delete from pendingRequests
354
+
355
+ └── type === "event"
356
+ → handleEvent(message)
357
+ ```
358
+
359
+ **`close`** — logs the close event, does not auto-reconnect (reconnection is left to the caller).
360
+
361
+ **`error`** — rejects `connectionPromise`, rejects all pending requests, cleans up state.
362
+
363
+ ---
364
+
365
+ #### `makeGlobalWebSocketRequest`
366
+
367
+ ```typescript
368
+ export const makeGlobalWebSocketRequest = async (
369
+ topic: string,
370
+ payload: any,
371
+ timeout?: number, // default: 30 000 ms
372
+ petitionAction?: string, // stored in PendingOperation.action
373
+ petitionInfo?: string, // stored in PendingOperation.entityName
374
+ entityType?: string, // stored in PendingOperation.entityType
375
+ originalData?: any, // stored in PendingOperation.originalData
376
+ ): Promise<any>
377
+ ```
378
+
379
+ Sends a request on the WebSocket and returns a `Promise` that resolves when the server responds with a matching `messageId`.
380
+
381
+ **Mechanics:**
382
+
383
+ 1. Generates a UUID v7 `messageId`.
384
+ 2. Builds a `WSMessage` of type `"request"`.
385
+ 3. Registers a `PendingOperation` in `pendingRequests` keyed by `messageId`.
386
+ 4. Sets a timeout — on expiry, rejects the promise with a timeout error and removes the entry from `pendingRequests`.
387
+ 5. Sends the JSON-serialised message through `wsConnection.send()`.
388
+ 6. When the `"message"` listener receives a `success`/`error` with the matching `messageId`, it calls `resolve`/`reject` and removes the entry.
389
+
390
+ **Topic format:** `"group:method"` — for example:
391
+ - `"tenant:create_tenant"`
392
+ - `"service:delete_service"`
393
+ - `"account:create_account"`
394
+
395
+ ---
396
+
397
+ #### `makeGlobalWebSocketRequestWithRetry`
398
+
399
+ ```typescript
400
+ export const makeGlobalWebSocketRequestWithRetry = async (
401
+ topic: string,
402
+ payload: any,
403
+ maxRetries?: number, // default: 3
404
+ timeout?: number,
405
+ ...same as makeGlobalWebSocketRequest
406
+ ): Promise<any>
407
+ ```
408
+
409
+ Wraps `makeGlobalWebSocketRequest` with **exponential backoff**:
410
+ - Attempt 1: immediate
411
+ - Attempt 2: 1 000 ms delay
412
+ - Attempt 3: 2 000 ms delay
413
+ - Attempt 4: 4 000 ms delay (capped)
414
+
415
+ Used for operations that may transiently fail due to network conditions.
416
+
417
+ ---
418
+
419
+ #### `getWebSocketStatus`
420
+
421
+ ```typescript
422
+ export const getWebSocketStatus = (): {
423
+ connected: boolean;
424
+ readyState?: number;
425
+ pendingRequests: number;
426
+ currentToken: string | null;
427
+ }
428
+ ```
429
+
430
+ Diagnostic snapshot of the connection. Useful for debugging and health checks.
431
+
432
+ ---
433
+
434
+ #### `closeGlobalWebSocketConnection`
435
+
436
+ ```typescript
437
+ export const closeGlobalWebSocketConnection = (): void
438
+ ```
439
+
440
+ Explicitly closes the socket. Rejects all pending requests with `"Connection closed"` before closing.
441
+
442
+ ---
443
+
444
+ #### `updateUserComplete`
445
+
446
+ ```typescript
447
+ export const updateUserComplete = (updatedUserData: UserData): User
448
+ ```
449
+
450
+ Called every time the backend sends a fresh full-user payload (after WS connect, after a reload). Performs:
451
+
452
+ 1. **Token merge** — `updatedUserData.tokens` are merged with any tokens already cached in `userData.tokens` so nothing is lost across partial updates.
453
+ 2. Overwrites `userData` with the new data.
454
+ 3. Rebuilds the `User` domain object.
455
+ 4. Rebuilds tenant/account/environment/service hierarchy via `rebuildHierarchy()`.
456
+ 5. Loads platform capabilities via `loadPlatformInfo()`.
457
+ 6. Starts background reporting via `loadAllEnvironmentsReporting()`.
458
+ 7. Publishes the updated `User` through `eventHelper.user.publish.loaded(user)`.
459
+
460
+ ---
461
+
462
+ #### `fetchAndStoreMarketplaceSchema`
463
+
464
+ ```typescript
465
+ export const fetchAndStoreMarketplaceSchema = async (
466
+ item: MarketplaceItem,
467
+ ): Promise<void>
468
+ ```
469
+
470
+ Fetches the deployment schema for a marketplace item, stores the result in `tenantsMap` (inside the corresponding tenant's marketplace items list), and calls `rebuildHierarchy()` so the UI sees the updated item immediately.
471
+
472
+ ---
473
+
474
+ ### 5.4 Internal routing functions
475
+
476
+ These are not exported but are the core of the event-processing engine.
477
+
478
+ ---
479
+
480
+ #### `handleEvent(message: WSMessage)`
481
+
482
+ Handles messages of `type === "event"` — unsolicited push notifications from the server.
483
+
484
+ **Routing logic:**
485
+
486
+ ```
487
+ message.topic
488
+
489
+ ├── contains "deleted" or key-path ends with entity
490
+ │ → handleDeleteEvent(message)
491
+
492
+ ├── "user:*" → handleUserEvent(userData, message, ...)
493
+ ├── "tenant:*" → handleTenantEvent(tenantsMap, message, ...)
494
+ ├── "service:*" → handleServiceEvent(servicesMap, message, ...)
495
+ ├── "account:*" → handleAccountEvent(accountsMap, message, ...)
496
+ ├── "environment:*" → handleEnvironmentEvent(environmentsMap, message, ...)
497
+ ├── "revision:*" → handleRevisionEvent(revisionsMap, message, ...)
498
+ ├── "domain:*" → handleDomainEvent(...)
499
+ ├── "port:*" → handlePortEvent(...)
500
+ ├── "secret:*" → handleSecretEvent(...)
501
+ ├── "volume:*" → handleVolumeEvent(...)
502
+ ├── "certificate:*" → handleCertificateEvent(...)
503
+ ├── "ca:*" → handleCAEvent(...)
504
+ ├── "plan:*" → handlePlanInstanceEvent(...)
505
+ ├── "registry:*" → handleRegistryEvent(...)
506
+ ├── "token:*" → handleTokenEvent(...)
507
+ └── "link:*" → handleLinkEvent(...)
508
+ ```
509
+
510
+ After every event that modifies entity state, `handleEvent` calls:
511
+ - `rebuildHierarchy()` — re-links all entities into the User hierarchy.
512
+ - `loadPlatformInfo()` — ensures platform capabilities are current (cached; loads only once).
513
+
514
+ ---
515
+
516
+ #### `handleOperationSuccess(operation: PendingOperation, response: WSMessage)`
517
+
518
+ Called when an outgoing request receives a `success` response. Routes by `operation.entityType`:
519
+
520
+ | `entityType` | Handler |
521
+ |--------------|---------|
522
+ | `"tenant"` | `handleTenantOperationSuccess(...)` |
523
+ | `"service"` | `handleServiceOperationSuccess(...)` |
524
+ | `"account"` | `handleAccountOperationSuccess(...)` |
525
+ | `"environment"` | `handleEnvironmentOperationSuccess(...)` |
526
+ | `"resource"` | `processResourceResult(...)` |
527
+ | `"token"` | `handleTokenOperationSuccess(...)` |
528
+ | `"user"` | Calls `updateUserComplete(response.payload)` |
529
+
530
+ Special-cased actions that bypass the entity-type routing:
531
+ - `"GET_CHANNELS"` — parses channel data, maps it onto the service, resolves the promise with channel info.
532
+ - `"GET_REVISION"` — processes revision schema via `processRevisionData(...)`.
533
+ - `"UPDATE_CONFIG"` — triggers a service config refresh.
534
+
535
+ After every success: calls `rebuildHierarchy()` to propagate the change to the full User object.
536
+
537
+ ---
538
+
539
+ #### `handleOperationError(operation: PendingOperation, error: WSMessage["error"])`
540
+
541
+ Called when an outgoing request receives an `error` response. Routes by `operation.entityType` to the matching `handle*OperationError(...)` function in the helpers. Each helper publishes the appropriate error event through `eventHelper` and creates an error `Notification`.
542
+
543
+ ---
544
+
545
+ #### `handleDeleteEvent(message: WSMessage)`
546
+
547
+ Processes deletion events pushed from the server. The message payload contains a hierarchical key path such as `/tenant/mytenant/service/myservice`.
548
+
549
+ 1. Parses the key path via `parseKeyPath()` to extract entity types and names.
550
+ 2. Deletes the entity from the appropriate Map (`tenantsMap`, `servicesMap`, etc.).
551
+ 3. For services: also deletes all related revisions from `revisionsMap`.
552
+ 4. Publishes the appropriate `deleted` event through `eventHelper`.
553
+ 5. Creates a deletion `Notification`.
554
+ 6. Calls `rebuildHierarchy()`.
555
+
556
+ ---
557
+
558
+ ### 5.5 State management helpers
559
+
560
+ #### `rebuildHierarchy()`
561
+
562
+ Reconstructs the full nested object tree from the flat Maps:
563
+
564
+ ```
565
+ tenantsMap
566
+ └── for each Tenant
567
+ └── accountsMap (filter by tenant)
568
+ └── environmentsMap (filter by account)
569
+ └── servicesMap (filter by environment)
570
+ └── revisionsMap (filter by service)
571
+ ```
572
+
573
+ Produces a new `User` object and publishes it via `eventHelper.user.publish.loaded(user)`. This is how the UI always has a consistent view of the full state.
574
+
575
+ Called after every mutation — both from operation results and from push events.
576
+
577
+ ---
578
+
579
+ #### `loadPlatformInfo()`
580
+
581
+ Fetches the platform's capabilities (supported cloud providers, features, limits) from the backend. Results are stored in `platformInfo`. Uses `isPlatformInfoLoading` / `isPlatformInfoLoaded` flags to ensure it is fetched only once per connection, regardless of how many concurrent callers invoke it.
582
+
583
+ ---
584
+
585
+ #### `updateUserWithPlatformInfo()`
586
+
587
+ Merges `platformInfo` into the `User` object and re-publishes. Called after both `rebuildHierarchy()` and `loadPlatformInfo()` complete.
588
+
589
+ ---
590
+
591
+ #### `loadAllEnvironmentsReporting()`
592
+
593
+ Background polling loop. Runs `REPORTING_ITERATIONS` (5) times with `REPORTING_INTERVAL` (1 000 ms) between iterations. For each environment in `environmentsMap`, calls `getReporting(env)` to fetch CPU/memory usage metrics and merges results back into the environment objects. Uses `isLoadingReporting` to prevent overlapping runs.
594
+
595
+ ---
596
+
597
+ #### `resolveServiceStatus(service: Service): string`
598
+
599
+ Determines the effective status of a service by inspecting its current revision state (running, deploying, failed, etc.). Used after revision events to keep service status consistent.
600
+
601
+ ---
602
+
603
+ #### `getChannelsInfo(service: Service, token: string): Promise<Channel[]>`
604
+
605
+ Sends a `GET_CHANNELS` WS request for a service. Returns channel details (protocol, port, host). Called after a service is deployed or updated, so the UI immediately has connectivity info.
606
+
607
+ ---
608
+
609
+ #### `updateEnvironmentsCloudProvider(accountId: string, provider: string)`
610
+
611
+ When a cloud-provider update event arrives for an account, propagates the new provider value to all environments that belong to that account. Then calls `rebuildHierarchy()`.
612
+
613
+ ---
614
+
615
+ #### `safeStringifyError(error: any): string`
616
+
617
+ Pure helper that extracts a readable string from any error value — handles strings, Error objects, objects with `message` / `content` / `description` / `error` fields, and falls back to `JSON.stringify`. Used everywhere an error needs to go into a `Notification`.
618
+
619
+ ---
620
+
621
+ ### 5.6 Message flow diagram
622
+
623
+ ```
624
+ Consumer UI
625
+ │ publishes service.deploy(serviceData)
626
+
627
+ BackendHandler.subscribeServiceEvents()
628
+ │ calls deployService(serviceData, token)
629
+
630
+ service-api-service.ts
631
+ │ calls makeGlobalWebSocketRequest("service:deploy_service", payload, ...)
632
+
633
+ websocket-manager.ts
634
+ │ generates messageId = uuid-v7
635
+ │ stores PendingOperation in pendingRequests
636
+ │ sends JSON over wsConnection
637
+ │ sets timeout timer
638
+
639
+ Aurora Backend
640
+ │ processes request
641
+ │ sends back { messageId, type: "success", payload: {...} }
642
+
643
+ wsConnection "message" event
644
+ │ parses JSON → WSMessage
645
+ │ finds PendingOperation by messageId
646
+ │ deletes entry from pendingRequests
647
+ │ calls handleOperationSuccess(pending, message)
648
+
649
+ handleOperationSuccess
650
+ │ routes to handleServiceOperationSuccess(...)
651
+ │ updates servicesMap
652
+ │ calls rebuildHierarchy() → publishes updated User
653
+ │ calls eventHelper.service.publish.deployed(service)
654
+
655
+ Consumer UI
656
+ receives service.deployed event + updated User
657
+ ```
658
+
659
+ ---
660
+
661
+ ## 6. API service modules
662
+
663
+ These modules translate domain operations into WebSocket or HTTP calls. They are called exclusively by `BackendHandler`'s subscription callbacks.
664
+
665
+ ### `api/user-api-service.ts`
666
+
667
+ | Function | Transport | Description |
668
+ |----------|-----------|-------------|
669
+ | `getUserHTTP()` | HTTP GET | Loads minimal user info to bootstrap the app before WS is open |
670
+ | `loadUser(token, previousData)` | WS | Opens WS connection, then calls `loadUserData()` |
671
+ | `loadUserData()` | WS (many requests) | Fetches the full user graph: tenants, accounts, environments, services, revisions, resources, marketplace items, organizations, platform info — builds `UserData` and calls `updateUserComplete()` |
672
+ | `isUserLogged(token)` | HTTP | Auth check |
673
+ | `createUser(user)` | HTTP POST | Creates new user account |
674
+ | `updateUser(user)` | WS | Updates user profile |
675
+ | `deleteUSer(user, token)` | WS | Deletes user account |
676
+
677
+ `loadUserData()` is the most important function here. It makes a sequence of WS requests to assemble a complete `UserData` object. The order matters — tenants first, then accounts per tenant, then environments per account, etc. This is the "initial hydration" that populates all the Maps in `websocket-manager.ts`.
678
+
679
+ ### `api/tenant-api-service.ts`
680
+
681
+ All operations use `makeGlobalWebSocketRequest` with topic `"tenant:<action>"`.
682
+
683
+ | Function | Action |
684
+ |----------|--------|
685
+ | `createTenant(tenant, security)` | `tenant:create_tenant` |
686
+ | `updateTenant(tenant, security)` | `tenant:update_tenant` |
687
+ | `deleteTenant(tenant, security)` | `tenant:delete_tenant` |
688
+ | `createRegistry(tenant, security, registry)` | `tenant:create_registry` |
689
+ | `updateRegistry(tenant, registry, security, force?)` | `tenant:update_registry` |
690
+ | `deleteRegistry(tenant, registry, security, force?)` | `tenant:delete_registry` |
691
+ | `inviteUser(tenant, email, role, security)` | `tenant:invite_user` |
692
+ | `removeUser(tenant, userId, security)` | `tenant:remove_user` |
693
+ | `updateUserRole(tenant, userId, role, security)` | `tenant:update_user_role` |
694
+ | `acceptInvite(tenant, security)` | `tenant:accept_invite` |
695
+ | `rejectInvite(tenant, security, leave)` | `tenant:reject_invite` |
696
+ | `createToken(tenantName, description, expiration, security)` | `tenant:create_token` |
697
+ | `deleteToken(tenantName, tokenId, security)` | `tenant:delete_token` |
698
+
699
+ HTTP variants (`createTenantHTTP`, `updateTenantHTTP`, `deleteTenantHTTP`) exist for cases where the WS is not yet available.
700
+
701
+ ### `api/account-api-service.ts`
702
+
703
+ Operations use topic `"account:<action>"`. The `createAccount` payload includes provider-specific credentials and resource marks (CPU/memory/storage limits) and IaaS flavors. Supported providers: AWS, Azure, OVH, OASix, CloudOS, OpenNebula.
704
+
705
+ | Function | Description |
706
+ |----------|-------------|
707
+ | `createAccount(account, security)` | Creates cloud account with all provider credentials |
708
+ | `updateAccount(account, security)` | Modifies account config |
709
+ | `deleteAccount(account, security)` | Removes account |
710
+ | `clearAccount(account, security)` | Frees resources without deleting the account |
711
+
712
+ ### `api/environment-api-service.ts`
713
+
714
+ Operations use topic `"environment:<action>"`.
715
+
716
+ | Function | Description |
717
+ |----------|-------------|
718
+ | `createEnvironment(env, security)` | Creates deployment environment (primary or secondary type, HA config) |
719
+ | `updateEnvironment(env, security)` | Modifies environment |
720
+ | `deleteEnvironment(env, security)` | Deletes environment |
721
+ | `clearEnvironment(env, security)` | Clears resources |
722
+ | `scaleEnvironment(env, security)` | Adjusts node/replica counts |
723
+
724
+ ### `api/service-api-service.ts`
725
+
726
+ The most complex API service. Service deployments use **HTTP multipart** (to upload DSL/bundle files); all other operations use WebSocket.
727
+
728
+ | Function | Transport | Description |
729
+ |----------|-----------|-------------|
730
+ | `deployService(data, token)` | HTTP multipart POST | Deploys a service from a DSL or bundle file. Optionally performs a dry-run or validation pass first. Links services post-deployment. |
731
+ | `redeployService(data)` | HTTP POST | Re-deploys from an existing revision |
732
+ | `deleteService(data, security)` | WS | Deletes service + removes its links |
733
+ | `restartService(data, security)` | WS | Restarts a service |
734
+ | `requestRevisionData(service, token)` | WS (`GET_REVISION`) | Fetches revision schema and config |
735
+ | `updateService(data, token, scale?)` | WS | Updates config: parameters, resources, resilience, auto-scaling per role |
736
+ | `updateServiceLinks(link, token)` | WS | Creates or removes inter-service link |
737
+ | `changeRevision(data, token)` | WS | Rolls back to a previous revision |
738
+ | `restartInstance(service, roleId, instanceId, token)` | WS | Restarts a specific service instance |
739
+ | `linkPendingServices(service, token)` | WS | Called automatically after deploy if the service has pending link targets |
740
+ | `unlinkServices(service, token)` | WS | Removes links between services |
741
+
742
+ ### `api/marketplace-api-service.ts`
743
+
744
+ | Function | Transport | Description |
745
+ |----------|-----------|-------------|
746
+ | `deployMarketplaceItem(item)` | HTTP multipart | Deploys a catalog item. Handles complex deployments with public channels, variants (Inbound with HTTP/HTTPS/TCP), parameter mapping, resource binding, scaling rules. |
747
+ | `getMarketplaceItems(tenants, security)` | WS | Fetches available catalog items per tenant. Parses CPU/memory requirements from tags. |
748
+ | `getMarketplaceSchema(tenant, item, security)` | WS | Gets the full deployment schema for an item (parameters, resources, channels, variants, role definitions). |
749
+ | `loadMarketplaceItemsForTenant(tenant, security)` | WS | Variant used during WS-driven initial load. |
750
+
751
+ ### `api/resources-api-service.ts`
752
+
753
+ Handles all resource types: `domain`, `port`, `ca`, `certificate`, `secret`, `volume`. Each `create*` function sends a typed WS payload with resource-specific fields.
754
+
755
+ | Function | Description |
756
+ |----------|-------------|
757
+ | `createResource(tenant, resource, security)` | Routes to the type-specific create function |
758
+ | `createDomain(tenant, domain, security)` | Creates a DNS domain resource |
759
+ | `createPort(tenant, port, security)` | Creates an exposed port resource |
760
+ | `createCA(tenant, ca, security)` | Creates a Certificate Authority |
761
+ | `createCertificate(tenant, cert, security)` | Creates a TLS certificate (requires cert + key + domain) |
762
+ | `createSecret(tenant, secret, security)` | Creates an opaque secret |
763
+ | `createVolume(tenant, volume, security)` | Creates a volume (persistent / nonReplicated / volatile) |
764
+ | `deleteResource(tenant, resource, security)` | Deletes any resource type |
765
+ | `updateResource(tenant, resource, security)` | Updates any resource type (validates type before sending) |
766
+
767
+ ### `api/planProvider-api-service.ts`
768
+
769
+ | Function | Description |
770
+ |----------|-------------|
771
+ | `getPlanProviders(token)` | Fetches available subscription plans. Publishes result via `eventHelper.planProviders.publish.plansLoaded(plans)`. |
772
+
773
+ ### `api/reporting-api-service.ts`
774
+
775
+ | Function | Description |
776
+ |----------|-------------|
777
+ | `getReporting(environment)` | HTTP GET for CPU/memory usage metrics for one environment |
778
+
779
+ ---
780
+
781
+ ## 7. Helper modules
782
+
783
+ Located in [helpers/](helpers/). Each file processes raw WS response payloads for one entity domain. They are called from `websocket-manager.ts` and never called directly by API services or `BackendHandler`.
784
+
785
+ | File | Responsibility |
786
+ |------|---------------|
787
+ | `user-helper.ts` | `handleUserEvent`, `handleUserOperationError` — processes user push events and user operation errors |
788
+ | `tenant-helper.ts` | `handleTenantEvent`, `handleTenantOperationSuccess` — processes tenant push events and maps creation/update results into `tenantsMap` |
789
+ | `service-helper.ts` | `handleServiceEvent`, `handleServiceOperationSuccess`, `handleServiceOperationError`, `mapChannelsFromApiData` — the most complex helper; handles deployment results, status resolution, channel mapping |
790
+ | `account-helper.ts` | `handleAccountEvent`, `handleAccountOperationSuccess`, `handleAccountOperationError`, `syncAccountStatusFromNotifications` |
791
+ | `environment-helper.ts` | `handleEnvironmentEvent`, `handleEnvironmentOperationSuccess`, `handleEnvironmentOperationError` |
792
+ | `resource-helper.ts` | `handleDomainEvent`, `handlePortEvent`, `handleSecretEvent`, `handleVolumeEvent`, `handleCertificateEvent`, `handleCAEvent`, `processResourceResult` — one handler per resource type |
793
+ | `revision-helper.ts` | `handleRevisionEvent`, `processRevisionData` — maps raw revision API data into `Revision` domain objects |
794
+ | `plan-helper.ts` | `handlePlanInstanceEvent`, `updateUserPlansAfterPlanEvent` — handles plan subscription changes |
795
+ | `registry-helper.ts` | `handleRegistryEvent` — handles Docker registry push events |
796
+ | `token-helper.ts` | `handleTokenEvent`, `handleTokenOperationSuccess` — handles API token create/delete operations |
797
+ | `link-helper.ts` | `handleLinkEvent` — updates service link state in `servicesMap` |
798
+ | `marketplace-helper.ts` | `processMarketplaceSchemaResponse` — parses raw marketplace schema API responses into typed structures |
799
+
800
+ **Pattern common to all helpers:**
801
+
802
+ ```typescript
803
+ export function handleXxxOperationSuccess(
804
+ map: Map<string, Xxx>,
805
+ operation: PendingOperation,
806
+ response: WSMessage,
807
+ eventHelper: EventHelper,
808
+ ) {
809
+ // 1. Parse response.payload into a domain object
810
+ // 2. Update the Map
811
+ // 3. Publish the result event: eventHelper.xxx.publish.created(domainObj)
812
+ // 4. Create a Notification: eventHelper.notification.publish.creation(notification)
813
+ }
814
+ ```
815
+
816
+ ---
817
+
818
+ ## 8. Utilities
819
+
820
+ **File:** [utils/utils.ts](utils/utils.ts)
821
+
822
+ | Function | Signature | Description |
823
+ |----------|-----------|-------------|
824
+ | `getTimestamp` | `(val: any) => number` | Parses any timestamp representation (epoch ms, epoch s, ISO string, Date) to milliseconds |
825
+ | `parseKeyPath` | `(key: string) => Record<string, string>` | Converts a hierarchical key path like `/tenant/mytenant/service/myservice` into `{ tenant: "mytenant", service: "myservice" }`. Used in `handleDeleteEvent` to determine which entity was deleted. |
826
+ | `decodeRegistryAuth` | `(base64Secret: string) => { username: string; password: string }` | Decodes a base-64 Docker registry auth secret into credentials |
827
+ | `convertToGigabytes` | `(size: number, unit: string) => number` | Converts storage sizes in any SI/IEC unit (k, M, G, T, P, E, Ki, Mi, Gi, Ti, Pi, Ei) to gigabytes |
828
+
829
+ ---
830
+
831
+ ## 9. Configuration
832
+
833
+ **File:** [environment.ts](environment.ts)
834
+
835
+ ```typescript
836
+ export const environment = {
837
+ apiServer: {
838
+ baseUrl: '', // set by BackendHandler constructor
839
+ apiVersion: '', // set by BackendHandler constructor
840
+ },
841
+ };
842
+ ```
843
+
844
+ This is a module-level singleton. `BackendHandler` writes to it once during construction, and all API services and `websocket-manager.ts` read from it. This is the only configuration surface — there are no `.env` files consumed by this library.
845
+
846
+ **tsconfig.json** — key settings:
847
+ - `target: "ES2020"` — uses native async/await and Map/Set
848
+ - `module: "ESNext"` — ESM output
849
+ - `moduleResolution: "bundler"` — expects a bundler (Vite, webpack, etc.)
850
+ - `strict: true` — full TypeScript strict mode
851
+ - External types come from `@kumori/aurora-interfaces` (not part of this repo)
852
+
853
+ ---
854
+
855
+ ## 10. End-to-end data flow examples
856
+
857
+ ### Example A — Create a Tenant
858
+
859
+ ```
860
+ 1. UI: eventHelper.tenant.publish.creation(tenantData)
861
+
862
+ 2. BackendHandler.subscribeTenantEvents:
863
+ creationCb receives tenantData
864
+ calls createTenant(tenantData, token)
865
+
866
+ 3. tenant-api-service.createTenant:
867
+ calls makeGlobalWebSocketRequest(
868
+ "tenant:create_tenant",
869
+ { name: tenantData.name, plan: tenantData.plan, ... },
870
+ 30000,
871
+ "CREATE",
872
+ tenantData.name,
873
+ "tenant",
874
+ tenantData
875
+ )
876
+
877
+ 4. websocket-manager:
878
+ messageId = uuidv7()
879
+ pendingRequests.set(messageId, {
880
+ resolve, reject, action: "CREATE",
881
+ entityType: "tenant", entityName: tenantData.name
882
+ })
883
+ wsConnection.send(JSON.stringify(message))
884
+ setTimeout(reject, 30000)
885
+
886
+ 5. Backend sends: { messageId, type: "success", payload: { name: "...", ... } }
887
+
888
+ 6. websocket-manager "message" handler:
889
+ pending = pendingRequests.get(messageId)
890
+ pendingRequests.delete(messageId)
891
+ handleOperationSuccess(pending, message)
892
+
893
+ 7. handleOperationSuccess → handleTenantOperationSuccess:
894
+ creates Tenant domain object from payload
895
+ tenantsMap.set(tenant.name, tenant)
896
+ eventHelper.tenant.publish.created(tenant)
897
+ eventHelper.notification.publish.creation({ type: "success", subtype: "tenant-created", ... })
898
+ rebuildHierarchy() → eventHelper.user.publish.loaded(updatedUser)
899
+
900
+ 8. UI receives:
901
+ tenant.created event with new Tenant
902
+ notification.creation event
903
+ user.loaded event with updated User tree
904
+ ```
905
+
906
+ ---
907
+
908
+ ### Example B — Server pushes a deletion event
909
+
910
+ ```
911
+ 1. Backend sends: {
912
+ type: "event",
913
+ topic: "service:deleted",
914
+ payload: { key: "/tenant/acme/account/prod/environment/env1/service/api" }
915
+ }
916
+
917
+ 2. wsConnection "message" handler:
918
+ message.type === "event"
919
+ handleEvent(message)
920
+
921
+ 3. handleEvent → handleDeleteEvent(message):
922
+ parseKeyPath("/tenant/acme/account/prod/environment/env1/service/api")
923
+ → { tenant: "acme", account: "prod", environment: "env1", service: "api" }
924
+ servicesMap.delete("api")
925
+ revisionsMap: delete all entries belonging to "api"
926
+ eventHelper.service.publish.deleted({ name: "api", tenant: "acme" })
927
+ eventHelper.notification.publish.creation({ type: "success", subtype: "deployment-deleted", ... })
928
+ rebuildHierarchy()
929
+ ```
930
+
931
+ ---
932
+
933
+ ### Example C — Initial app load
934
+
935
+ ```
936
+ 1. new BackendHandler(route, globalEventHandler, baseUrl, apiVersion)
937
+ → getUserHTTP() [HTTP GET /user]
938
+
939
+ 2. HTTP response arrives:
940
+ → eventHelper.user.publish.load({} as UserData)
941
+
942
+ 3. user.load subscriber (backend-handler.ts):
943
+ → loadUser(token, data)
944
+ → initializeGlobalWebSocketClient(token) [opens WS]
945
+ → loadUserData()
946
+
947
+ 4. loadUserData() sends sequential WS requests:
948
+ user:get_user
949
+ tenant:get_tenants
950
+ for each tenant:
951
+ account:get_accounts
952
+ for each account:
953
+ environment:get_environments
954
+ for each environment:
955
+ service:get_services
956
+ ...
957
+ marketplace:get_items
958
+ organization:get_organizations
959
+ platform:get_info
960
+
961
+ 5. After all requests complete:
962
+ assembles UserData
963
+ calls updateUserComplete(userData)
964
+
965
+ 6. updateUserComplete:
966
+ merges tokens
967
+ populates all Maps (tenantsMap, accountsMap, ...)
968
+ rebuildHierarchy()
969
+ loadPlatformInfo()
970
+ loadAllEnvironmentsReporting()
971
+ eventHelper.user.publish.loaded(user)
972
+
973
+ 7. UI receives user.loaded with the full populated User object
974
+ ```
975
+
976
+ ---
977
+
978
+ ## 11. Adding a new feature — checklist
979
+
980
+ When adding support for a new entity type or operation, follow these steps:
981
+
982
+ 1. **Add event names** to [event-names.ts](event-names.ts) — add an entry to the `EventNames` enum for each event (`CreateFoo`, `FooCreated`, `FooCreationError`, …).
983
+
984
+ 2. **Extend EventHelper** in [event-helper.ts](event-helper.ts) — add a new getter that exposes `publish`, `subscribe`, and `unsubscribe` objects using the new event names.
985
+
986
+ 3. **Create an API service** in `api/foo-api-service.ts` — implement functions that call `makeGlobalWebSocketRequest` with the correct topic and payload shape.
987
+
988
+ 4. **Create a helper** in `helpers/foo-helper.ts` — implement `handleFooOperationSuccess`, `handleFooOperationError`, and `handleFooEvent`. These functions update the relevant Map and publish events.
989
+
990
+ 5. **Extend `websocket-manager.ts`** — add a new Map for the entity, import and call the new helper from `handleOperationSuccess`, `handleOperationError`, and `handleEvent`. Update `rebuildHierarchy()` if the new entity participates in the hierarchy.
991
+
992
+ 6. **Extend `BackendHandler`** in [backend-handler.ts](backend-handler.ts) — add a `subscribeFooEvents()` method that wires the UI action events to the API service functions. Call it from `subscribeForRoute()`.
993
+
994
+ 7. **Add to `@kumori/aurora-interfaces`** if you need new domain types — that external package defines `User`, `Tenant`, `Service`, etc.
995
+
996
+ ---
997
+
998
+ *Last updated: 2026-05-08*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kumori/aurora-backend-handler",
3
- "version": "1.1.43",
3
+ "version": "1.1.45",
4
4
  "description": "backend handler",
5
5
  "main": "backend-handler.ts",
6
6
  "scripts": {