@pulze-io/renderflow 1.2.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/dist/index.mjs ADDED
@@ -0,0 +1,776 @@
1
+ // src/index.ts
2
+ import axios2 from "axios";
3
+
4
+ // src/api.ts
5
+ import axios from "axios";
6
+ var HttpClient = class {
7
+ constructor({
8
+ securityWorker,
9
+ secure,
10
+ format,
11
+ ...axiosConfig
12
+ } = {}) {
13
+ this.securityData = null;
14
+ this.setSecurityData = (data) => {
15
+ this.securityData = data;
16
+ };
17
+ this.request = async ({
18
+ secure,
19
+ path,
20
+ type,
21
+ query,
22
+ format,
23
+ body,
24
+ ...params
25
+ }) => {
26
+ const secureParams = (typeof secure === "boolean" ? secure : this.secure) && this.securityWorker && await this.securityWorker(this.securityData) || {};
27
+ const requestParams = this.mergeRequestParams(params, secureParams);
28
+ const responseFormat = format || this.format || void 0;
29
+ if (type === "multipart/form-data" /* FormData */ && body && body !== null && typeof body === "object") {
30
+ body = this.createFormData(body);
31
+ }
32
+ if (type === "text/plain" /* Text */ && body && body !== null && typeof body !== "string") {
33
+ body = JSON.stringify(body);
34
+ }
35
+ return this.instance.request({
36
+ ...requestParams,
37
+ headers: {
38
+ ...requestParams.headers || {},
39
+ ...type ? { "Content-Type": type } : {}
40
+ },
41
+ params: query,
42
+ responseType: responseFormat,
43
+ data: body,
44
+ url: path
45
+ }).then((response) => response.data);
46
+ };
47
+ this.instance = axios.create({
48
+ ...axiosConfig,
49
+ baseURL: axiosConfig.baseURL || "/api/v1"
50
+ });
51
+ this.secure = secure;
52
+ this.format = format;
53
+ this.securityWorker = securityWorker;
54
+ }
55
+ mergeRequestParams(params1, params2) {
56
+ const method = params1.method || params2 && params2.method;
57
+ return {
58
+ ...this.instance.defaults,
59
+ ...params1,
60
+ ...params2 || {},
61
+ headers: {
62
+ ...method && this.instance.defaults.headers[method.toLowerCase()] || {},
63
+ ...params1.headers || {},
64
+ ...params2 && params2.headers || {}
65
+ }
66
+ };
67
+ }
68
+ stringifyFormItem(formItem) {
69
+ if (typeof formItem === "object" && formItem !== null) {
70
+ return JSON.stringify(formItem);
71
+ } else {
72
+ return `${formItem}`;
73
+ }
74
+ }
75
+ createFormData(input) {
76
+ if (input instanceof FormData) {
77
+ return input;
78
+ }
79
+ return Object.keys(input || {}).reduce((formData, key) => {
80
+ const property = input[key];
81
+ const propertyContent = property instanceof Array ? property : [property];
82
+ for (const formItem of propertyContent) {
83
+ const isFileType = formItem instanceof Blob || formItem instanceof File;
84
+ formData.append(
85
+ key,
86
+ isFileType ? formItem : this.stringifyFormItem(formItem)
87
+ );
88
+ }
89
+ return formData;
90
+ }, new FormData());
91
+ }
92
+ };
93
+ var Api = class {
94
+ constructor(http) {
95
+ this.users = {
96
+ /**
97
+ * @description Get all users
98
+ *
99
+ * @tags Users
100
+ * @name GetUsers
101
+ * @request GET:/users
102
+ * @secure
103
+ */
104
+ getUsers: (params = {}) => this.http.request({
105
+ path: `/users`,
106
+ method: "GET",
107
+ secure: true,
108
+ format: "json",
109
+ ...params
110
+ }),
111
+ /**
112
+ * @description Get a specific user by id
113
+ *
114
+ * @tags Users
115
+ * @name GetUser
116
+ * @request GET:/users/{id}
117
+ * @secure
118
+ */
119
+ getUser: (id, params = {}) => this.http.request({
120
+ path: `/users/${id}`,
121
+ method: "GET",
122
+ secure: true,
123
+ format: "json",
124
+ ...params
125
+ })
126
+ };
127
+ this.tasks = {
128
+ /**
129
+ * @description Get tasks for a specific job
130
+ *
131
+ * @tags Tasks
132
+ * @name GetTasks
133
+ * @request GET:/tasks
134
+ * @secure
135
+ */
136
+ getTasks: (query, params = {}) => this.http.request({
137
+ path: `/tasks`,
138
+ method: "GET",
139
+ query,
140
+ secure: true,
141
+ format: "json",
142
+ ...params
143
+ }),
144
+ /**
145
+ * @description Receive update stream from tasks
146
+ *
147
+ * @tags Tasks
148
+ * @name GetTaskEvents
149
+ * @request GET:/tasks/events/{job_id}
150
+ * @secure
151
+ */
152
+ getTaskEvents: (jobId, params = {}) => this.http.request({
153
+ path: `/tasks/events/${jobId}`,
154
+ method: "GET",
155
+ secure: true,
156
+ ...params
157
+ }),
158
+ /**
159
+ * @description Get task logs
160
+ *
161
+ * @tags Tasks
162
+ * @name GetTaskLogs
163
+ * @request GET:/tasks/{id}/logs
164
+ * @secure
165
+ */
166
+ getTaskLogs: (id, query, params = {}) => this.http.request({
167
+ path: `/tasks/${id}/logs`,
168
+ method: "GET",
169
+ query,
170
+ secure: true,
171
+ format: "json",
172
+ ...params
173
+ }),
174
+ /**
175
+ * @description Stream task logs via SSE
176
+ *
177
+ * @tags Tasks
178
+ * @name StreamTaskLogs
179
+ * @request GET:/tasks/{id}/logs/stream
180
+ * @secure
181
+ */
182
+ streamTaskLogs: (id, params = {}) => this.http.request({
183
+ path: `/tasks/${id}/logs/stream`,
184
+ method: "GET",
185
+ secure: true,
186
+ ...params
187
+ }),
188
+ /**
189
+ * @description Get task thumbnail
190
+ *
191
+ * @tags Tasks
192
+ * @name GetThumbnail
193
+ * @request GET:/tasks/{id}/thumbnail
194
+ * @secure
195
+ */
196
+ getThumbnail: (id, params = {}) => this.http.request({
197
+ path: `/tasks/${id}/thumbnail`,
198
+ method: "GET",
199
+ secure: true,
200
+ ...params
201
+ }),
202
+ /**
203
+ * @description Get a specific task by id
204
+ *
205
+ * @tags Tasks
206
+ * @name GetTask
207
+ * @request GET:/tasks/{id}
208
+ * @secure
209
+ */
210
+ getTask: (id, params = {}) => this.http.request({
211
+ path: `/tasks/${id}`,
212
+ method: "GET",
213
+ secure: true,
214
+ format: "json",
215
+ ...params
216
+ })
217
+ };
218
+ this.pools = {
219
+ /**
220
+ * @description Get all pools
221
+ *
222
+ * @tags Pools
223
+ * @name GetPools
224
+ * @request GET:/pools
225
+ * @secure
226
+ */
227
+ getPools: (params = {}) => this.http.request({
228
+ path: `/pools`,
229
+ method: "GET",
230
+ secure: true,
231
+ format: "json",
232
+ ...params
233
+ }),
234
+ /**
235
+ * @description Get a specific pool by id
236
+ *
237
+ * @tags Pools
238
+ * @name GetPool
239
+ * @request GET:/pools/{id}
240
+ * @secure
241
+ */
242
+ getPool: (id, params = {}) => this.http.request({
243
+ path: `/pools/${id}`,
244
+ method: "GET",
245
+ secure: true,
246
+ format: "json",
247
+ ...params
248
+ })
249
+ };
250
+ this.nodes = {
251
+ /**
252
+ * @description Get all nodes
253
+ *
254
+ * @tags Nodes
255
+ * @name GetNodes
256
+ * @request GET:/nodes
257
+ * @secure
258
+ */
259
+ getNodes: (params = {}) => this.http.request({
260
+ path: `/nodes`,
261
+ method: "GET",
262
+ secure: true,
263
+ format: "json",
264
+ ...params
265
+ }),
266
+ /**
267
+ * @description Get a specific node by id
268
+ *
269
+ * @tags Nodes
270
+ * @name GetNode
271
+ * @request GET:/nodes/{id}
272
+ * @secure
273
+ */
274
+ getNode: (id, params = {}) => this.http.request({
275
+ path: `/nodes/${id}`,
276
+ method: "GET",
277
+ secure: true,
278
+ format: "json",
279
+ ...params
280
+ }),
281
+ /**
282
+ * @description Delete a node by id
283
+ *
284
+ * @tags Nodes
285
+ * @name DeleteNode
286
+ * @request DELETE:/nodes/{id}
287
+ * @secure
288
+ */
289
+ deleteNode: (id, params = {}) => this.http.request({
290
+ path: `/nodes/${id}`,
291
+ method: "DELETE",
292
+ secure: true,
293
+ format: "json",
294
+ ...params
295
+ }),
296
+ /**
297
+ * @description Receive update stream from nodes
298
+ *
299
+ * @tags Nodes
300
+ * @name GetNodeEvents
301
+ * @request GET:/nodes/events
302
+ * @secure
303
+ */
304
+ getNodeEvents: (params = {}) => this.http.request({
305
+ path: `/nodes/events`,
306
+ method: "GET",
307
+ secure: true,
308
+ ...params
309
+ }),
310
+ /**
311
+ * @description Update the status of a node
312
+ *
313
+ * @tags Nodes
314
+ * @name UpdateStatus
315
+ * @request PATCH:/nodes/{id}/status
316
+ * @secure
317
+ */
318
+ updateStatus: (id, data, params = {}) => this.http.request({
319
+ path: `/nodes/${id}/status`,
320
+ method: "PATCH",
321
+ body: data,
322
+ secure: true,
323
+ type: "application/json" /* Json */,
324
+ format: "json",
325
+ ...params
326
+ }),
327
+ /**
328
+ * @description Get current hardware utilization for a node
329
+ *
330
+ * @tags Nodes
331
+ * @name GetNodeUtilization
332
+ * @request GET:/nodes/{id}/utilization
333
+ * @secure
334
+ */
335
+ getNodeUtilization: (id, params = {}) => this.http.request({
336
+ path: `/nodes/${id}/utilization`,
337
+ method: "GET",
338
+ secure: true,
339
+ format: "json",
340
+ ...params
341
+ }),
342
+ /**
343
+ * @description Update the pool of a node
344
+ *
345
+ * @tags Nodes
346
+ * @name UpdateNodePool
347
+ * @request PATCH:/nodes/{id}/pool
348
+ * @secure
349
+ */
350
+ updateNodePool: (id, data, params = {}) => this.http.request({
351
+ path: `/nodes/${id}/pool`,
352
+ method: "PATCH",
353
+ body: data,
354
+ secure: true,
355
+ type: "application/json" /* Json */,
356
+ format: "json",
357
+ ...params
358
+ }),
359
+ /**
360
+ * @description Get node benchmark rankings
361
+ *
362
+ * @tags Nodes
363
+ * @name GetNodeBenchmarkRanking
364
+ * @request GET:/nodes/benchmarks/ranking
365
+ * @secure
366
+ */
367
+ getNodeBenchmarkRanking: (query, params = {}) => this.http.request({
368
+ path: `/nodes/benchmarks/ranking`,
369
+ method: "GET",
370
+ query,
371
+ secure: true,
372
+ format: "json",
373
+ ...params
374
+ })
375
+ };
376
+ this.jobs = {
377
+ /**
378
+ * @description Receive update stream from jobs
379
+ *
380
+ * @tags Jobs
381
+ * @name GetJobEvents
382
+ * @request GET:/jobs/events
383
+ * @secure
384
+ */
385
+ getJobEvents: (params = {}) => this.http.request({
386
+ path: `/jobs/events`,
387
+ method: "GET",
388
+ secure: true,
389
+ ...params
390
+ }),
391
+ /**
392
+ * @description Get a specific job by id
393
+ *
394
+ * @tags Jobs
395
+ * @name GetJob
396
+ * @request GET:/jobs/{id}
397
+ * @secure
398
+ */
399
+ getJob: (id, params = {}) => this.http.request({
400
+ path: `/jobs/${id}`,
401
+ method: "GET",
402
+ secure: true,
403
+ format: "json",
404
+ ...params
405
+ }),
406
+ /**
407
+ * @description Update a job
408
+ *
409
+ * @tags Jobs
410
+ * @name UpdateJob
411
+ * @request PATCH:/jobs/{id}
412
+ * @secure
413
+ */
414
+ updateJob: (id, data, params = {}) => this.http.request({
415
+ path: `/jobs/${id}`,
416
+ method: "PATCH",
417
+ body: data,
418
+ secure: true,
419
+ type: "application/json" /* Json */,
420
+ format: "json",
421
+ ...params
422
+ }),
423
+ /**
424
+ * @description Delete a job
425
+ *
426
+ * @tags Jobs
427
+ * @name DeleteJob
428
+ * @request DELETE:/jobs/{id}
429
+ * @secure
430
+ */
431
+ deleteJob: (id, params = {}) => this.http.request({
432
+ path: `/jobs/${id}`,
433
+ method: "DELETE",
434
+ secure: true,
435
+ format: "json",
436
+ ...params
437
+ }),
438
+ /**
439
+ * @description Get all jobs
440
+ *
441
+ * @tags Jobs
442
+ * @name GetJobs
443
+ * @request GET:/jobs
444
+ * @secure
445
+ */
446
+ getJobs: (params = {}) => this.http.request({
447
+ path: `/jobs`,
448
+ method: "GET",
449
+ secure: true,
450
+ format: "json",
451
+ ...params
452
+ }),
453
+ /**
454
+ * @description Create a new job
455
+ *
456
+ * @tags Jobs
457
+ * @name CreateJob
458
+ * @request POST:/jobs/create
459
+ */
460
+ createJob: (data, params = {}) => this.http.request({
461
+ path: `/jobs/create`,
462
+ method: "POST",
463
+ body: data,
464
+ type: "application/json" /* Json */,
465
+ format: "json",
466
+ ...params
467
+ }),
468
+ /**
469
+ * @description Start a job
470
+ *
471
+ * @tags Jobs
472
+ * @name StartJob
473
+ * @request POST:/jobs/{id}/start
474
+ * @secure
475
+ */
476
+ startJob: (id, params = {}) => this.http.request({
477
+ path: `/jobs/${id}/start`,
478
+ method: "POST",
479
+ secure: true,
480
+ format: "json",
481
+ ...params
482
+ }),
483
+ /**
484
+ * @description Stop a job
485
+ *
486
+ * @tags Jobs
487
+ * @name StopJob
488
+ * @request POST:/jobs/{id}/stop
489
+ * @secure
490
+ */
491
+ stopJob: (id, params = {}) => this.http.request({
492
+ path: `/jobs/${id}/stop`,
493
+ method: "POST",
494
+ secure: true,
495
+ format: "json",
496
+ ...params
497
+ }),
498
+ /**
499
+ * @description Reset a job
500
+ *
501
+ * @tags Jobs
502
+ * @name ResetJob
503
+ * @request POST:/jobs/{id}/reset
504
+ * @secure
505
+ */
506
+ resetJob: (id, params = {}) => this.http.request({
507
+ path: `/jobs/${id}/reset`,
508
+ method: "POST",
509
+ secure: true,
510
+ format: "json",
511
+ ...params
512
+ }),
513
+ /**
514
+ * @description Archive a job
515
+ *
516
+ * @tags Jobs
517
+ * @name ArchiveJob
518
+ * @request POST:/jobs/{id}/archive
519
+ * @secure
520
+ */
521
+ archiveJob: (id, params = {}) => this.http.request({
522
+ path: `/jobs/${id}/archive`,
523
+ method: "POST",
524
+ secure: true,
525
+ format: "json",
526
+ ...params
527
+ }),
528
+ /**
529
+ * @description Update the pool of a job
530
+ *
531
+ * @tags Jobs
532
+ * @name UpdateJobPool
533
+ * @request PATCH:/jobs/{id}/pool
534
+ * @secure
535
+ */
536
+ updateJobPool: (id, data, params = {}) => this.http.request({
537
+ path: `/jobs/${id}/pool`,
538
+ method: "PATCH",
539
+ body: data,
540
+ secure: true,
541
+ type: "application/json" /* Json */,
542
+ format: "json",
543
+ ...params
544
+ })
545
+ };
546
+ this.info = {
547
+ /**
548
+ * @description Get information about the currently running RenderFlow instance
549
+ *
550
+ * @tags Info
551
+ * @name GetInfo
552
+ * @request GET:/info
553
+ * @secure
554
+ */
555
+ getInfo: (params = {}) => this.http.request({
556
+ path: `/info`,
557
+ method: "GET",
558
+ secure: true,
559
+ format: "json",
560
+ ...params
561
+ })
562
+ };
563
+ this.icons = {
564
+ /**
565
+ * @description Get job icon
566
+ *
567
+ * @tags Icons
568
+ * @name GetJobIcon
569
+ * @request GET:/icons/jobs/{type}
570
+ * @secure
571
+ */
572
+ getJobIcon: (type, params = {}) => this.http.request({
573
+ path: `/icons/jobs/${type}`,
574
+ method: "GET",
575
+ secure: true,
576
+ ...params
577
+ }),
578
+ /**
579
+ * @description Get software icon
580
+ *
581
+ * @tags Icons
582
+ * @name GetSoftwareIcon
583
+ * @request GET:/icons/software/{id}
584
+ * @secure
585
+ */
586
+ getSoftwareIcon: (id, params = {}) => this.http.request({
587
+ path: `/icons/software/${id}`,
588
+ method: "GET",
589
+ secure: true,
590
+ ...params
591
+ })
592
+ };
593
+ this.errors = {
594
+ /**
595
+ * @description Get all errors
596
+ *
597
+ * @tags Errors
598
+ * @name GetErrors
599
+ * @request GET:/errors
600
+ * @secure
601
+ */
602
+ getErrors: (params = {}) => this.http.request({
603
+ path: `/errors`,
604
+ method: "GET",
605
+ secure: true,
606
+ format: "json",
607
+ ...params
608
+ }),
609
+ /**
610
+ * @description Get errors by job id
611
+ *
612
+ * @tags Errors
613
+ * @name GetErrorsByJobId
614
+ * @request GET:/errors/job/{id}
615
+ * @secure
616
+ */
617
+ getErrorsByJobId: (id, params = {}) => this.http.request({
618
+ path: `/errors/job/${id}`,
619
+ method: "GET",
620
+ secure: true,
621
+ format: "json",
622
+ ...params
623
+ }),
624
+ /**
625
+ * @description Get errors by node id
626
+ *
627
+ * @tags Errors
628
+ * @name GetErrorsByNodeId
629
+ * @request GET:/errors/node/{id}
630
+ * @secure
631
+ */
632
+ getErrorsByNodeId: (id, params = {}) => this.http.request({
633
+ path: `/errors/node/${id}`,
634
+ method: "GET",
635
+ secure: true,
636
+ format: "json",
637
+ ...params
638
+ })
639
+ };
640
+ this.http = http;
641
+ }
642
+ };
643
+
644
+ // src/index.ts
645
+ function parseEvent(raw) {
646
+ return {
647
+ type: raw.operationType,
648
+ document: raw.fullDocument ?? {},
649
+ updated: raw.updateDescription?.updatedFields,
650
+ time: raw.wallTime ?? 0
651
+ };
652
+ }
653
+ var EventListener = class {
654
+ constructor(url, headers, onEvent, onError) {
655
+ this._closed = false;
656
+ this._controller = new AbortController();
657
+ this._connect(url, headers, onEvent, onError);
658
+ }
659
+ async _connect(url, headers, onEvent, onError) {
660
+ try {
661
+ const response = await axios2.get(url, {
662
+ headers: { ...headers, Accept: "text/event-stream" },
663
+ responseType: "stream",
664
+ signal: this._controller.signal
665
+ });
666
+ let buffer = "";
667
+ response.data.on("data", (chunk) => {
668
+ buffer += chunk.toString();
669
+ const parts = buffer.split("\n\n");
670
+ buffer = parts.pop() || "";
671
+ for (const part of parts) {
672
+ const line = part.trim();
673
+ if (!line || line.startsWith(":")) continue;
674
+ const dataPrefix = "data: ";
675
+ const dataLine = line.split("\n").find((l) => l.startsWith(dataPrefix));
676
+ if (!dataLine) continue;
677
+ try {
678
+ const raw = JSON.parse(dataLine.slice(dataPrefix.length));
679
+ onEvent(parseEvent(raw));
680
+ } catch {
681
+ }
682
+ }
683
+ });
684
+ response.data.on("error", (err) => {
685
+ if (!this._closed && onError) {
686
+ onError(err);
687
+ }
688
+ });
689
+ } catch (err) {
690
+ if (!this._closed && onError) {
691
+ onError(err instanceof Error ? err : new Error(String(err)));
692
+ }
693
+ }
694
+ }
695
+ close() {
696
+ this._closed = true;
697
+ this._controller.abort();
698
+ }
699
+ };
700
+ var RenderFlow = class {
701
+ constructor(options = {}) {
702
+ const { baseUrl = "http://localhost:44442/api/v1", apiKey, timeout = 3e4 } = options;
703
+ this._baseUrl = baseUrl;
704
+ this._headers = apiKey ? { "x-renderflow-api-key": apiKey } : {};
705
+ const httpClient = new HttpClient({
706
+ baseURL: baseUrl,
707
+ timeout,
708
+ headers: this._headers
709
+ });
710
+ const api = new Api(httpClient);
711
+ const self = this;
712
+ this.info = {
713
+ get: () => api.info.getInfo()
714
+ };
715
+ this.jobs = {
716
+ list: () => api.jobs.getJobs(),
717
+ get: (id) => api.jobs.getJob(id),
718
+ create: (data) => api.jobs.createJob(data),
719
+ update: (id, data) => api.jobs.updateJob(id, data),
720
+ delete: (id) => api.jobs.deleteJob(id),
721
+ start: (id) => api.jobs.startJob(id),
722
+ stop: (id) => api.jobs.stopJob(id),
723
+ reset: (id) => api.jobs.resetJob(id),
724
+ archive: (id) => api.jobs.archiveJob(id),
725
+ updatePool: (id, poolId) => api.jobs.updateJobPool(id, { poolId }),
726
+ on: (callback, onError) => {
727
+ return new EventListener(`${self._baseUrl}/jobs/events`, self._headers, callback, onError);
728
+ }
729
+ };
730
+ this.tasks = {
731
+ list: (jobId, page = 1, limit = 20) => api.tasks.getTasks({ jobId, pageInput: page, limit }),
732
+ get: (id) => api.tasks.getTask(id),
733
+ logs: (id, offset = 0, limit = 200) => api.tasks.getTaskLogs(id, { offset, limit }),
734
+ thumbnail: (id) => api.tasks.getThumbnail(id),
735
+ on: (jobId, callback, onError) => {
736
+ return new EventListener(`${self._baseUrl}/tasks/events/${jobId}`, self._headers, callback, onError);
737
+ }
738
+ };
739
+ this.nodes = {
740
+ list: () => api.nodes.getNodes(),
741
+ get: (id) => api.nodes.getNode(id),
742
+ delete: (id) => api.nodes.deleteNode(id),
743
+ updateStatus: (id, status) => api.nodes.updateStatus(id, { status }),
744
+ updatePool: (id, poolId) => api.nodes.updateNodePool(id, { poolId }),
745
+ utilization: (id) => api.nodes.getNodeUtilization(id),
746
+ benchmarks: (type) => api.nodes.getNodeBenchmarkRanking({ type }),
747
+ on: (callback, onError) => {
748
+ return new EventListener(`${self._baseUrl}/nodes/events`, self._headers, callback, onError);
749
+ }
750
+ };
751
+ this.pools = {
752
+ list: () => api.pools.getPools(),
753
+ get: (id) => api.pools.getPool(id)
754
+ };
755
+ this.users = {
756
+ list: () => api.users.getUsers(),
757
+ get: (id) => api.users.getUser(id)
758
+ };
759
+ this.errors = {
760
+ list: () => api.errors.getErrors(),
761
+ byJob: (id) => api.errors.getErrorsByJobId(id),
762
+ byNode: (id) => api.errors.getErrorsByNodeId(id)
763
+ };
764
+ }
765
+ };
766
+ export {
767
+ EventListener,
768
+ RenderFlow
769
+ };
770
+ /**
771
+ * @title RenderFlow API Docs
772
+ * @version 1.0
773
+ * @license UNLICENSED
774
+ * @baseUrl /api/v1
775
+ * @contact Pulze
776
+ */