goblin-laboratory 4.10.0 → 4.11.1

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/README.md CHANGED
@@ -1,107 +1,585 @@
1
- # Laboratory
1
+ # 📘 goblin-laboratory
2
2
 
3
- > Bienvenue dans le labo des gobelins!
3
+ ## Aperçu
4
4
 
5
- Le labo offre un espace de travail facilitant la construction d'application UI.
5
+ `goblin-laboratory` est le module central de l'écosystème Xcraft pour la construction d'interfaces utilisateur React. Il orchestre la création de fenêtres applicatives (via Electron ou WebSocket), gère le cycle de vie des widgets connectés au store Redux, assure la synchronisation des états backend vers le frontend, et fournit la classe de base `Widget` dont dérivent tous les composants graphiques Xcraft.
6
6
 
7
- Il instancie pour vous une fenêtre electron et se connecte au contenu du laboratoire
8
- en ouvrant le bundle webpack à une URL donnée.
7
+ Le laboratoire est l'acteur Goblin qui fait le lien entre le monde backend (bus Xcraft, acteurs Goblin/Elf) et le monde frontend (React, Redux). Il gère également le thème visuel, le zoom de l'interface, et expose un terminal intégré (`Termux`) pour l'administration.
9
8
 
10
- Il optimise la communication avec les gobelins en s'occupant de souscrire aux state des différents gobelins dans l'entrepôt de states (voir. gobelin-warehouse).
9
+ ## Sommaire
11
10
 
12
- En créant un labo, vous optenez un espace de travail utilisateur prêt à l'emploi.
11
+ - [Aperçu](#aperçu)
12
+ - [Structure du module](#structure-du-module)
13
+ - [Fonctionnement global](#fonctionnement-global)
14
+ - [Exemples d'utilisation](#exemples-dutilisation)
15
+ - [Interactions avec d'autres modules](#interactions-avec-dautres-modules)
16
+ - [Configuration avancée](#configuration-avancée)
17
+ - [Détails des sources](#détails-des-sources)
18
+ - [Licence](#licence)
13
19
 
14
- Cet espace est pré-découpé en zone de montages stratégiques:
20
+ ## Structure du module
15
21
 
16
- - root
17
- - top-bar
18
- - task-bar
19
- - content
22
+ Le module s'articule autour de plusieurs couches :
20
23
 
21
- Par défaut, un laboratoire va monter un gestionnaire de travaux utilisateurs qui permet un routage des tâches utilisateurs dans la UI. Ce comportement par défaut peut-être remplacé en explicitant le mapping montage/vue à la création du laboratoire.
24
+ **Acteurs backend (Goblin/Elf) :**
22
25
 
23
- ## Widget
26
+ - `laboratory` — acteur Goblin principal, gère la fenêtre et le cycle de vie de l'interface
27
+ - `carnotzet` — acteur Goblin léger pour les interfaces sans fenêtre native (mode WebSocket pur)
28
+ - `Termux` — acteur Elf singleton, terminal de commandes intégré à l'interface
29
+ - `Blueprint` / `Blueprints` — acteurs Elf pour la persistance de métadonnées d'entités
24
30
 
25
- Un labo est automatiquement cablé pour écouter les changements d'états des widgets dans le state.
31
+ **Infrastructure frontend :**
26
32
 
27
- Un composant react `Widget` de base permet d'écrire des composant connecté pour vos gobelins.
33
+ - `Renderer` et ses variantes (`ElectronRenderer`, `BrowsersRenderer`, `ElectronRendererWS`) bootstrapping React
34
+ - `Widget` — classe de base pour tous les composants graphiques Xcraft
35
+ - Redux store avec ses reducers (`backend`, `widgets`, `commands`, `network`, `app`, `router`)
36
+ - Middlewares Redux (`transit`, `quest`, `form`) pour la communication bidirectionnelle
28
37
 
29
- ### Formulaires
38
+ **Widgets utilitaires :**
30
39
 
31
- Si votre widget doit se comporter en formulaire de saisies, il faut exposer un getter spécifique:
40
+ - `ThemeContext` injection du thème CSS calculé dynamiquement
41
+ - `WithModel`, `WithC`, `C` — connexion déclarative des props au state Redux
42
+ - `StateLoader`, `CollectionLoader` — chargement conditionnel selon l'état backend
43
+ - `ErrorHandler` — boundary d'erreurs React avec récupération
44
+ - `DisconnectOverlay`, `Maintenance` — états de connexion et maintenance
45
+ - `Frame`, `Root` — racine de l'arbre React
32
46
 
33
- ```js
34
- get isForm () {
35
- return true;
47
+ ## Fonctionnement global
48
+
49
+ ### Flux de données backend → frontend
50
+
51
+ ```
52
+ Backend (Goblin/Elf)
53
+
54
+
55
+ Warehouse (état)
56
+
57
+
58
+ Channel (ElectronChannel / WebSocketChannel)
59
+ │ envoie NEW_BACKEND_STATE / PUSH_PATH / DISPATCH_IN_APP
60
+
61
+ Renderer (frontend)
62
+
63
+
64
+ transitMiddleware → backendReducer → Redux Store
65
+
66
+
67
+ Widget.connect() → React re-render
68
+ ```
69
+
70
+ Le backend sérialise les états Xcraft en transit JSON (via `xcraft-core-transport`) et les envoie au frontend. Le `transitMiddleware` désérialise et gère les générations pour détecter les pertes de paquets. Le `backendReducer` applique les patches ou remplace l'état complet.
71
+
72
+ ### Flux de données frontend → backend
73
+
74
+ ```
75
+ Widget React (interaction utilisateur)
76
+ │ cmd() / doFor()
77
+
78
+ questMiddleware → send('QUEST', action)
79
+
80
+
81
+ IPC Electron / WebSocket
82
+
83
+
84
+ Bus Xcraft → quête Goblin
85
+ ```
86
+
87
+ Les actions utilisateur déclenchent des quêtes backend via le mécanisme `QUEST`. Le middleware sérialise l'action et l'envoie via IPC ou WebSocket selon le transport utilisé.
88
+
89
+ ### Compensation optimiste
90
+
91
+ Le `backend-reducer` implémente un mécanisme de compensation : lors d'une modification de champ (`FIELD-CHANGED`), la valeur est mise à jour immédiatement dans le store frontend avant confirmation backend. Les compensateurs sont débounés à 300ms.
92
+
93
+ ### Gestion des générations
94
+
95
+ Chaque état backend porte un numéro de génération. Si des générations sont perdues (réseau instable), le frontend demande un renvoi complet (`RESEND`). En mode patch (`_xcraftPatch`), seules les différences sont transmises.
96
+
97
+ ## Exemples d'utilisation
98
+
99
+ ### Créer un laboratoire (acteur Goblin)
100
+
101
+ ```javascript
102
+ // Depuis un acteur Goblin
103
+ await quest.create('laboratory', {
104
+ id: 'laboratory@myapp',
105
+ desktopId: 'desktop@user1',
106
+ clientSessionId: 'client-session@abc',
107
+ url: 'http://localhost:3000',
108
+ config: {
109
+ feeds: ['workshop'],
110
+ themeContexts: ['theme'],
111
+ useWS: false,
112
+ title: 'Mon Application',
113
+ },
114
+ });
115
+ ```
116
+
117
+ ### Créer un carnotzet (mode WebSocket)
118
+
119
+ ```javascript
120
+ await quest.create('carnotzet', {
121
+ id: 'carnotzet@session1',
122
+ clientSessionId: 'client-session@abc',
123
+ config: {
124
+ feed: 'desktop@user1',
125
+ feeds: ['workshop'],
126
+ theme: 'default',
127
+ themeContexts: ['theme'],
128
+ },
129
+ });
130
+ ```
131
+
132
+ ### Créer un widget connecté
133
+
134
+ ```javascript
135
+ import Widget from 'goblin-laboratory/widgets/widget';
136
+
137
+ class MyWidget extends Widget {
138
+ render() {
139
+ const {name, age} = this.props;
140
+ return (
141
+ <div>
142
+ {name} ({age})
143
+ </div>
144
+ );
36
145
  }
146
+ }
147
+
148
+ export default Widget.connect((state, props) => ({
149
+ name: state.get(`backend.${props.id}.name`),
150
+ age: state.get(`backend.${props.id}.age`),
151
+ }))(MyWidget);
152
+ ```
153
+
154
+ ### Utiliser les props connectées avec `C` et `withC`
155
+
156
+ ```javascript
157
+ import C from 'goblin-laboratory/widgets/connect-helpers/c';
158
+ import withC from 'goblin-laboratory/widgets/connect-helpers/with-c';
159
+
160
+ // Connecter un champ texte à l'état backend
161
+ const TextField = withC(TextFieldNC, {value: 'onChange'});
162
+
163
+ // Utilisation dans un render
164
+ <TextField value={C('.name')} />
165
+
166
+ // Avec transformation
167
+ <TextField value={C('.age', age => String(age), str => Number(str))} />
168
+
169
+ // Spread sur plusieurs props
170
+ <Label {...C('.person', ({firstname, lastname}) => ({
171
+ text: `${firstname} ${lastname}`,
172
+ }))} />
173
+ ```
174
+
175
+ ### Utiliser `WithModel` pour contextualiser les chemins
176
+
177
+ ```javascript
178
+ import WithModel from 'goblin-laboratory/widgets/with-model/widget';
179
+
180
+ // Tous les C('.field') dans les enfants seront relatifs à backend.person@123
181
+ <WithModel model="backend.person@123">
182
+ <TextField value={C('.name')} />
183
+ <TextField value={C('.email')} />
184
+ </WithModel>;
185
+ ```
186
+
187
+ ### Utiliser l'acteur Termux depuis un autre Elf
188
+
189
+ ```javascript
190
+ const termux = new Termux(this);
191
+ await termux.init();
192
+ // Le terminal est maintenant accessible via Alt+F12 dans l'UI
193
+ ```
194
+
195
+ ### Créer un Blueprint (métadonnée d'entité persistée)
196
+
197
+ ```javascript
198
+ const blueprint = new Blueprint(this);
199
+ await blueprint.create('blueprint@case', desktopId);
200
+ await blueprint.change('fields.status.label', 'Nouveau statut');
37
201
  ```
38
202
 
39
- ### Wiring (cablâge des propriétés)
203
+ ## Interactions avec d'autres modules
204
+
205
+ - **[xcraft-core-goblin]** : Fournit les mécanismes de base Goblin (quêtes, Shredder, dispatch, Elf)
206
+ - **[xcraft-core-transport]** : Sérialisation/désérialisation des états en transit JSON
207
+ - **[xcraft-core-stones]** : Typage des shapes d'acteurs Elf (`string`, `boolean`, `array`, etc.)
208
+ - **[xcraft-core-utils]** : Utilitaires divers dont `parseOptions` pour le parsing de commandes
209
+ - **[xcraft-core-probe]** : Instrumentation des performances IPC/WebSocket
210
+ - **[xcraft-core-log]** : Journalisation des avertissements et erreurs internes
211
+ - **[goblin-theme]** : Fournit les builders de thème consommés par `ThemeContext`
212
+
213
+ ## Configuration avancée
214
+
215
+ | Option | Description | Type | Valeur par défaut |
216
+ | ------------- | ------------------------------------------------- | -------- | ----------------- |
217
+ | `defaultZoom` | Facteur de zoom initial pour le frontend Electron | `number` | `1.0` |
218
+
219
+ ### Variables d'environnement
220
+
221
+ | Variable | Description | Exemple | Valeur par défaut |
222
+ | ------------------ | -------------------------------------------------------- | ------------- | ----------------- |
223
+ | `GOBLINS_DEVTOOLS` | Active les DevTools Electron à l'ouverture de la fenêtre | `1` | non défini |
224
+ | `NODE_ENV` | Active Redux DevTools en mode développement | `development` | non défini |
225
+
226
+ ## Détails des sources
227
+
228
+ ### `lib/carnotzet.js`
229
+
230
+ Acteur Goblin léger destiné aux interfaces sans fenêtre native (mode WebSocket/navigateur). Il joue le même rôle de coordinateur que `laboratory` mais sans gestion de fenêtre Electron.
231
+
232
+ #### Cycle de vie
233
+
234
+ À la création (`create`), le carnotzet :
235
+
236
+ 1. Valide que `config.feed` est présent
237
+ 2. Crée un acteur `theme-composer` pour chaque contexte de thème
238
+ 3. Souscrit au warehouse pour les feeds configurés
239
+ 4. S'abonne à `goblin.released` pour nettoyer les widgets libérés
240
+
241
+ À la destruction (`delete`), il se désabonne du warehouse et libère sa branche.
242
+
243
+ #### Méthodes publiques (quêtes)
244
+
245
+ - **`create(clientSessionId, config)`** — Initialise le carnotzet avec la configuration du feed et des thèmes.
246
+ - **`get-feed()`** — Retourne l'identifiant du feed courant.
247
+ - **`set-root(widget, widgetId)`** — Définit le widget racine à afficher.
248
+ - **`del(widgetId)`** — Supprime un widget du feed (désabonnement warehouse).
249
+ - **`change-theme(name)`** — Change le thème actif.
250
+ - **`when-ui-crash(desktopId, error, info)`** — Journalise les erreurs de rendu React.
251
+
252
+ ### `widgets/laboratory/service.js`
253
+
254
+ Acteur Goblin principal du module. Il orchestre la création complète d'une fenêtre applicative Electron, y compris la gestion du zoom, du thème, des feeds warehouse, et du terminal Termux.
255
+
256
+ #### Cycle de vie
257
+
258
+ 1. **`create`** : instancie le Termux, crée les `theme-composer`, crée le `wm`, initialise zoom et thème depuis la session client, abonne les listeners de fermeture de fenêtre et de reload de thème.
259
+ 2. **`listen`** : active les abonnements aux événements de navigation, changement de thème et dispatch provenant du desktop.
260
+ 3. **`close`** : ferme la fenêtre via `client-session` et `client`, puis libère l'acteur.
261
+ 4. **`delete`** : désactive les listeners (`unlisten`).
262
+
263
+ #### Méthodes publiques (quêtes)
264
+
265
+ - **`create(desktopId, clientSessionId, userId, url, config)`** — Crée la fenêtre et l'infrastructure complète.
266
+ - **`close()`** — Ferme proprement la fenêtre et libère l'acteur.
267
+ - **`listen(desktopId, userId, useConfigurator)`** — Active l'écoute des événements du desktop.
268
+ - **`set-root(widget, widgetId, themeContext)`** — Définit le widget racine affiché.
269
+ - **`set-feed(desktopId)`** — Change le feed souscrit (greffe les branches dans le warehouse).
270
+ - **`change-theme(name)`** — Change le thème et persiste la préférence.
271
+ - **`zoom()` / `un-zoom()` / `default-zoom()` / `change-zoom(zoom)`** — Contrôle du zoom avec persistance.
272
+ - **`dispatch(action)`** — Envoie une action Redux directement au frontend.
273
+ - **`nav(route)`** — Navigation vers une route.
274
+ - **`duplicate(forId)`** — Duplique le laboratoire dans une nouvelle fenêtre.
275
+ - **`del(widgetId)`** — Supprime un widget du feed ou ferme l'application selon le contexte.
276
+
277
+ ### `lib/termux.js` et `termux.js`
278
+
279
+ #### Rôle
280
+
281
+ Le Termux est un terminal de commandes intégré à l'interface, accessible via `Alt+F12`. Il permet d'exécuter des commandes du bus Xcraft directement depuis l'UI, avec autocomplétion, historique de navigation, et gestion des signaux (`SIGINT`).
282
+
283
+ L'acteur `Termux` est un `Elf.Alone` (singleton). La logique de mutation d'état est dans `TermuxLogic` (un `Elf.Spirit`).
284
+
285
+ #### État et modèle de données
286
+
287
+ L'état est décrit par `TermuxShape` :
288
+
289
+ | Champ | Type | Description |
290
+ | -------------- | ---------------- | ------------------------------------------------------ |
291
+ | `id` | `string` | Identifiant de l'acteur (`"termux"`) |
292
+ | `prompt` | `string` | Invite de commande courante (`"~ $"` ou `"~ #"`) |
293
+ | `busy` | `boolean` | Indique si une commande est en cours d'exécution |
294
+ | `history` | `array(string)` | Historique des entrées et sorties affichées |
295
+ | `completion` | `string` | Suggestion d'autocomplétion courante |
296
+ | `value` | `string` | Valeur courante de la ligne de saisie |
297
+ | `toolName` | `option(string)` | Nom de l'outil en cours d'exécution (pour SIGINT) |
298
+ | `inputCommand` | `boolean` | Indique si le terminal attend une saisie interactive |
299
+ | `cmd` | `option(string)` | Commande en attente d'input interactif |
300
+ | `args` | `option(object)` | Arguments de la commande en attente d'input interactif |
301
+
302
+ #### Cycle de vie
303
+
304
+ - **`init()`** — Initialise le prompt selon le rang de l'utilisateur (`admin` → `~ #`, sinon `~ $`), charge les outils disponibles depuis le registre de commandes, s'abonne aux événements `<termux-input>` et `<termux-output>`, écoute les changements de registre de commandes.
305
+ - **`dispose()`** — Désabonne le listener du registre de commandes.
306
+
307
+ #### Méthodes publiques
308
+
309
+ - **`init()`** — Initialise le terminal (idempotent).
310
+ - **`beginCommand(command)`** — Parse et exécute une commande, met à jour l'historique.
311
+ - **`endCommand(result)`** — Termine une commande et affiche le résultat.
312
+ - **`inputCommand(input)`** — Traite une saisie interactive (mode `forInputCommand`).
313
+ - **`forInputCommand(question, cmd, args)`** — Met le terminal en attente d'une saisie utilisateur.
314
+ - **`forOutputCommand(value)`** — Ajoute une sortie dans l'historique.
315
+ - **`askForCompletion(input)`** — Calcule et affiche les suggestions d'autocomplétion.
316
+ - **`setFromHistory(up, input)`** — Navigation dans l'historique des commandes (haut/bas).
317
+ - **`clearCompletion()`** — Efface la suggestion courante.
318
+ - **`signal(signal)`** — Envoie un signal (`SIGINT`) à la commande en cours.
319
+
320
+ #### Outils intégrés (quêtes `$tool`)
321
+
322
+ Les outils sont des commandes spéciales enregistrées avec le suffixe `$tool`. Ils sont listés et exécutables directement depuis le terminal Termux.
323
+
324
+ - **`clear$tool()`** — Vide l'historique du terminal.
325
+ - **`man$tool(name)`** — Affiche la documentation d'une commande (module, emplacement, usage, paramètres).
326
+ - **`buslog$tool(horde, verbosityLevel, ...moduleNames)`** — Configure le niveau de verbosité des logs du bus (admin uniquement pour les hordes passives).
327
+ - **`metrics$tool(horde, output?)`** — Récupère les métriques du bus Xcraft au format JSON, avec export optionnel vers un fichier.
328
+ - **`heapdump$tool(horde)`** — Déclenche un heap dump sur le processus cible.
329
+ - **`malloctrim$tool(horde)`** — Libère la mémoire non utilisée via `malloc_trim`.
330
+
331
+ ### `lib/index.js`
332
+
333
+ Fournit les classes `ElectronChannel` et `WebSocketChannel` utilisées par le backend pour envoyer des messages au frontend.
334
+
335
+ - **`ElectronChannel(win)`** — Utilise `win.webContents.send` pour la communication intra-processus Electron.
336
+ - **`WebSocketChannel(win)`** — Utilise une connexion WebSocket pour les clients distants ou les navigateurs.
337
+
338
+ Les deux canaux exposent : `sendBackendState(msg)`, `sendPushPath(path)`, `sendAction(action)`, `beginRender(labId, tokens)`.
339
+
340
+ ### `lib/blueprints/blueprint.js`
341
+
342
+ Acteur Elf persisté (`Elf.Archetype`) représentant un blueprint d'entité. Les blueprints décrivent la structure d'une entité (champs, références, collections, configuration UI) sous forme de métadonnées persistées dans la base `blueprints`.
343
+
344
+ #### État et modèle de données
345
+
346
+ L'état est décrit par `BlueprintShape` :
347
+
348
+ | Champ | Type | Description |
349
+ | ------------- | ----------------------------------------- | -------------------------------------- |
350
+ | `id` | `string` | Identifiant du blueprint |
351
+ | `entity` | `string` | Nom de l'entité décrite (ex: `"case"`) |
352
+ | `fields` | `record(string, FieldShape)` | Dictionnaire des champs de l'entité |
353
+ | `references` | `option(record(string, ReferenceShape))` | Pointeurs vers d'autres entités |
354
+ | `collections` | `option(record(string, CollectionShape))` | Collections de pointeurs |
355
+ | `ui` | `option(UiConfigShape)` | Configuration UI globale de l'entité |
356
+
357
+ Les shapes imbriquées clés :
358
+
359
+ - **`FieldShape`** : `type` (text, enum, date…), `label`, `required`, `readonly`, `hidden`, `values` (pour les enums), `ui` (hints de filtrage, tri, recherche)
360
+ - **`ReferenceShape`** : `entity` (cible), `label`, `lookup` (représentation dans l'UI avec `labelPaths`, `iconPath`, `drilldown`)
361
+ - **`UiConfigShape`** : `icon`, `primaryLabel`, `secondaryLabel`, `defaultSort`
362
+
363
+ #### Méthodes publiques
364
+
365
+ - **`create(id, desktopId)`** — Crée et persiste le blueprint.
366
+ - **`change(path, newValue)`** — Met à jour un champ du blueprint et persiste.
367
+ - **`delete()`** — Destructeur (no-op actuellement).
368
+
369
+ ### `lib/blueprints/blueprints.js`
370
+
371
+ Acteur Elf singleton (`Elf.Alone`) qui charge l'ensemble des blueprints persistés au démarrage.
372
+
373
+ #### Méthodes publiques
374
+
375
+ - **`loadAll(desktopId)`** — Lit tous les IDs depuis la base `blueprints` via `cryo.reader` et monte chaque `Blueprint` dans la session desktop.
376
+
377
+ ### `widgets/widget/index.js`
378
+
379
+ La classe `Widget` est la brique fondamentale de tous les composants graphiques Xcraft. Elle étend `React.Component` et fournit :
380
+
381
+ **Connexion au store :**
382
+
383
+ - **`Widget.connect(mapStateToProps)`** — HOC de connexion Redux avec égalité Shredder optimisée.
384
+ - **`Widget.connectBackend(mapStateToProps)`** — Connexion automatique sur `backend.${props.id}`, affiche `null` si l'état n'est pas chargé.
385
+ - **`Widget.connectWidget(mapStateToProps)`** — Connexion sur `widgets.${props.id}`.
386
+ - **`Widget.Wired(Component)`** — Connecte automatiquement selon la définition `static get wiring()`.
387
+
388
+ **Communication avec le backend :**
389
+
390
+ - **`cmd(cmd, args)`** — Envoie une quête au bus Xcraft (vérifie les droits via le registre).
391
+ - **`do(action, args)`** — Appelle une quête sur le service correspondant au nom du widget.
392
+ - **`doFor(serviceId, action, args)`** — Appelle une quête sur un service spécifique par ID.
393
+ - **`doDispatch(model, name, args)`** — Route vers `doFor` (backend) ou `dispatchTo` (widgets) selon le modèle.
394
+ - **`canDo(cmd)`** — Vérifie si une commande est autorisée pour l'utilisateur courant.
395
+
396
+ **Dispatch Redux :**
397
+
398
+ - **`dispatch(action, name?)`** — Dispatch dans le reducer frontend du widget courant.
399
+ - **`dispatchTo(id, action, name?)`** — Dispatch dans le reducer d'un widget cible.
400
+ - **`dispatchToCache(id, payload)`** — Stocke une valeur dans le cache du desktop (persisté entre montages).
401
+ - **`rawDispatch(action)`** — Dispatch direct dans le store Redux.
402
+
403
+ **Accès à l'état :**
404
+
405
+ - **`getState(path?)`** — Retourne l'état complet du store ou une valeur à un chemin.
406
+ - **`getBackendState(path?)`** — Retourne l'état backend de ce widget ou d'un ID spécifié.
407
+ - **`getWidgetState(path?)`** — Retourne l'état frontend du widget.
408
+ - **`getWidgetCacheState(widgetId)`** — Retourne la valeur du cache desktop pour un widget.
409
+
410
+ **Styles :**
411
+ La propriété `styles` est calculée via Aphrodite à partir d'un fichier `styles.js` companion. Le système fusionne les définitions de styles héritées et met en cache les résultats (LRU 2048 entrées).
412
+
413
+ **Navigation :**
414
+
415
+ - **`nav(route, frontOnly?)`** — Navigation via le router (frontend seul ou via le laboratoire).
416
+
417
+ **Utilitaires :**
418
+
419
+ - **`setBackendValue(path, value)`** — Modifie directement une valeur dans le state backend (compensation).
420
+ - **`reportError(error, info)`** — Remonte une erreur React au laboratoire.
421
+ - **`Widget.copyTextToClipboard(text)`** — Copie du texte dans le presse-papiers.
422
+ - **`Widget.getUserSession(state)`** — Retourne la session utilisateur courante depuis le state.
423
+ - **`Widget.getLoginSession(state)`** — Retourne la session de login courante.
424
+ - **`Widget.getSchema(state, path?)`** — Retourne le schéma depuis `workshop.schema`.
425
+
426
+ ### `widgets/renderer.js`, `widgets/index-electron.js`, `widgets/index-browsers.js`, `widgets/index-electron-ws.js`
427
+
428
+ Ces fichiers constituent le bootstrap du frontend React selon le mode de rendu :
429
+
430
+ - **`ElectronRenderer`** (`index-electron.js`) — Utilise `ipcRenderer` pour communiquer avec le main process Electron. Récupère `wid` et `labId` depuis les paramètres d'URL.
431
+ - **`BrowsersRenderer`** (`index-browsers.js`) — Utilise WebSocket avec reconnexion exponentielle (125ms → doublement), gestion des tokens de session via `localStorage`/`sessionStorage`. Supporte le cas `Epsitec.Cresus.Shell` avec token via cookie.
432
+ - **`ElectronRendererWS`** (`index-electron-ws.js`) — Variante Electron utilisant WebSocket (pour les fenêtres secondaires ou le mode hybride). Récupère le port WebSocket via le paramètre `wss=` dans l'URL.
433
+
434
+ Tous héritent de `Renderer` (`renderer.js`) qui initialise le store Redux, l'historique de navigation et les handlers de drag & drop. Les messages JSON entrants sont parsés via un Web Worker dédié pour éviter de bloquer le thread principal.
435
+
436
+ ### `widgets/store/`
437
+
438
+ Le store Redux est composé de plusieurs reducers combinés :
439
+
440
+ - **`backend-reducer`** — Gère l'état backend reçu du serveur. Supporte les patches différentiels (`_xcraftPatch`), les compensations optimistes et la mise à jour directe de champs (`FIELD-CHANGED`).
441
+ - **`widgets-reducer`** — Gère les états locaux frontend des widgets (découverte dynamique des reducers par namespace). Supporte `WIDGETS_COLLECT` pour nettoyer les états orphelins.
442
+ - **`commands-reducer`** — Maintient le registre des commandes disponibles sur le bus (`COMMANDS_REGISTRY`).
443
+ - **`network-reducer`** — Suit l'état de connexion des hordes (lag, overlay, message de déconnexion) via `CONNECTION_STATUS`.
444
+ - **`app-reducer`** — Délègue aux reducers d'application spécifiques (`app-reducer` de chaque module goblin, discriminé par `action._appName`).
445
+ - **`router-reducer`** — Gère la navigation (compatible `connected-react-router`).
446
+
447
+ Les middlewares configurés :
448
+
449
+ - **`transitMiddleware`** — Désérialise les états de transit, gère les générations, déclenche le resend en cas de perte, injecte les compensateurs.
450
+ - **`questMiddleware`** — Sérialise et envoie les actions `QUEST` au backend via le canal de communication.
451
+ - **`formMiddleware`** — Intercepte `FIELD-CHANGED` et les actions de formulaire (`rrf/change`) pour déclencher automatiquement les quêtes de mise à jour backend (`{goblin}.change` ou `{goblin}.change-{field}`) avec debounce 200ms pour les hinters.
452
+
453
+ ### `widgets/connect-helpers/`
454
+
455
+ Ensemble d'utilitaires pour la connexion déclarative des props :
456
+
457
+ - **`C(path, inFunc?, outFunc?)`** — Crée une `ConnectedProp` liant une prop à un chemin dans le state. Supporte les tableaux de chemins pour passer plusieurs valeurs à `inFunc`.
458
+ - **`withC(Component, dispatchProps?, options?)`** — HOC qui donne à un composant la capacité de recevoir des `ConnectedProp`. Gère les chemins relatifs/absolus (via `ModelContext`), les transformations et les dispatches retour. L'option `modelProp` permet de définir le contexte de modèle à partir d'une prop connectée.
459
+ - **`joinModels(baseModel, nextModel)`** — Résout les chemins relatifs (préfixés par `.`) par rapport à un modèle de base.
460
+
461
+ ### `widgets/theme-context/widget.js`
462
+
463
+ Composant `ThemeContext` qui injecte le thème calculé dynamiquement dans l'arbre React. Il :
464
+
465
+ 1. Importe le contexte de thème (builders) via l'importer
466
+ 2. Appelle les builders (`paletteBuilder`, `shapesBuilder`, `stylesBuilder`, etc.) pour construire le thème complet
467
+ 3. Injecte les styles globaux CSS et les polices via des balises `<style>`
468
+ 4. Force le re-rendu de tous les enfants via la prop `key` (basée sur `cacheName`) lors d'un changement de thème
469
+
470
+ Le `cacheName` combine le nom du thème et sa génération (incrémentée à chaque `reload-theme`) pour invalider le cache Aphrodite.
471
+
472
+ ### `widgets/termux/widget.js`
473
+
474
+ Widget React connecté qui affiche le terminal Termux. Activé par `Alt+F12`, il s'affiche en superposition semi-transparente sur l'interface. Il gère :
475
+
476
+ - La saisie clavier avec raccourcis bash (`Ctrl+A`/`Ctrl+E` curseur, `Ctrl+U` effacer ligne, `Ctrl+W` effacer mot, `Tab` autocomplétion, flèches historique)
477
+ - L'historique défilant avec rendu inversé (le plus récent en bas)
478
+ - La transmission des signaux (`Ctrl+C` → `SIGINT`)
479
+ - L'autocomplétion via Tab
480
+
481
+ ### `widgets/disconnect-overlay/widget.js`
482
+
483
+ Overlay plein écran affiché lorsque la connexion au backend est perdue. Affiche une icône réseau clignotante (animation 1.2s) et un message d'état. Le fond noir semi-transparent avec `backdrop-filter: blur` est positionné en `fixed` avec `z-index` configurable (défaut 20).
484
+
485
+ **Props :** `message` (string), `zIndex` (number, défaut 20), `children` (le contenu rendu derrière l'overlay).
486
+
487
+ ### `widgets/maintenance/widget.js`
488
+
489
+ Overlay plein écran affiché pendant une opération de maintenance. Affiche une icône de verrouillage, une barre de progression et un message. Utilise le wiring automatique (`Widget.Wired`) pour lire `maintenance.status`, `maintenance.progress` et `maintenance.message`.
490
+
491
+ **Wiring :** `{id: 'id', status: 'maintenance.status', progress: 'maintenance.progress', message: 'maintenance.message'}`.
492
+
493
+ ### `widgets/error-handler/widget.js`
494
+
495
+ Error boundary React (`getDerivedStateFromError`) qui capture les erreurs de rendu des composants enfants. Affiche une icône d'avertissement orange cliquable pour relancer le rendu (`setState({error: null})`).
496
+
497
+ **Props :** `big` (boolean, agrandit l'icône à 400%), `renderError` (function, personnalise le rendu d'erreur), `children`.
498
+
499
+ ### `widgets/state-loader/widget.js`
500
+
501
+ Composant utilitaire qui attend qu'un état backend soit chargé avant de rendre ses enfants. Accepte `path` (relatif à `backend.`, vérifie l'existence de `.id`) ou `fullPath` (chemin absolu). Affiche optionnellement un `FallbackComponent` pendant le chargement.
502
+
503
+ ### `widgets/collection-loader/widget.js`
504
+
505
+ Variante de `StateLoader` pour les collections : attend que tous les IDs d'une liste (`props.ids`) soient présents dans le backend avant de rendre les enfants. Si les enfants sont une fonction, elle leur passe la collection mappée comme argument.
506
+
507
+ ### `widgets/with-model/widget.js`
508
+
509
+ Composant fournisseur de contexte de modèle. Définit `context.model` pour tous les enfants, permettant aux props `C('.relativePath')` de se résoudre correctement. Compatible avec l'API Context React moderne (`ModelContext`) et l'ancienne API legacy (`childContextTypes`).
510
+
511
+ ### `widgets/with-desktop-id/widget.js` et `widgets/with-readonly/widget.js`
512
+
513
+ Fournisseurs de contexte pour `desktopId` et `readonly`, accessibles via hooks (`useDesktopId()`, `useReadonly()`) ou via le contexte legacy (`childContextTypes`). Exposent respectivement `DesktopIdContext` et `ReadonlyContext`.
514
+
515
+ ### `widgets/with-workitem/widget.js`
40
516
 
41
- Un widget est dit cablé lorsqu'on fournit un `id` à celui-ci lors de son utilisation.
517
+ Fournisseur de contexte qui expose `id`, `entityId` et `dragServiceId` aux descendants, tout en wrappant les enfants dans `WithReadonly`.
42
518
 
43
- Dans ce cas, les propriétés cablée respecterons l'état du widget dans le state.
519
+ ### `widgets/with-route/with-route.js`
44
520
 
45
- Un widget cablé doit être créé par un goblin, et ces propriétés cablée doivent être définie via son API.
46
- Certaine propriétés du widget ne sont pas cablée sur le state est peuvent être définie au rendu.
521
+ HOC de connexion au router Redux. Permet de connecter un composant à la route courante avec surveillance de paramètres (`watchedParams`), query strings (`watchedSearchs`) et hash (`watchHash`). Expose `isDisplayed` (booléen) indiquant si la route courante correspond. Optimisé avec `shallowEqualShredder`.
47
522
 
48
- Si le widget n'est pas cablé par id, il peut-être utilisé librement en définissant ces propriétés au rendu.
523
+ ### `widgets/frame/widget.js`
49
524
 
50
- ### Styling
525
+ Composant racine qui fournit le store Redux, le `labId`, `dispatch` et le thème aux composants enfants via le contexte React legacy. Utilisé pour encapsuler des sous-arbres React dans un contexte Xcraft complet (ex : fenêtres secondaires).
51
526
 
52
- Tout widget peut-être accompagné d'un fichier de style permettant de calculer le style du widget à l'aide du theme courant.
527
+ ### `widgets/root/index.js`
53
528
 
54
- ## List
529
+ Composant racine monté par `Renderer`. Fournit le store Redux via `<Provider>` et optionnellement le router via `<ConnectedRouter>`. Instancie le widget `Laboratory` correspondant au `labId`, avec ou sans routing selon `props.useRouter`.
55
530
 
56
- Un widget spécial permettant l'affichage de liste très longues de manière efficace.
531
+ ### `widgets/importer/`
57
532
 
58
- ## specs goblin-laboratory
533
+ Système de découverte dynamique des widgets via `require.context` Webpack. Permet d'importer n'importe quel type de ressource widget (`widget`, `styles`, `reducer`, `theme-context`, `app-reducer`, `compensator`, etc.) par namespace. Un importer personnalisé peut être fourni par `mainGoblinModule` (via `lib/.webpack-config.js`).
59
534
 
60
- - routage et composition de vues inter-app
61
- - pilotage du goblin wm
62
- - persistance des paramètres liés a l'espace de travail inter-gadgets
63
- - composant d'auto-layout
64
- - persistance de settings de fenêtre inter-gadgets (écoute events wm)
65
- - lazy loading des gagdgets https://webpack.js.org/guides/lazy-load-react/
535
+ ### `lib/.webpack-config.js`
66
536
 
67
- ### Exemple de quest
537
+ Génère la configuration des alias Webpack pour le bundle frontend. Résout les aliases pour `t` (traductions Nabu), `nabu`, `goblin_importer` (avec support d'un importer personnalisé depuis `mainGoblinModule`) et `goblin_theme_fa` (FontAwesome Pro ou Free selon disponibilité du package `@fortawesome/fontawesome-pro`).
68
538
 
69
- crée des gadgets avec leurs states:
539
+ ### `lib/helpers.js`
70
540
 
71
- `laboratory.create goblin-passport /passport [passport, passport_nabu]`
541
+ Utilitaire de parsing d'URL : `getParameter(search, name)` extrait un paramètre d'une query string (décode les caractères URL-encodés).
72
542
 
73
- `laboratory.create goblin-sync /syncui [syncui, syncui_nabu, passport]`
543
+ ### `widgets/frontend-form/`
74
544
 
75
- lister l'état des gadgets:
545
+ Composant `FrontendForm` qui crée un contexte `WithModel` sur `widgets.${widgetId}`. Le `reducer.js` associé gère les actions `INIT` (initialisation de l'état si vide) et `CHANGE` (mise à jour d'un chemin) pour un état de formulaire purement frontend.
76
546
 
77
- `laboratory.list`
547
+ ### `widgets/store/middlewares.js`
78
548
 
79
- ouvrir la vue principale de passport dans une nouvelle fenetres:
549
+ Détails des middlewares Redux :
80
550
 
81
- `laboratory.open /passport`
551
+ **`questMiddleware`** — Intercepte les actions de type `QUEST` et les envoie au backend via le canal de communication. Gère les states de compensation.
82
552
 
83
- passe de la vue de passport a la vue principale de SyncUI
84
- dans la même fenetre:
553
+ **`formMiddleware`** — Intercepte `FIELD-CHANGED` et les actions de formulaire (`rrf/change`, `rrf/batch`, `hinter/search`) pour déclencher automatiquement les quêtes de mise à jour backend correspondantes (`{goblin}.change` ou `{goblin}.change-{field}`). Utilise un debounce de 200ms pour les recherches hinter.
85
554
 
86
- `laboratory.switch /passport /syncui`
555
+ **`transitMiddleware`** — Désérialise les états backend (`NEW_BACKEND_STATE`), vérifie la continuité des générations et injecte les compensateurs. Déclenche `RESEND` si des générations sont perdues.
87
556
 
88
- ouvrir la vue principale de passport et de syncui dans une nouvelle fenetres:
557
+ ### `widgets/widget/style/`
89
558
 
90
- `laboratory.open /passport /syncui`
559
+ Pipeline de style Aphrodite optimisé :
91
560
 
92
- crée et ouvre nabu pour syncui dans une nouvelle fenetre:
561
+ - `build-style.js` — Point d'entrée, orchestre le calcul et met en cache via `StylesCache`. Étend Aphrodite avec un handler de sélecteur personnalisé supportant le sélecteur `&` (pour les styles imbriqués).
562
+ - `styles-cache.js` — Cache LRU (2048 entrées max) avec liste doublement chaînée pour l'éviction. Les entrées les moins récemment utilisées migrent vers le début de la liste.
563
+ - `compute-style-hash.js` — Hash basé sur `theme.cacheName` (si le style dépend du thème) et la sérialisation stable des props de style (via `safe-stable-stringify`).
564
+ - `get-style-props.js` — Extrait uniquement les props déclarées dans `propNames` (et applique `mapProps` si défini) pour le calcul de style.
565
+ - `merge-style-definitions.js` — Fusionne les définitions de style de la hiérarchie d'héritage du widget en une seule définition combinée.
93
566
 
94
- `laboratory.create goblin-nabu /nabu_sync [syncui_nabu] open:true`
567
+ ### `widgets/searchkit/index.js`
95
568
 
96
- fermer une fenetre:
569
+ Intégration expérimentale avec Elasticsearch via SearchKit. Permet d'afficher un champ de recherche full-text et les résultats depuis un index Elasticsearch local (`http://localhost:9200`). Ce composant est considéré comme expérimental.
97
570
 
98
- `laboratory.close /nabu_sync`
571
+ ## Licence
99
572
 
100
- naviguer dans une vue:
573
+ Ce module est distribué sous [licence MIT](./LICENSE).
101
574
 
102
- `laboratory.navigate /syncui/someview`
575
+ ---
103
576
 
104
- cacher/afficher une fenêtre:
577
+ [xcraft-core-goblin]: https://github.com/Xcraft-Inc/xcraft-core-goblin
578
+ [xcraft-core-transport]: https://github.com/Xcraft-Inc/xcraft-core-transport
579
+ [xcraft-core-stones]: https://github.com/Xcraft-Inc/xcraft-core-stones
580
+ [xcraft-core-utils]: https://github.com/Xcraft-Inc/xcraft-core-utils
581
+ [xcraft-core-probe]: https://github.com/Xcraft-Inc/xcraft-core-probe
582
+ [xcraft-core-log]: https://github.com/Xcraft-Inc/xcraft-core-log
583
+ [goblin-theme]: https://github.com/Xcraft-Inc/goblin-theme
105
584
 
106
- `laboratory.hide /syncui`
107
- `laboratory.show /syncui`
585
+ _Ce contenu a été généré par IA_
package/config.js CHANGED
@@ -3,11 +3,4 @@
3
3
  /**
4
4
  * Retrieve the inquirer definition for xcraft-core-etc
5
5
  */
6
- module.exports = [
7
- {
8
- type: 'input',
9
- name: 'defaultZoom',
10
- message: 'Set default zoom for Electron frontend',
11
- default: 1.0,
12
- },
13
- ];
6
+ module.exports = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-laboratory",
3
- "version": "4.10.0",
3
+ "version": "4.11.1",
4
4
  "description": "Laboratory",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -48,7 +48,7 @@
48
48
  "prop-types": "^15.5.10",
49
49
  "react": "^17.0.1",
50
50
  "react-dom": "^17.0.1",
51
- "react-redux": "^7.1.0",
51
+ "react-redux": "^8.1.3",
52
52
  "react-router": "^5.0.0",
53
53
  "redux-thunk": "^2.3.0",
54
54
  "safe-stable-stringify": "^2.5.0",
@@ -58,4 +58,4 @@
58
58
  "xcraft-traverse": "^0.7.0"
59
59
  },
60
60
  "prettier": "xcraft-dev-prettier"
61
- }
61
+ }
@@ -0,0 +1,17 @@
1
+ import C, {ConnectedProp} from './c.js';
2
+
3
+ export default function mapC(value, inFunc, outFunc) {
4
+ if (value instanceof ConnectedProp) {
5
+ return C(
6
+ value.path,
7
+ value.inFunc ? (...args) => inFunc(value.inFunc(...args)) : inFunc,
8
+ value.outFunc
9
+ ? value.outFunc.length > 1 || outFunc.length > 1
10
+ ? (newValue, ...oldValues) =>
11
+ value.outFunc(outFunc(newValue, ...oldValues), ...oldValues)
12
+ : (newValue) => value.outFunc(outFunc(newValue))
13
+ : outFunc
14
+ );
15
+ }
16
+ return inFunc(value);
17
+ }
@@ -225,8 +225,21 @@ export default function withC(Component, dispatchProps = {}, {modelProp} = {}) {
225
225
  const dispatchPropName = dispatchProps[name];
226
226
  const outFunc = prop.outFunc;
227
227
  if (outFunc) {
228
- onChangeProps[dispatchPropName] = (value) =>
229
- this.handlePropChange(name, outFunc(value));
228
+ if (outFunc.length > 1) {
229
+ let currentValues;
230
+ if (Array.isArray(prop.fullPath)) {
231
+ currentValues = prop.fullPath.map((path) =>
232
+ this.getState(path)
233
+ );
234
+ } else {
235
+ currentValues = [this.getState(prop.fullPath)];
236
+ }
237
+ onChangeProps[dispatchPropName] = (value) =>
238
+ this.handlePropChange(name, outFunc(value, ...currentValues));
239
+ } else {
240
+ onChangeProps[dispatchPropName] = (value) =>
241
+ this.handlePropChange(name, outFunc(value));
242
+ }
230
243
  } else {
231
244
  onChangeProps[dispatchPropName] = (value) =>
232
245
  this.handlePropChange(name, value);
@@ -85,14 +85,13 @@ const logicHandlers = {
85
85
  };
86
86
 
87
87
  // Register quest's according rc.json
88
- Goblin.registerQuest(goblinName, 'create', function* (
88
+ Goblin.registerQuest(goblinName, 'create', async function (
89
89
  quest,
90
90
  desktopId,
91
91
  clientSessionId,
92
92
  userId,
93
93
  url,
94
- config,
95
- next
94
+ config
96
95
  ) {
97
96
  quest.goblin.setX('url', url);
98
97
  quest.goblin.setX('desktopId', desktopId);
@@ -106,7 +105,7 @@ Goblin.registerQuest(goblinName, 'create', function* (
106
105
  config.feeds.push('termux');
107
106
 
108
107
  const termux = new Termux(quest);
109
- yield termux.init();
108
+ await termux.init();
110
109
 
111
110
  const promises = [];
112
111
  for (const ctx of themeContexts) {
@@ -119,13 +118,13 @@ Goblin.registerQuest(goblinName, 'create', function* (
119
118
  );
120
119
  config.feeds.push(composerId);
121
120
  }
122
- yield Promise.all(promises);
121
+ await Promise.all(promises);
123
122
 
124
123
  const id = quest.goblin.id;
125
124
 
126
125
  quest.goblin.defer(
127
- quest.sub('goblin.released', function* (err, {msg, resp}) {
128
- yield resp.cmd('laboratory.del', {
126
+ quest.sub('goblin.released', async (err, {msg, resp}) => {
127
+ await resp.cmd('laboratory.del', {
129
128
  id,
130
129
  widgetId: msg.data.id,
131
130
  });
@@ -135,8 +134,8 @@ Goblin.registerQuest(goblinName, 'create', function* (
135
134
  quest.goblin.defer(
136
135
  quest.sub(
137
136
  `*::theme-composer@*.${clientSessionId}.reload-theme.requested`,
138
- function* (err, {msg, resp}) {
139
- yield resp.cmd('laboratory.reload-theme', {...msg.data, id});
137
+ async (err, {msg, resp}) => {
138
+ await resp.cmd('laboratory.reload-theme', {...msg.data, id});
140
139
  }
141
140
  )
142
141
  );
@@ -164,19 +163,16 @@ Goblin.registerQuest(goblinName, 'create', function* (
164
163
  })
165
164
  );
166
165
 
167
- quest.doSync(
168
- {id: quest.goblin.id, feed, wid: winId, url, config},
169
- next.parallel()
170
- );
166
+ await quest.doSync({id: quest.goblin.id, feed, wid: winId, url, config});
171
167
 
172
- yield quest.me.initZoom({clientSessionId});
173
- yield quest.me.initTheme({clientSessionId});
168
+ await quest.me.initZoom({clientSessionId});
169
+ await quest.me.initTheme({clientSessionId});
174
170
 
175
171
  quest.goblin.defer(
176
172
  quest.sub.local(
177
173
  `*::${winId}.${clientSessionId}.<window-closed>`,
178
- function* (err, {msg, resp}) {
179
- yield resp.cmd('laboratory.close', {id});
174
+ async (err, {msg, resp}) => {
175
+ await resp.cmd('laboratory.close', {id});
180
176
  }
181
177
  )
182
178
  );
@@ -184,8 +180,8 @@ Goblin.registerQuest(goblinName, 'create', function* (
184
180
  quest.goblin.defer(
185
181
  quest.sub.local(
186
182
  `*::${winId}.${clientSessionId}.<window-state-changed>`,
187
- function* (err, {msg, resp}) {
188
- yield resp.cmd('laboratory.save-window-state', {
183
+ async (err, {msg, resp}) => {
184
+ await resp.cmd('laboratory.save-window-state', {
189
185
  id,
190
186
  winId,
191
187
  state: msg.data.state,
@@ -193,7 +189,7 @@ Goblin.registerQuest(goblinName, 'create', function* (
193
189
  }
194
190
  )
195
191
  );
196
- yield quest.create('wm', {
192
+ await quest.create('wm', {
197
193
  id: winId,
198
194
  desktopId,
199
195
  url,
@@ -211,24 +207,19 @@ Goblin.registerQuest(goblinName, 'create', function* (
211
207
  },
212
208
  });
213
209
 
214
- /*const titlebarInfos = yield win.getTitlebar();
215
- if (titlebarInfos) {
216
- const {titlebar, titlebarId} = titlebarInfos;
217
- yield quest.me.setTitlebar({titlebar, titlebarId});
218
- }*/
219
210
  quest.log.info(() => `Laboratory ${quest.goblin.id} created!`);
220
211
  return quest.goblin.id;
221
212
  });
222
213
 
223
- Goblin.registerQuest(goblinName, 'close', function* (quest) {
214
+ Goblin.registerQuest(goblinName, 'close', async function (quest) {
224
215
  const clientSessionId = quest.goblin.getX('clientSessionId');
225
216
  const labId = quest.goblin.id;
226
217
 
227
- yield quest.cmd('client-session.close-window', {
218
+ await quest.cmd('client-session.close-window', {
228
219
  id: clientSessionId,
229
220
  winId: `wm@${labId}`,
230
221
  });
231
- yield quest.cmd('client.close-window', {labId});
222
+ await quest.cmd('client.close-window', {labId});
232
223
  quest.release(labId);
233
224
  });
234
225
 
@@ -244,17 +235,17 @@ Goblin.registerQuest(goblinName, 'get-win-feed', function (quest) {
244
235
  };
245
236
  });
246
237
 
247
- Goblin.registerQuest(goblinName, 'set-feed', function* (quest, desktopId) {
238
+ Goblin.registerQuest(goblinName, 'set-feed', async function (quest, desktopId) {
248
239
  quest.goblin.setX('desktopId', desktopId);
249
240
  const feeds = quest.goblin.getState().get('feeds');
250
241
  const wm = quest.getAPI(`wm@${quest.goblin.id}`);
251
- yield wm.feedSub({desktopId, feeds: feeds.valueSeq().toArray()});
242
+ await wm.feedSub({desktopId, feeds: feeds.valueSeq().toArray()});
252
243
 
253
244
  const labId = quest.goblin.id;
254
245
  const clientSessionId = quest.goblin.getState().get('clientSessionId');
255
246
  const fromFeed = quest.goblin.getState().get('feed');
256
247
  for (const branch of [labId, clientSessionId].filter((id) => !!id)) {
257
- yield quest.warehouse.graft({
248
+ await quest.warehouse.graft({
258
249
  branch,
259
250
  fromFeed,
260
251
  toFeed: desktopId,
@@ -262,7 +253,7 @@ Goblin.registerQuest(goblinName, 'set-feed', function* (quest, desktopId) {
262
253
  }
263
254
 
264
255
  quest.do();
265
- yield quest.warehouse.resend({feed: desktopId});
256
+ await quest.warehouse.resend({feed: desktopId});
266
257
  });
267
258
 
268
259
  Goblin.registerQuest(goblinName, 'set-titlebar', function (
@@ -281,11 +272,11 @@ Goblin.registerQuest(goblinName, 'get-url', function (quest) {
281
272
  return quest.goblin.getX('url');
282
273
  });
283
274
 
284
- Goblin.registerQuest(goblinName, 'duplicate', function* (quest, forId) {
275
+ Goblin.registerQuest(goblinName, 'duplicate', async function (quest, forId) {
285
276
  const state = quest.goblin.getState();
286
277
  const url = state.get('url');
287
278
  const newLabId = `laboratory@${quest.uuidV4()}`;
288
- const lab = yield quest.createFor(forId, forId, newLabId, {
279
+ const lab = await quest.createFor(forId, forId, newLabId, {
289
280
  id: newLabId,
290
281
  url,
291
282
  });
@@ -324,7 +315,7 @@ Goblin.registerQuest(goblinName, 'set-root', function (
324
315
  quest.do();
325
316
  });
326
317
 
327
- Goblin.registerQuest(goblinName, 'listen', function* (
318
+ Goblin.registerQuest(goblinName, 'listen', async function (
328
319
  quest,
329
320
  desktopId,
330
321
  userId,
@@ -338,14 +329,14 @@ Goblin.registerQuest(goblinName, 'listen', function* (
338
329
 
339
330
  if (userId) {
340
331
  const wmAPI = quest.getAPI(`wm@${quest.goblin.id}`);
341
- yield wmAPI.setUserId({userId});
332
+ await wmAPI.setUserId({userId});
342
333
  }
343
334
 
344
335
  const labId = quest.goblin.id;
345
336
  quest.goblin.setX(
346
337
  `nav-unsub`,
347
- quest.sub(`*::<${desktopId}>.nav.requested`, function* (err, {msg, resp}) {
348
- yield resp.cmd('laboratory.nav', {
338
+ quest.sub(`*::<${desktopId}>.nav.requested`, async (err, {msg, resp}) => {
339
+ await resp.cmd('laboratory.nav', {
349
340
  id: labId,
350
341
  desktopId,
351
342
  ...msg.data,
@@ -355,28 +346,28 @@ Goblin.registerQuest(goblinName, 'listen', function* (
355
346
 
356
347
  quest.goblin.setX(
357
348
  `change-theme-unsub`,
358
- quest.sub(`*::<${desktopId}>.change-theme.requested`, function* (
359
- err,
360
- {msg, resp}
361
- ) {
362
- yield resp.cmd('laboratory.change-theme', {
363
- id: labId,
364
- ...msg.data,
365
- });
366
- })
349
+ quest.sub(
350
+ `*::<${desktopId}>.change-theme.requested`,
351
+ async (err, {msg, resp}) => {
352
+ await resp.cmd('laboratory.change-theme', {
353
+ id: labId,
354
+ ...msg.data,
355
+ });
356
+ }
357
+ )
367
358
  );
368
359
 
369
360
  quest.goblin.setX(
370
361
  `dispatch-unsub`,
371
- quest.sub(`*::<${desktopId}>.dispatch.requested`, function* (
372
- err,
373
- {msg, resp}
374
- ) {
375
- yield resp.cmd('laboratory.dispatch', {
376
- id: labId,
377
- ...msg.data,
378
- });
379
- })
362
+ quest.sub(
363
+ `*::<${desktopId}>.dispatch.requested`,
364
+ async (err, {msg, resp}) => {
365
+ await resp.cmd('laboratory.dispatch', {
366
+ id: labId,
367
+ ...msg.data,
368
+ });
369
+ }
370
+ )
380
371
  );
381
372
  });
382
373
 
@@ -391,29 +382,32 @@ function unlisten(quest) {
391
382
  }
392
383
  }
393
384
 
394
- Goblin.registerQuest(goblinName, 'nav', function* (quest, route) {
385
+ Goblin.registerQuest(goblinName, 'nav', async function (quest, route) {
395
386
  const win = quest.getAPI(`wm@${quest.goblin.id}`);
396
- yield win.nav({route});
387
+ await win.nav({route});
397
388
  });
398
389
 
399
390
  /************************ SETTINGS *********************************/
400
391
 
401
- Goblin.registerQuest(goblinName, 'save-settings', function* (quest, propertie) {
392
+ Goblin.registerQuest(goblinName, 'save-settings', async function (
393
+ quest,
394
+ propertie
395
+ ) {
402
396
  const value = quest.goblin.getState().get(propertie);
403
397
  const clientSessionId = quest.goblin.getX('clientSessionId');
404
- yield quest.cmd(`client-session.set-${propertie}`, {
398
+ await quest.cmd(`client-session.set-${propertie}`, {
405
399
  id: clientSessionId,
406
400
  [propertie]: value,
407
401
  });
408
402
  });
409
403
 
410
- Goblin.registerQuest(goblinName, 'save-window-state', function* (
404
+ Goblin.registerQuest(goblinName, 'save-window-state', async function (
411
405
  quest,
412
406
  winId,
413
407
  state
414
408
  ) {
415
409
  const clientSessionId = quest.goblin.getX('clientSessionId');
416
- yield quest.cmd(`client-session.set-window-state`, {
410
+ await quest.cmd(`client-session.set-window-state`, {
417
411
  id: clientSessionId,
418
412
  winId,
419
413
  state,
@@ -422,22 +416,22 @@ Goblin.registerQuest(goblinName, 'save-window-state', function* (
422
416
 
423
417
  /******************************************************************************/
424
418
 
425
- Goblin.registerQuest(goblinName, 'init-theme', function* (
419
+ Goblin.registerQuest(goblinName, 'init-theme', async function (
426
420
  quest,
427
421
  clientSessionId
428
422
  ) {
429
- const name = yield quest.cmd('client-session.get-theme', {
423
+ const name = await quest.cmd('client-session.get-theme', {
430
424
  id: clientSessionId,
431
425
  });
432
426
 
433
427
  if (name) {
434
- yield quest.me.changeTheme({name});
428
+ await quest.me.changeTheme({name});
435
429
  }
436
430
  });
437
431
 
438
- Goblin.registerQuest(goblinName, 'change-theme', function* (quest, name) {
432
+ Goblin.registerQuest(goblinName, 'change-theme', async function (quest, name) {
439
433
  quest.do({name});
440
- yield quest.me.saveSettings({propertie: 'theme'});
434
+ await quest.me.saveSettings({propertie: 'theme'});
441
435
  });
442
436
 
443
437
  Goblin.registerQuest(goblinName, 'reload-theme', function (quest, name) {
@@ -446,49 +440,47 @@ Goblin.registerQuest(goblinName, 'reload-theme', function (quest, name) {
446
440
 
447
441
  /******************************************************************************/
448
442
 
449
- Goblin.registerQuest(goblinName, 'init-zoom', function* (
443
+ Goblin.registerQuest(goblinName, 'init-zoom', async function (
450
444
  quest,
451
445
  clientSessionId
452
446
  ) {
453
- let zoom = yield quest.cmd('client-session.get-zoom', {
447
+ let zoom = await quest.cmd('client-session.get-zoom', {
454
448
  id: clientSessionId,
455
449
  });
456
450
 
457
- yield quest.me.setZoom({
458
- zoom,
459
- });
451
+ await quest.me.setZoom({zoom});
460
452
  });
461
453
 
462
- Goblin.registerQuest(goblinName, 'set-zoom', function* (quest) {
454
+ Goblin.registerQuest(goblinName, 'set-zoom', async function (quest) {
463
455
  quest.do();
464
- yield quest.me.saveSettings({propertie: 'zoom'});
456
+ await quest.me.saveSettings({propertie: 'zoom'});
465
457
  });
466
458
 
467
- Goblin.registerQuest(goblinName, 'zoom', function* (quest) {
459
+ Goblin.registerQuest(goblinName, 'zoom', async function (quest) {
468
460
  quest.do();
469
- yield quest.me.saveSettings({propertie: 'zoom'});
461
+ await quest.me.saveSettings({propertie: 'zoom'});
470
462
  });
471
463
 
472
- Goblin.registerQuest(goblinName, 'un-zoom', function* (quest) {
464
+ Goblin.registerQuest(goblinName, 'un-zoom', async function (quest) {
473
465
  quest.do();
474
- yield quest.me.saveSettings({propertie: 'zoom'});
466
+ await quest.me.saveSettings({propertie: 'zoom'});
475
467
  });
476
468
 
477
- Goblin.registerQuest(goblinName, 'default-zoom', function* (quest) {
469
+ Goblin.registerQuest(goblinName, 'default-zoom', async function (quest) {
478
470
  quest.do();
479
- yield quest.me.saveSettings({propertie: 'zoom'});
471
+ await quest.me.saveSettings({propertie: 'zoom'});
480
472
  });
481
473
 
482
- Goblin.registerQuest(goblinName, 'change-zoom', function* (quest) {
474
+ Goblin.registerQuest(goblinName, 'change-zoom', async function (quest) {
483
475
  quest.do();
484
- yield quest.me.saveSettings({propertie: 'zoom'});
476
+ await quest.me.saveSettings({propertie: 'zoom'});
485
477
  });
486
478
 
487
479
  /******************************************************************************/
488
480
 
489
- Goblin.registerQuest(goblinName, 'dispatch', function* (quest, action) {
481
+ Goblin.registerQuest(goblinName, 'dispatch', async function (quest, action) {
490
482
  const win = quest.getAPI(`wm@${quest.goblin.id}`);
491
- yield win.dispatch({action});
483
+ await win.dispatch({action});
492
484
  });
493
485
 
494
486
  Goblin.registerQuest(goblinName, 'open', function (quest, route) {
@@ -496,7 +488,7 @@ Goblin.registerQuest(goblinName, 'open', function (quest, route) {
496
488
  quest.log.info(route);
497
489
  });
498
490
 
499
- Goblin.registerQuest(goblinName, 'del', function* (quest, widgetId) {
491
+ Goblin.registerQuest(goblinName, 'del', async function (quest, widgetId) {
500
492
  const state = quest.goblin.getState();
501
493
  const feed = state.get('feed');
502
494
  const branch = widgetId;
@@ -506,12 +498,12 @@ Goblin.registerQuest(goblinName, 'del', function* (quest, widgetId) {
506
498
  if (quest.goblin.getX('forceDispose')) {
507
499
  const WM = require('xcraft-core-host/lib/wm.js').instance;
508
500
  WM.disposeAll();
509
- quest.cmd('shutdown'); /* no yield here because it's terminated */
501
+ quest.cmd('shutdown'); /* no await here because it's terminated */
510
502
  return;
511
503
  }
512
504
 
513
505
  if (branch === feed || (branch === labId && useConfigurator === false)) {
514
- yield quest.warehouse.unsubscribe({feed: branch});
506
+ await quest.warehouse.unsubscribe({feed: branch});
515
507
  } else {
516
508
  quest.log.info(
517
509
  `Laboratory deleting widget ${widgetId} from window ${feed}`
@@ -523,7 +515,7 @@ Goblin.registerQuest(goblinName, 'del', function* (quest, widgetId) {
523
515
  if (branch === labId) {
524
516
  parents.push(`goblin-orc@*`);
525
517
  }
526
- yield quest.warehouse.feedSubscriptionDel({feed, branch, parents});
518
+ await quest.warehouse.feedSubscriptionDel({feed, branch, parents});
527
519
  }
528
520
  });
529
521
 
@@ -29,6 +29,8 @@ const throttle250 = _.throttle((fct) => fct(), 250);
29
29
  // }
30
30
 
31
31
  class Widget extends React.Component {
32
+ static #nameCache = new Map();
33
+
32
34
  constructor() {
33
35
  super(...arguments);
34
36
  this._names = this._getInheritedNames();
@@ -44,7 +46,14 @@ class Widget extends React.Component {
44
46
  }
45
47
 
46
48
  static getWidgetName(constructorName) {
47
- return constructorName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
49
+ if (this.#nameCache.has(constructorName)) {
50
+ return this.#nameCache.get(constructorName);
51
+ }
52
+ const name = constructorName
53
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
54
+ .toLowerCase();
55
+ this.#nameCache.set(constructorName, name);
56
+ return name;
48
57
  }
49
58
 
50
59
  _getInheritedNames() {
@@ -305,7 +314,8 @@ class Widget extends React.Component {
305
314
  }
306
315
  /** @deprecated Replace by doFor.
307
316
  * It's possible to have a mismatch between service name and serviceId.
308
- * Prefer to use doFor with the service id. */
317
+ * Prefer to use doFor with the service id.
318
+ */
309
319
  doAs(service, action, args) {
310
320
  const id = this.props.id || this.context.id;
311
321
  if (!id) {