@live-change/frontend-template 0.9.197 → 0.9.199

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +184 -0
  2. package/.claude/rules/live-change-backend-architecture.md +126 -0
  3. package/.claude/rules/live-change-backend-models-and-relations.md +188 -0
  4. package/.claude/rules/live-change-frontend-vue-primevue.md +291 -0
  5. package/.claude/rules/live-change-service-structure.md +89 -0
  6. package/.claude/skills/create-skills-and-rules.md +196 -0
  7. package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
  8. package/.claude/skills/live-change-design-models-relations.md +173 -0
  9. package/.claude/skills/live-change-design-service.md +132 -0
  10. package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
  11. package/.claude/skills/live-change-frontend-action-form.md +143 -0
  12. package/.claude/skills/live-change-frontend-analytics.md +146 -0
  13. package/.claude/skills/live-change-frontend-command-forms.md +215 -0
  14. package/.claude/skills/live-change-frontend-data-views.md +182 -0
  15. package/.claude/skills/live-change-frontend-editor-form.md +177 -0
  16. package/.claude/skills/live-change-frontend-locale-time.md +171 -0
  17. package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
  18. package/.claude/skills/live-change-frontend-range-list.md +128 -0
  19. package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
  20. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +202 -0
  21. package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
  22. package/.cursor/rules/live-change-backend-models-and-relations.mdc +194 -0
  23. package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
  24. package/.cursor/rules/live-change-service-structure.mdc +107 -0
  25. package/.cursor/skills/live-change-design-actions-views-triggers.md +197 -0
  26. package/.cursor/skills/live-change-design-models-relations.md +168 -0
  27. package/.cursor/skills/live-change-design-service.md +75 -0
  28. package/.cursor/skills/live-change-frontend-action-buttons.md +128 -0
  29. package/.cursor/skills/live-change-frontend-action-form.md +143 -0
  30. package/.cursor/skills/live-change-frontend-analytics.md +146 -0
  31. package/.cursor/skills/live-change-frontend-command-forms.md +215 -0
  32. package/.cursor/skills/live-change-frontend-data-views.md +182 -0
  33. package/.cursor/skills/live-change-frontend-editor-form.md +177 -0
  34. package/.cursor/skills/live-change-frontend-locale-time.md +171 -0
  35. package/.cursor/skills/live-change-frontend-page-list-detail.md +200 -0
  36. package/.cursor/skills/live-change-frontend-range-list.md +128 -0
  37. package/.cursor/skills/live-change-frontend-ssr-setup.md +119 -0
  38. package/README.md +71 -0
  39. package/package.json +50 -50
  40. package/server/app.config.js +35 -0
  41. package/server/services.list.js +2 -0
  42. package/.nx/workspace-data/file-map.json +0 -195
  43. package/.nx/workspace-data/nx_files.nxt +0 -0
  44. package/.nx/workspace-data/project-graph.json +0 -8
  45. package/.nx/workspace-data/project-graph.lock +0 -0
  46. package/.nx/workspace-data/source-maps.json +0 -1
@@ -0,0 +1,290 @@
1
+ ---
2
+ description: Rules for Vue 3, PrimeVue, Tailwind frontend development on LiveChange
3
+ globs: **/front/src/**/*.{vue,js,ts}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Frontend na live-change-stack – Vue 3 + PrimeVue + Tailwind
8
+
9
+ ## Stack
10
+
11
+ - Vue 3 + TypeScript
12
+ - PrimeVue 4 – główna biblioteka komponentów UI
13
+ - Tailwind CSS – podstawowe stylowanie (unikaj zbędnego custom CSS)
14
+ - vite-plugin-pages – file-based routing w `src/pages/`
15
+ - @live-change/vue3-ssr – integracja z backendem LiveChange
16
+
17
+ ## Pobieranie danych – `live` + `Promise.all`
18
+
19
+ - **Nie** używaj `ref(null)` + `onMounted` do ładowania danych.
20
+ - Dane ładuj w `setup`/`script setup` przez `await Promise.all(...)` na `live(path()....)`.
21
+ - Rodzic powinien owijać stronę w `<Suspense>` (w projektach live-change robi to zazwyczaj `ViewRoot` globalnie).
22
+
23
+ Przykład:
24
+
25
+ ```js
26
+ import { path, live, api as useApi } from '@live-change/vue3-ssr'
27
+
28
+ const api = useApi()
29
+
30
+ const [devices] = await Promise.all([
31
+ live(path().deviceManager.myUserDevices({}))
32
+ ])
33
+
34
+ const [device, connections] = await Promise.all([
35
+ live(path().deviceManager.myUserDevice({ device: deviceId })),
36
+ live(path().deviceManager.deviceOwnedDeviceConnections({ device: deviceId }))
37
+ ])
38
+ ```
39
+
40
+ Dostęp w template:
41
+
42
+ ```vue
43
+ <template>
44
+ <div v-if="device.value">
45
+ {{ device.value.name }}
46
+ </div>
47
+ </template>
48
+ ```
49
+
50
+ ## Komendy i formularze – wybór wzorca
51
+
52
+ Są 4 sposoby wywoływania akcji backendowych. Używaj właściwego:
53
+
54
+ | Wzorzec | Kiedy używać |
55
+ |---|---|
56
+ | `editorData` | **Edycja rekordów modelu** (create/update). Drafty, walidacja, `AutoField`. Ustawienia, edytory, profile. |
57
+ | `actionData` | **Jednorazowe formularze akcji** (nie CRUD). Submit → done. Publikacja, zaproszenie, import. |
58
+ | `api.command` | **Pojedynczy przycisk lub wywołanie z kodu** (bez pól formularza). Usuwanie, toggle, akcje z kodu. |
59
+ | `<command-form>` | **Unikaj.** Legacy, tylko trywialne prototypy. Preferuj `editorData` lub `actionData`. |
60
+
61
+ Schemat decyzji:
62
+
63
+ 1. Użytkownik wypełnia pola formularza? → **Nie**: użyj `api.command` (owijaj w `workingZone.addPromise` dla przycisków).
64
+ 2. Edytuje rekord modelu (create/update)? → **Tak**: użyj `editorData`. **Nie**: użyj `actionData`.
65
+ 3. `<command-form>` tylko do najprostszych jednorazowych przypadków.
66
+
67
+ ### `api.command`
68
+
69
+ ```js
70
+ await api.command(['deviceManager', 'createMyUserDevice'], {
71
+ name: 'Moje urządzenie'
72
+ })
73
+
74
+ await api.command(['deviceManager', 'deleteMyUserDevice'], {
75
+ device: id
76
+ })
77
+ ```
78
+
79
+ Format komendy:
80
+
81
+ - tablica `[serviceName, actionName]`,
82
+ - payload jako zwykły obiekt.
83
+
84
+ ## Routing – blok `<route>` i meta `signedIn`
85
+
86
+ - Każda strona w `src/pages/` może mieć blok `<route>` z metadanymi.
87
+ - Używaj `meta.signedIn` do oznaczania stron wymagających logowania.
88
+
89
+ ```vue
90
+ <route>
91
+ { "name": "devices", "meta": { "signedIn": true } }
92
+ </route>
93
+ ```
94
+
95
+ Dynamiczne ścieżki:
96
+
97
+ - plik `[id].vue` odpowiada ścieżce `/devices/:id`.
98
+
99
+ ```js
100
+ import { useRoute } from 'vue-router'
101
+
102
+ const route = useRoute()
103
+ const deviceId = route.params.id
104
+ ```
105
+
106
+ ## Confirm + Toast – wzorzec dla akcji destrukcyjnych
107
+
108
+ ```js
109
+ import { useConfirm } from 'primevue/useconfirm'
110
+ import { useToast } from 'primevue/usetoast'
111
+
112
+ const confirm = useConfirm()
113
+ const toast = useToast()
114
+
115
+ function deleteDevice(id) {
116
+ confirm.require({
117
+ message: 'Czy na pewno chcesz usunąć to urządzenie?',
118
+ header: 'Potwierdzenie',
119
+ icon: 'pi pi-exclamation-triangle',
120
+ accept: async () => {
121
+ await api.command(['deviceManager', 'deleteMyUserDevice'], { device: id })
122
+ toast.add({
123
+ severity: 'success',
124
+ summary: 'Usunięto',
125
+ life: 2000
126
+ })
127
+ }
128
+ })
129
+ }
130
+ ```
131
+
132
+ ## Wzorce UI – lista i szczegóły
133
+
134
+ ### Lista
135
+
136
+ ```vue
137
+ <template>
138
+ <div class="container mx-auto p-4">
139
+ <div class="flex items-center justify-between mb-6">
140
+ <h1 class="text-2xl font-bold">Urządzenia</h1>
141
+ <Button label="Dodaj" icon="pi pi-plus" @click="openDialog" />
142
+ </div>
143
+
144
+ <Card v-if="devices.value?.length === 0">
145
+ <template #content>
146
+ <p class="text-center text-gray-500">
147
+ Brak urządzeń
148
+ </p>
149
+ </template>
150
+ </Card>
151
+
152
+ <div class="grid gap-4">
153
+ <Card v-for="device in devices.value" :key="device.id">
154
+ <template #content>
155
+ <!-- zawartość -->
156
+ </template>
157
+ </Card>
158
+ </div>
159
+ </div>
160
+ </template>
161
+ ```
162
+
163
+ ### Tag statusu
164
+
165
+ ```vue
166
+ <Tag
167
+ :value="conn.status"
168
+ :severity="conn.status === 'online' ? 'success' : 'secondary'"
169
+ />
170
+ ```
171
+
172
+ ## Computed paths – reaktywne parametry
173
+
174
+ Gdy ścieżki zależą od reaktywnych wartości (route params, props), owijaj je w `computed()`:
175
+
176
+ ```js
177
+ import { computed, unref } from 'vue'
178
+ import { usePath, live } from '@live-change/vue3-ssr'
179
+
180
+ const path = usePath()
181
+
182
+ const articlePath = computed(() => path.blog.article({ article: unref(articleId) }))
183
+ const [article] = await Promise.all([live(articlePath)])
184
+ ```
185
+
186
+ Dla warunkowego ładowania (np. tylko gdy zalogowany), zwróć wartość falsy:
187
+
188
+ ```js
189
+ import { useClient } from '@live-change/vue3-ssr'
190
+ const client = useClient()
191
+
192
+ const myDataPath = computed(() => client.value.user && path.blog.myArticles({}))
193
+ const [myData] = await Promise.all([live(myDataPath)])
194
+ ```
195
+
196
+ ## Powiązane dane – `.with()`
197
+
198
+ Dołączaj powiązane obiekty do elementów w jednym reaktywnym zapytaniu:
199
+
200
+ ```js
201
+ path.blog.articles({})
202
+ .with(article => path.userIdentification.identification({
203
+ sessionOrUserType: article.authorType,
204
+ sessionOrUser: article.author
205
+ }).bind('authorProfile'))
206
+ ```
207
+
208
+ Dostęp: `article.authorProfile?.firstName`. Działa zarówno z `live()` jak i `RangeViewer`.
209
+
210
+ ## WorkingZone dla akcji asynchronicznych
211
+
212
+ `ViewRoot` opakowuje każdą stronę w `<WorkingZone>`. Używaj `inject('workingZone')` dla akcji przycisków poza formularzami:
213
+
214
+ ```js
215
+ import { inject } from 'vue'
216
+ const workingZone = inject('workingZone')
217
+
218
+ function doAction() {
219
+ workingZone.addPromise('actionName', (async () => {
220
+ await actions.blog.publishArticle({ article: id })
221
+ toast.add({ severity: 'success', summary: 'Opublikowano', life: 2000 })
222
+ })())
223
+ }
224
+ ```
225
+
226
+ Aktywuje globalny spinner/blur na czas trwania promisy.
227
+
228
+ ## Kontrola dostępu z `useClient`
229
+
230
+ ```js
231
+ import { useClient } from '@live-change/vue3-ssr'
232
+ const client = useClient()
233
+ ```
234
+
235
+ - `client.value.user` – truthy gdy zalogowany
236
+ - `client.value.roles` – tablica ról (np. `['admin', 'owner']`)
237
+
238
+ W template:
239
+
240
+ ```vue
241
+ <Button v-if="client.roles.includes('admin')" label="Admin" />
242
+ <div v-if="!client.user">Zaloguj się</div>
243
+ ```
244
+
245
+ ## Locale i czas
246
+
247
+ - Wywołaj `useLocale().captureLocale()` w `App.vue` żeby zapisać locale przeglądarki do backendu.
248
+ - Używaj `locale.localTime(date)` z `d()` z vue-i18n do wyświetlania dat bezpiecznego dla SSR.
249
+ - `currentTime` z `@live-change/frontend-base` to reaktywny ref który tykuje co 500ms.
250
+ - `useTimeSynchronization()` z `@live-change/vue3-ssr` koryguje różnicę zegarów – używaj gdy timing jest krytyczny (odliczanie, eventy real-time).
251
+
252
+ ## Analityka
253
+
254
+ Używaj `analytics` z `@live-change/vue3-components`:
255
+
256
+ ```js
257
+ import { analytics } from '@live-change/vue3-components'
258
+ analytics.emit('article:published', { articleId: id })
259
+ ```
260
+
261
+ Podpinaj providery (PostHog, GA4) w osobnym pliku importowanym z `App.vue`.
262
+
263
+ ## SSR i konfiguracja frontendu
264
+
265
+ - Utrzymuj standardowy układ entry points:
266
+
267
+ ```js
268
+ // entry-client.js
269
+ import { clientEntry } from '@live-change/frontend-base/client-entry.js'
270
+ export default clientEntry(App, createRouter, config)
271
+
272
+ // entry-server.js
273
+ import { serverEntry, sitemapEntry } from '@live-change/frontend-base/server-entry.js'
274
+ export const render = serverEntry(App, createRouter, config)
275
+ export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
276
+ ```
277
+
278
+ - Konfigurację PrimeVue trzymaj w jednym miejscu (`config.js`) i używaj `definePreset` do nadpisania motywu.
279
+
280
+ ## Odkrywanie widoków i akcji z `describe`
281
+
282
+ Komenda CLI `describe` pozwala znaleźć dostępne widoki (do `live()`) i akcje (do `api.command` / `editorData` / `actionData`):
283
+
284
+ ```bash
285
+ node server/start.js describe --service blog
286
+ node server/start.js describe --service blog --view articlesByCreatedAt --output yaml
287
+ node server/start.js describe --service blog --action createMyUserArticle --output yaml
288
+ ```
289
+
290
+ To najszybszy sposób na odkrycie jakie ścieżki i akcje są dostępne, w tym te wygenerowane automatycznie przez relacje.
@@ -0,0 +1,107 @@
1
+ ---
2
+ description: LiveChange service file structure — every service must be a directory with separate definition.js, index.js, etc.
3
+ globs: server/**/*.js
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # LiveChange Service Structure
8
+
9
+ Every LiveChange service **must** be a directory, not a single file.
10
+
11
+ ## Required structure
12
+
13
+ ```
14
+ server/<serviceName>/
15
+ definition.js # creates app.createServiceDefinition({ name }) – nothing else
16
+ index.js # imports definition, imports all domain files, exports definition
17
+ config.js # required – resolves definition.config, sets definition.clientConfig, exports plain config object
18
+ <domain>.js # one file per domain area (models, views, actions, triggers)
19
+ ```
20
+
21
+ ## definition.js
22
+
23
+ Only creates and exports the definition. No models, no actions here.
24
+
25
+ ```js
26
+ import App from '@live-change/framework'
27
+ const app = App.app()
28
+
29
+ const definition = app.createServiceDefinition({ name: 'myService' })
30
+
31
+ export default definition
32
+ ```
33
+
34
+ ## index.js
35
+
36
+ Imports definition and all domain files (side-effect imports), then re-exports definition.
37
+
38
+ ```js
39
+ import App from '@live-change/framework'
40
+ const app = App.app()
41
+
42
+ import definition from './definition.js'
43
+
44
+ import './authenticator.js'
45
+ import './myModel.js'
46
+ import './otherModel.js'
47
+
48
+ export default definition
49
+ ```
50
+
51
+ ## config.js (required)
52
+
53
+ Reads `definition.config` (passed from `app.config.js`), resolves defaults, sets `definition.clientConfig` (for frontend),
54
+ and exports a plain config object used by other domain files.
55
+
56
+ ```js
57
+ import definition from './definition.js'
58
+
59
+ const {
60
+ defaultAccess = {
61
+ writeAccessControl: ['owner', 'admin'],
62
+ readAllAccess: ['admin']
63
+ }
64
+ } = definition.config || {}
65
+
66
+ definition.clientConfig = {
67
+ // values exposed to frontend, if needed
68
+ }
69
+
70
+ export default { defaultAccess }
71
+ ```
72
+
73
+ ## Domain files (e.g. myModel.js)
74
+
75
+ Each file imports `definition` (and `config` if needed) and registers models/views/actions/triggers.
76
+
77
+ ```js
78
+ import App from '@live-change/framework'
79
+ const app = App.app()
80
+
81
+ import definition from './definition.js'
82
+ import config from './config.js'
83
+
84
+ export const MyModel = definition.model({ name: 'MyModel', ... })
85
+
86
+ definition.view({ name: 'myList', ... })
87
+ definition.action({ name: 'doThing', ... })
88
+ ```
89
+
90
+ Rule of thumb:
91
+
92
+ - Domain files should read configuration from `config` (exported from `config.js`), not directly from `definition.config`.
93
+
94
+ ## services.list.js import
95
+
96
+ Always import from the directory index, not a flat file:
97
+
98
+ ```js
99
+ import myService from './myService/index.js' // ✓
100
+ import myService from './myService.js' // ✗
101
+ ```
102
+
103
+ ## Why
104
+
105
+ A flat single-file service makes it hard to add new domain files (auth, models, triggers, etc.)
106
+ without the file growing unmanageably. The directory structure mirrors the pattern used in
107
+ `@live-change/live-change-stack/services/*`.
@@ -0,0 +1,197 @@
1
+ ---
2
+ description: Design actions, views, triggers with indexes and batch processing patterns
3
+ ---
4
+
5
+ # Skill: live-change-design-actions-views-triggers
6
+
7
+ Ten skill opisuje **krok po kroku**, jak projektować akcje, widoki i triggery w serwisach LiveChange, korzystając z indeksów i unikając pełnych skanów.
8
+
9
+ ## Kiedy używać
10
+
11
+ Użyj tego skilla, gdy:
12
+
13
+ - dodajesz nowe akcje do istniejących modeli,
14
+ - tworzysz widoki (szczególnie zakresowe) dla list,
15
+ - implementujesz triggery (online/offline, batchowe przetwarzanie, asynchroniczne wyniki).
16
+
17
+ ## 1. Projekt akcji
18
+
19
+ 1. **Określ cel akcji**
20
+ - Czy tworzy/aktualizuje/usunie obiekt?
21
+ - Czy ma czekać na zewnętrzny wynik (np. urządzenie)?
22
+
23
+ 2. **Zdefiniuj `properties`**
24
+ - Każde pole opisane wieloliniowo,
25
+ - tylko to, co faktycznie potrzebne – resztę pobieraj z bazy na podstawie kluczy.
26
+
27
+ 3. **Użyj indeksów do wyszukiwania**
28
+ - Zamiast pełnych skanów, korzystaj z `indexObjectGet` / `indexRangeGet`.
29
+
30
+ 4. **Zwróć sensowny wynik**
31
+ - ID utworzonego obiektu,
32
+ - klucze sesyjne, jeśli trzeba je przechować po stronie klienta,
33
+ - dane potrzebne do dalszych kroków.
34
+
35
+ ### Szkic wzorca
36
+
37
+ ```js
38
+ definition.action({
39
+ name: 'someAction',
40
+ properties: {
41
+ someKey: {
42
+ type: String
43
+ }
44
+ },
45
+ async execute({ someKey }, { client, service }) {
46
+ const obj = await SomeModel.indexObjectGet('bySomeKey', { someKey })
47
+ if(!obj) throw new Error('notFound')
48
+
49
+ const id = app.generateUid()
50
+
51
+ await SomeOtherModel.create({
52
+ id,
53
+ // ...
54
+ })
55
+
56
+ return { id }
57
+ }
58
+ })
59
+ ```
60
+
61
+ ## 2. Projekt widoku
62
+
63
+ 1. **Zdecyduj, czy to pojedynczy obiekt czy lista**
64
+ - pojedynczy: użyj `get` lub `indexObjectGet`,
65
+ - lista: użyj `indexRangeGet` z indeksem.
66
+
67
+ 2. **Zdefiniuj `properties` widoku**
68
+ - tylko parametry potrzebne do wyszukiwania,
69
+ - typy zgodne z modelem (String/Number/itp.).
70
+
71
+ 3. **Użyj indeksów**
72
+
73
+ Przykład widoku zakresowego:
74
+
75
+ ```js
76
+ definition.view({
77
+ name: 'myItemsByStatus',
78
+ properties: {
79
+ status: {
80
+ type: String
81
+ }
82
+ },
83
+ async get({ status }, { client, service }) {
84
+ return MyModel.indexRangeGet('byStatus', {
85
+ status
86
+ })
87
+ }
88
+ })
89
+ ```
90
+
91
+ ## 3. Triggery – online/offline
92
+
93
+ 1. **Zidentyfikuj zdarzenie**
94
+ - np. „połączenie online/offline”, „sesja utworzona”, „serwer się uruchomił”.
95
+
96
+ 2. **Zdefiniuj trigger z `properties`**
97
+ - triggery online/offline zwykle potrzebują tylko ID obiektu.
98
+
99
+ 3. **Aktualizuj minimalny zestaw pól**
100
+ - np. `status`, `lastSeenAt`.
101
+
102
+ Przykład:
103
+
104
+ ```js
105
+ definition.trigger({
106
+ name: 'sessionConnectionOnline',
107
+ properties: {
108
+ connection: {
109
+ type: String
110
+ }
111
+ },
112
+ async execute({ connection }, { service }) {
113
+ await Connection.update(connection, {
114
+ status: 'online',
115
+ lastSeenAt: new Date()
116
+ })
117
+ }
118
+ })
119
+ ```
120
+
121
+ ## 4. Triggery batchowe – unikaj pełnych skanów
122
+
123
+ 1. **Ustal limit batcha** (np. 32 lub 128 rekordów).
124
+ 2. **Wykorzystaj `rangeGet` z `gt: lastId`**
125
+ - inicjalnie `last = ''`,
126
+ - po każdym batchu ustaw `last` na ID ostatniego rekordu.
127
+ 3. **Kończ, gdy batch jest pusty**.
128
+
129
+ Przykład:
130
+
131
+ ```js
132
+ definition.trigger({
133
+ name: 'allOffline',
134
+ async execute({}, { service }) {
135
+ let last = ''
136
+ while(true) {
137
+ const items = await Connection.rangeGet({
138
+ gt: last,
139
+ limit: 32
140
+ })
141
+ if(items.length === 0) break
142
+
143
+ for(const item of items) {
144
+ await Connection.update(item.id, {
145
+ status: 'offline'
146
+ })
147
+ }
148
+
149
+ last = items[items.length - 1].id
150
+ }
151
+ }
152
+ })
153
+ ```
154
+
155
+ ## 5. Wzorzec „pending + resolve” dla asynchronicznych wyników
156
+
157
+ Użyj tego wzorca, gdy:
158
+
159
+ - akcja tworzy zlecenie/komendę,
160
+ - wynik przychodzi później z innego procesu (urządzenie, worker, itp.),
161
+ - chcesz, żeby akcja czekała na wynik z timeoutem.
162
+
163
+ ### Kroki
164
+
165
+ 1. Utwórz helper `pendingCommands` (Map) w osobnym module.
166
+ 2. W akcji tworzącej:
167
+ - utwórz rekord z `status: 'pending'`,
168
+ - wywołaj `waitForCommand(id, timeoutMs)`.
169
+ 3. W akcji raportującej:
170
+ - zaktualizuj rekord (`status: 'completed'`, `result`),
171
+ - wywołaj `resolveCommand(id, result)`.
172
+
173
+ ### Szkic helpera
174
+
175
+ ```js
176
+ const pendingCommands = new Map()
177
+
178
+ export function waitForCommand(commandId, timeoutMs = 115000) {
179
+ return new Promise((resolve, reject) => {
180
+ const timer = setTimeout(() => {
181
+ pendingCommands.delete(commandId)
182
+ reject(new Error('timeout'))
183
+ }, timeoutMs)
184
+ pendingCommands.set(commandId, { resolve, reject, timer })
185
+ })
186
+ }
187
+
188
+ export function resolveCommand(commandId, result) {
189
+ const pending = pendingCommands.get(commandId)
190
+ if(pending) {
191
+ clearTimeout(pending.timer)
192
+ pendingCommands.delete(commandId)
193
+ pending.resolve(result)
194
+ }
195
+ }
196
+ ```
197
+