@norskvideo/norsk-manager-sdk 1.0.356

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/src/sdk.ts ADDED
@@ -0,0 +1,2423 @@
1
+ import * as grpc from "@grpc/grpc-js";
2
+ import { Empty, Timestamp } from "@bufbuild/protobuf";
3
+ import {
4
+ Version as CommonVersion,
5
+ Log_Level,
6
+ } from "@norskvideo/norsk-api/lib/shared/common_pb";
7
+ import { ManagerClient } from "@norskvideo/norsk-api/lib/manager_grpc_pb";
8
+ import * as CommonPB from "@norskvideo/norsk-api/lib/shared/common_pb";
9
+ import * as ManagerPB from "@norskvideo/norsk-api/lib/manager_pb";
10
+ import { debuglog, provideFull } from "./shared/utils";
11
+ import * as utils from "./shared/utils";
12
+ import * as util from "util";
13
+ import { SurfaceCall } from "@grpc/grpc-js/build/src/call";
14
+
15
+ // TODOs
16
+ //
17
+ // manager will need public & private ports to listen for grpc traffic
18
+ // - public port will need client-cert provided - all startup config (until we do real saas)
19
+ //
20
+
21
+ /** @public */
22
+ export type JobId = string;
23
+
24
+ /** @internal */
25
+ function toJobId(jobId: JobId): CommonPB.JobId {
26
+ return provideFull(CommonPB.JobId, { jobId: jobId });
27
+ }
28
+
29
+ /** @internal */
30
+ function fromJobId(jobId: CommonPB.JobId): JobId {
31
+ return jobId.jobId;
32
+ }
33
+
34
+ /** @public */
35
+ export type Role = string;
36
+
37
+ /** @public */
38
+ export type ContainerUrl = string;
39
+
40
+ /** @internal */
41
+ function toContainerUrl(url: ContainerUrl): CommonPB.ContainerUrl {
42
+ return provideFull(CommonPB.ContainerUrl, {
43
+ url,
44
+ });
45
+ }
46
+
47
+ /** @internal */
48
+ function fromContainerUrl(containerUrl: CommonPB.ContainerUrl): ContainerUrl {
49
+ return containerUrl.url;
50
+ }
51
+
52
+ /** @public */
53
+ export type DockerContainer = { codeType: "docker"; url: ContainerUrl };
54
+
55
+ /** @internal */
56
+ function toDockerContainer(
57
+ container: DockerContainer
58
+ ): CommonPB.DockerContainer {
59
+ return provideFull(CommonPB.DockerContainer, {
60
+ containerUrl: toContainerUrl(container.url),
61
+ });
62
+ }
63
+
64
+ /** @internal */
65
+ function fromDockerContainer(
66
+ container: CommonPB.DockerContainer
67
+ ): DockerContainer {
68
+ return {
69
+ codeType: "docker",
70
+ url: fromContainerUrl(utils.mandatory(container.containerUrl)),
71
+ };
72
+ }
73
+
74
+ /** @public */
75
+ export type ClientCode = DockerContainer;
76
+
77
+ /** @internal */
78
+ export type ClientCodePB = CommonPB.Service["container"];
79
+
80
+ /** @internal */
81
+ function toClientCode(clientCode: ClientCode): ClientCodePB {
82
+ switch (clientCode.codeType) {
83
+ case "docker":
84
+ return utils.mkCase({ docker: toDockerContainer(clientCode) });
85
+ }
86
+ }
87
+
88
+ /** @internal */
89
+ function fromClientCode(clientCode: ClientCodePB): ClientCode {
90
+ switch (clientCode.case) {
91
+ case "docker":
92
+ return fromDockerContainer(clientCode.value);
93
+ case undefined:
94
+ throw new Error();
95
+ }
96
+ }
97
+
98
+ /** @public */
99
+ export type JobHistoryJobCreated = {
100
+ event: "created";
101
+ timestamp: Date;
102
+ };
103
+
104
+ /** @public */
105
+ export type JobHistoryJobUpdated = {
106
+ event: "updated";
107
+ timestamp: Date;
108
+ previousJob: Job;
109
+ };
110
+
111
+ /** @public */
112
+ export type JobHistoryJobProvisioned = {
113
+ event: "provisioned";
114
+ timestamp: Date;
115
+ role: Role;
116
+ nodeMetadata: NodeMetadata;
117
+ };
118
+
119
+ /** @public */
120
+ export type JobHistoryJobRunning = {
121
+ event: "running";
122
+ timestamp: Date;
123
+ role: Role;
124
+ nodeMetadata: NodeMetadata;
125
+ runningNodeMetadata: RunningNodeMetadata;
126
+ };
127
+
128
+ /** @public */
129
+ export type JobHistoryJobStopping = {
130
+ event: "stopping";
131
+ timestamp: Date;
132
+ role: Role;
133
+ nodeMetadata: NodeMetadata;
134
+ runningNodeMetadata?: RunningNodeMetadata;
135
+ reason:
136
+ | "nodeStopped"
137
+ | "nodeTerminated"
138
+ | "userRequested"
139
+ | "unknownJob"
140
+ | "jobFailed";
141
+ };
142
+
143
+ /** @public */
144
+ export type JobHistoryJobStopped = {
145
+ event: "stopped";
146
+ timestamp: Date;
147
+ role: Role;
148
+ nodeMetadata: NodeMetadata;
149
+ runningNodeMetadata?: RunningNodeMetadata;
150
+ };
151
+
152
+ /** @public */
153
+ export type JobHistoryJobCompleted = {
154
+ event: "completed";
155
+ timestamp: Date;
156
+ };
157
+
158
+ /** @public */
159
+ export type JobHistoryEntry =
160
+ | JobHistoryJobCreated
161
+ | JobHistoryJobUpdated
162
+ | JobHistoryJobProvisioned
163
+ | JobHistoryJobRunning
164
+ | JobHistoryJobStopping
165
+ | JobHistoryJobStopped
166
+ | JobHistoryJobCompleted;
167
+
168
+ /** @public */
169
+ export type JobState = "pre" | "active" | "post";
170
+
171
+ /** @public */
172
+ export type ServiceRestart = "never" | "onFailure" | "always";
173
+
174
+ /** @public */
175
+ export type RestartIntensity = {
176
+ count: number;
177
+ period: number;
178
+ };
179
+
180
+ /** @public */
181
+ export type EphemeralHostPort = {
182
+ type: "ephemeral";
183
+ };
184
+
185
+ /** @public */
186
+ export type RangeHostPort = {
187
+ type: "range";
188
+ minPort: number;
189
+ maxPort: number;
190
+ };
191
+
192
+ /** @public */
193
+ export type SpecificHostPort = {
194
+ type: "specific";
195
+ port: number;
196
+ };
197
+
198
+ /** @public */
199
+ export type HostPort = EphemeralHostPort | RangeHostPort | SpecificHostPort;
200
+
201
+ /** @public */
202
+ export type PortMapping = {
203
+ containerPort: number;
204
+ protocol?: "tcp" | "udp";
205
+ hostPort: HostPort;
206
+ hostAddress?: string;
207
+ };
208
+
209
+ /** @public */
210
+ export type EnvironmentVariable = {
211
+ name: string;
212
+ value: string;
213
+ };
214
+
215
+ /** @public */
216
+ export type VolumeMount = {
217
+ volumeName: string;
218
+ containerPath: string;
219
+ readOnly: boolean;
220
+ };
221
+
222
+ /** @public */
223
+ export type ServiceParameters = {
224
+ portMappings?: PortMapping[];
225
+ environmentVariables?: EnvironmentVariable[];
226
+ sharedMemorySize?: bigint;
227
+ cpuSet?: number[];
228
+ volumeMounts?: VolumeMount[];
229
+ };
230
+
231
+ /** @public */
232
+ export type Service = {
233
+ name: string;
234
+ container: ClientCode;
235
+ restart?: ServiceRestart;
236
+ dependsOn?: Set<string>;
237
+ configuration?: string;
238
+ restartIntensity?: RestartIntensity;
239
+ command?: string;
240
+ parameters?: ServiceParameters;
241
+ };
242
+
243
+ /** @public */
244
+ export type Job = {
245
+ jobId: JobId;
246
+ cloud: Cloud;
247
+ description: string;
248
+ tags: Map<string, string>;
249
+ startDateTime: Date;
250
+ currentHash: bigint;
251
+ services: Service[];
252
+ volumes?: string[];
253
+ managerConfiguration?: string;
254
+ norskMediaVersion: Version;
255
+ norskServiceParameters?: ServiceParameters;
256
+ state: JobState;
257
+ shape: string;
258
+ availabilityDomain: string;
259
+ subnet: string;
260
+ architecture: string;
261
+ };
262
+
263
+ /** @public */
264
+ export type CreateJob = Omit<Job, "state" | "currentHash">;
265
+
266
+ /** @public */
267
+ export type JobWithHistory = {
268
+ job: Job;
269
+ history: JobHistoryEntry[];
270
+ };
271
+
272
+ /** @internal */
273
+ function toJobState(state: JobState): ManagerPB.Job_JobState {
274
+ switch (state) {
275
+ case "pre":
276
+ return ManagerPB.Job_JobState.PRE;
277
+ case "active":
278
+ return ManagerPB.Job_JobState.ACTIVE;
279
+ case "post":
280
+ return ManagerPB.Job_JobState.POST;
281
+ }
282
+ }
283
+
284
+ /** @internal */
285
+ function toPortMapping({
286
+ containerPort,
287
+ protocol,
288
+ hostPort,
289
+ hostAddress,
290
+ }: PortMapping): CommonPB.PortMapping {
291
+ var protocolPB: CommonPB.ProtocolFamily;
292
+ var hostPortPB: CommonPB.PortMapping["hostPort"];
293
+ switch (protocol) {
294
+ case "tcp":
295
+ protocolPB = CommonPB.ProtocolFamily.TCP;
296
+ break;
297
+ case "udp":
298
+ protocolPB = CommonPB.ProtocolFamily.UDP;
299
+ break;
300
+ case undefined:
301
+ protocolPB = CommonPB.ProtocolFamily.ANY;
302
+ break;
303
+ }
304
+ switch (hostPort.type) {
305
+ case "ephemeral":
306
+ hostPortPB = {
307
+ case: "ephemeral",
308
+ value: provideFull(CommonPB.HostPortEphemeral, {}),
309
+ };
310
+ break;
311
+ case "range":
312
+ hostPortPB = {
313
+ case: "range",
314
+ value: provideFull(CommonPB.HostPortRange, {
315
+ minPort: hostPort.minPort,
316
+ maxPort: hostPort.maxPort,
317
+ }),
318
+ };
319
+ break;
320
+ case "specific":
321
+ hostPortPB = {
322
+ case: "specific",
323
+ value: provideFull(CommonPB.HostPortSpecific, {
324
+ port: hostPort.port,
325
+ }),
326
+ };
327
+ break;
328
+ }
329
+ return provideFull(CommonPB.PortMapping, {
330
+ containerPort,
331
+ protocol: protocolPB,
332
+ hostPort: hostPortPB,
333
+ hostAddress: hostAddress || "",
334
+ });
335
+ }
336
+
337
+ /** @internal */
338
+ function fromPortMapping({
339
+ containerPort,
340
+ protocol: protocolPB,
341
+ hostPort: hostPortPB,
342
+ hostAddress,
343
+ }: CommonPB.PortMapping): PortMapping {
344
+ var protocol: PortMapping["protocol"];
345
+ var hostPort: HostPort;
346
+ switch (protocolPB) {
347
+ case CommonPB.ProtocolFamily.TCP:
348
+ protocol = "tcp";
349
+ break;
350
+ case CommonPB.ProtocolFamily.UDP:
351
+ protocol = "udp";
352
+ break;
353
+ case CommonPB.ProtocolFamily.ANY:
354
+ protocol = undefined;
355
+ break;
356
+ }
357
+ switch (hostPortPB.case) {
358
+ case "ephemeral":
359
+ hostPort = { type: "ephemeral" };
360
+ break;
361
+ case "range":
362
+ hostPort = {
363
+ type: "range",
364
+ minPort: hostPortPB.value.minPort,
365
+ maxPort: hostPortPB.value.maxPort,
366
+ };
367
+ break;
368
+ case "specific":
369
+ hostPort = {
370
+ type: "specific",
371
+ port: hostPortPB.value.port,
372
+ };
373
+ break;
374
+ case undefined:
375
+ hostPort = { type: "ephemeral" };
376
+ break;
377
+ }
378
+ return {
379
+ containerPort,
380
+ protocol,
381
+ hostPort,
382
+ hostAddress: hostAddress || "",
383
+ };
384
+ }
385
+
386
+ /** @internal */
387
+ function toEnvironmentVariable(
388
+ env: EnvironmentVariable
389
+ ): CommonPB.EnvironmentVariable {
390
+ return provideFull(CommonPB.EnvironmentVariable, env);
391
+ }
392
+
393
+ /** @internal */
394
+ function fromEnvironmentVariable({
395
+ name,
396
+ value,
397
+ }: CommonPB.EnvironmentVariable): EnvironmentVariable {
398
+ return { name, value };
399
+ }
400
+
401
+ /** @internal */
402
+ function toServiceParameters({
403
+ portMappings,
404
+ environmentVariables,
405
+ sharedMemorySize,
406
+ cpuSet,
407
+ volumeMounts,
408
+ }: ServiceParameters): CommonPB.ServiceParameters {
409
+ return provideFull(CommonPB.ServiceParameters, {
410
+ portMappings: portMappings ? portMappings.map(toPortMapping) : [],
411
+ environmentVariables: environmentVariables
412
+ ? environmentVariables.map(toEnvironmentVariable)
413
+ : [],
414
+ sharedMemorySize: sharedMemorySize
415
+ ? provideFull(CommonPB.OptionalInt64, { value: sharedMemorySize })
416
+ : undefined,
417
+ cpuSet: cpuSet || [],
418
+ volumeMounts: volumeMounts
419
+ ? volumeMounts.map((v) => provideFull(CommonPB.VolumeMount, v))
420
+ : [],
421
+ });
422
+ }
423
+
424
+ /** @internal */
425
+ function fromServiceParameters({
426
+ portMappings,
427
+ environmentVariables,
428
+ sharedMemorySize,
429
+ cpuSet,
430
+ volumeMounts,
431
+ }: CommonPB.ServiceParameters): ServiceParameters {
432
+ return {
433
+ portMappings: portMappings.map(fromPortMapping),
434
+ environmentVariables: environmentVariables.map(fromEnvironmentVariable),
435
+ sharedMemorySize: sharedMemorySize ? sharedMemorySize.value : undefined,
436
+ cpuSet,
437
+ volumeMounts,
438
+ };
439
+ }
440
+
441
+ /** @internal */
442
+ function toService({
443
+ name,
444
+ container,
445
+ restart,
446
+ dependsOn,
447
+ configuration,
448
+ restartIntensity,
449
+ command,
450
+ parameters,
451
+ }: Service): CommonPB.Service {
452
+ var restartPB;
453
+ switch (restart) {
454
+ case "never":
455
+ restartPB = CommonPB.Service_ServiceRestart.NEVER;
456
+ break;
457
+ case "onFailure":
458
+ restartPB = CommonPB.Service_ServiceRestart.ON_FAILURE;
459
+ break;
460
+ case "always":
461
+ restartPB = CommonPB.Service_ServiceRestart.ALWAYS;
462
+ break;
463
+ case undefined:
464
+ restartPB = CommonPB.Service_ServiceRestart.ALWAYS;
465
+ break;
466
+ }
467
+ return provideFull(CommonPB.Service, {
468
+ name,
469
+ container: toClientCode(container),
470
+ restart: restartPB,
471
+ dependsOn: Array.from(dependsOn?.keys() || []),
472
+ configuration: configuration || "",
473
+ restartCount: restartIntensity ? restartIntensity.count : 5,
474
+ restartPeriod: restartIntensity ? restartIntensity.period : 1000,
475
+ command: command || "",
476
+ parameters: parameters ? toServiceParameters(parameters) : undefined,
477
+ });
478
+ }
479
+
480
+ /** @internal */
481
+ function fromService({
482
+ name,
483
+ container,
484
+ restart: restartPB,
485
+ dependsOn: dependsOnPB,
486
+ configuration,
487
+ restartCount,
488
+ restartPeriod,
489
+ command,
490
+ parameters,
491
+ }: CommonPB.Service): Service {
492
+ var restart: ServiceRestart;
493
+ switch (restartPB) {
494
+ case CommonPB.Service_ServiceRestart.NEVER:
495
+ restart = "never";
496
+ break;
497
+ case CommonPB.Service_ServiceRestart.ON_FAILURE:
498
+ restart = "onFailure";
499
+ break;
500
+ case CommonPB.Service_ServiceRestart.ALWAYS:
501
+ restart = "always";
502
+ break;
503
+ }
504
+ const dependsOn: Set<string> = dependsOnPB.reduce(
505
+ (acc, val) => acc.add(val),
506
+ new Set<string>()
507
+ );
508
+ return {
509
+ name,
510
+ container: fromClientCode(container),
511
+ restart,
512
+ dependsOn,
513
+ configuration,
514
+ restartIntensity: { count: restartCount, period: restartPeriod },
515
+ command,
516
+ parameters: parameters ? fromServiceParameters(parameters) : undefined,
517
+ };
518
+ }
519
+
520
+ /** @internal */
521
+ function toJob({
522
+ jobId,
523
+ description,
524
+ tags,
525
+ startDateTime,
526
+ currentHash = BigInt(0),
527
+ services,
528
+ volumes,
529
+ managerConfiguration,
530
+ norskMediaVersion,
531
+ norskServiceParameters,
532
+ state,
533
+ shape,
534
+ availabilityDomain,
535
+ subnet,
536
+ architecture,
537
+ }: Job): ManagerPB.Job {
538
+ return provideFull(ManagerPB.Job, {
539
+ jobId: toJobId(jobId),
540
+ description: description,
541
+ tags: Object.fromEntries(tags.entries()),
542
+ startDateTime: Timestamp.fromDate(startDateTime),
543
+ currentHash: currentHash,
544
+ services: services.map(toService),
545
+ volumes: volumes || [],
546
+ managerConfiguration: managerConfiguration || "",
547
+ norskMediaVersion: toVersion(norskMediaVersion),
548
+ norskServiceParameters: norskServiceParameters
549
+ ? toServiceParameters(norskServiceParameters)
550
+ : undefined,
551
+ state: toJobState(state),
552
+ shape,
553
+ availabilityDomain,
554
+ subnet,
555
+ architecture
556
+ });
557
+ }
558
+
559
+ /** @internal */
560
+ function fromJobState(state: ManagerPB.Job_JobState): JobState {
561
+ switch (state) {
562
+ case ManagerPB.Job_JobState.PRE:
563
+ return "pre";
564
+ case ManagerPB.Job_JobState.ACTIVE:
565
+ return "active";
566
+ case ManagerPB.Job_JobState.POST:
567
+ return "post";
568
+ }
569
+ }
570
+
571
+ /** @internal */
572
+ function fromJob(job: ManagerPB.Job): Job {
573
+ return {
574
+ jobId: fromJobId(utils.mandatory(job.jobId)),
575
+ cloud: job.architecture == "" ? "aws" : "oci",
576
+ description: job.description,
577
+ tags: new Map(Object.entries(job.tags)),
578
+ startDateTime: utils.mandatory(job.startDateTime).toDate(),
579
+ currentHash: job.currentHash,
580
+ services: job.services.map(fromService),
581
+ volumes: job.volumes,
582
+ managerConfiguration: job.managerConfiguration,
583
+ norskMediaVersion: fromVersion(job.norskMediaVersion),
584
+ norskServiceParameters: job.norskServiceParameters
585
+ ? fromServiceParameters(job.norskServiceParameters)
586
+ : undefined,
587
+ state: fromJobState(job.state),
588
+ shape: job.shape,
589
+ availabilityDomain: job.availabilityDomain,
590
+ subnet: job.subnet,
591
+ architecture: job.architecture
592
+ };
593
+ }
594
+
595
+ /** @internal */
596
+ function fromJobHistoryJobCreated(
597
+ entry: ManagerPB.JobHistoryJobCreated
598
+ ): JobHistoryJobCreated {
599
+ return {
600
+ event: "created",
601
+ timestamp: utils.mandatory(entry.timestamp).toDate(),
602
+ };
603
+ }
604
+
605
+ /** @internal */
606
+ function fromJobHistoryJobUpdated(
607
+ entry: ManagerPB.JobHistoryJobUpdated
608
+ ): JobHistoryJobUpdated {
609
+ return {
610
+ event: "updated",
611
+ timestamp: utils.mandatory(entry.timestamp).toDate(),
612
+ previousJob: fromJob(utils.mandatory(entry.previousJob)),
613
+ };
614
+ }
615
+
616
+ /** @internal */
617
+ function fromJobHistoryJobProvisioned(
618
+ entry: ManagerPB.JobHistoryJobProvisioned
619
+ ): JobHistoryJobProvisioned {
620
+ return {
621
+ event: "provisioned",
622
+ timestamp: utils.mandatory(entry.timestamp).toDate(),
623
+ role: entry.role,
624
+ nodeMetadata: fromNodeMetadata(utils.mandatory(entry.nodeMetadata)),
625
+ };
626
+ }
627
+
628
+ /** @internal */
629
+ function fromJobHistoryJobRunning(
630
+ entry: ManagerPB.JobHistoryJobRunning
631
+ ): JobHistoryJobRunning {
632
+ return {
633
+ event: "running",
634
+ timestamp: utils.mandatory(entry.timestamp).toDate(),
635
+ role: entry.role,
636
+ nodeMetadata: fromNodeMetadata(utils.mandatory(entry.nodeMetadata)),
637
+ runningNodeMetadata: fromRunningNodeMetadata(
638
+ utils.mandatory(entry.runningNodeMetadata)
639
+ ),
640
+ };
641
+ }
642
+
643
+ /** @internal */
644
+ function fromJobHistoryJobStopping(
645
+ entry: ManagerPB.JobHistoryJobStopping
646
+ ): JobHistoryJobStopping {
647
+ let reason: JobHistoryJobStopping["reason"];
648
+ switch (entry.reason) {
649
+ case ManagerPB.JobHistoryJobStopping_Reason.JOB_STOPPED_NODE_STOPPED:
650
+ reason = "nodeStopped";
651
+ break;
652
+ case ManagerPB.JobHistoryJobStopping_Reason.JOB_STOPPED_NODE_TERMINATED:
653
+ reason = "nodeTerminated";
654
+ break;
655
+ case ManagerPB.JobHistoryJobStopping_Reason.JOB_STOPPED_USER_REQUESTED:
656
+ reason = "userRequested";
657
+ break;
658
+ case ManagerPB.JobHistoryJobStopping_Reason.JOB_STOPPED_UNKNOWN_JOB:
659
+ reason = "unknownJob";
660
+ break;
661
+ case ManagerPB.JobHistoryJobStopping_Reason.JOB_STOPPED_JOB_FAILED:
662
+ reason = "jobFailed";
663
+ break;
664
+ default: {
665
+ const exhaustiveCheck: never = entry.reason;
666
+ throw new Error(`Unhandled case: ${exhaustiveCheck}`);
667
+ }
668
+ }
669
+ return {
670
+ event: "stopping",
671
+ timestamp: utils.mandatory(entry.timestamp).toDate(),
672
+ role: entry.role,
673
+ nodeMetadata: fromNodeMetadata(utils.mandatory(entry.nodeMetadata)),
674
+ runningNodeMetadata: utils.mapOptional(
675
+ fromRunningNodeMetadata,
676
+ entry.runningNodeMetadata
677
+ ),
678
+ reason: reason,
679
+ };
680
+ }
681
+
682
+ /** @internal */
683
+ function fromJobHistoryJobStopped(
684
+ entry: ManagerPB.JobHistoryJobStopped
685
+ ): JobHistoryJobStopped {
686
+ return {
687
+ event: "stopped",
688
+ timestamp: utils.mandatory(entry.timestamp).toDate(),
689
+ role: entry.role,
690
+ nodeMetadata: fromNodeMetadata(utils.mandatory(entry.nodeMetadata)),
691
+ runningNodeMetadata: utils.mapOptional(
692
+ fromRunningNodeMetadata,
693
+ entry.runningNodeMetadata
694
+ ),
695
+ };
696
+ }
697
+
698
+ /** @internal */
699
+ function fromJobHistoryJobCompleted(
700
+ entry: ManagerPB.JobHistoryJobCompleted
701
+ ): JobHistoryJobCompleted {
702
+ return {
703
+ event: "completed",
704
+ timestamp: utils.mandatory(entry.timestamp).toDate(),
705
+ };
706
+ }
707
+
708
+ /** @internal */
709
+ function fromJobHistoryEntry(
710
+ historyEntry: ManagerPB.JobHistoryEntry
711
+ ): JobHistoryEntry {
712
+ const event = historyEntry.historyEntry.case;
713
+
714
+ switch (event) {
715
+ case undefined:
716
+ throw Error();
717
+ case "jobCreated":
718
+ return fromJobHistoryJobCreated(historyEntry.historyEntry.value);
719
+ case "jobUpdated":
720
+ return fromJobHistoryJobUpdated(historyEntry.historyEntry.value);
721
+ case "jobProvisioned":
722
+ return fromJobHistoryJobProvisioned(historyEntry.historyEntry.value);
723
+ case "jobRunning":
724
+ return fromJobHistoryJobRunning(historyEntry.historyEntry.value);
725
+ case "jobStopping":
726
+ return fromJobHistoryJobStopping(historyEntry.historyEntry.value);
727
+ case "jobStopped":
728
+ return fromJobHistoryJobStopped(historyEntry.historyEntry.value);
729
+ case "jobCompleted":
730
+ return fromJobHistoryJobCompleted(historyEntry.historyEntry.value);
731
+ default: {
732
+ const exhaustiveCheck: never = event;
733
+ throw new Error(`Unhandled case: ${exhaustiveCheck}`);
734
+ }
735
+ }
736
+ }
737
+
738
+ /** @internal */
739
+ function fromJobWithHistory(
740
+ jobWithHistory: ManagerPB.JobWithHistory
741
+ ): JobWithHistory {
742
+ return {
743
+ job: fromJob(utils.mandatory(jobWithHistory.job)),
744
+ history: jobWithHistory.historyEntry.map(fromJobHistoryEntry),
745
+ };
746
+ }
747
+
748
+ /** @public */
749
+ export type AwsNodeId = string;
750
+
751
+ /** @public */
752
+ export type PhysicalNodeId = string;
753
+
754
+ /** @public */
755
+ export type NodeId = string;
756
+
757
+ /** @public */
758
+ export type Version = "latest" | "recommended" | "previousRecommended" | "LTS";
759
+
760
+ /** @public */
761
+ export type Cloud = "aws" | "oci";
762
+
763
+ /** @internal */
764
+ function toNodeId(nodeId: NodeId): CommonPB.NodeId {
765
+ return provideFull(CommonPB.NodeId, { nodeId: nodeId });
766
+ }
767
+
768
+ /** @internal */
769
+ function fromNodeId(nodeId: CommonPB.NodeId): NodeId {
770
+ return nodeId.nodeId;
771
+ }
772
+
773
+ /** @public */
774
+ export type AwsInstanceId = string;
775
+
776
+ /** @public */
777
+ export type AwsInstanceType = string;
778
+
779
+ /** @public */
780
+ export type AwsRegion = string;
781
+
782
+ export type OciRegion = string;
783
+
784
+ /** @public */
785
+ export class AwsLaunchTemplateName {
786
+ name: string;
787
+ version?: string;
788
+ }
789
+
790
+ /** @public */
791
+ export class AwsLaunchTemplateId {
792
+ id: string;
793
+ version?: string;
794
+ }
795
+
796
+ /** @public */
797
+ export type DaemonVersion = string;
798
+
799
+ /** @public */
800
+ export type AwsNodeCreate = {
801
+ nodeId: NodeId;
802
+ tags: Map<string, string>;
803
+ region: AwsRegion;
804
+ instanceType: string;
805
+ template?: AwsLaunchTemplateName | AwsLaunchTemplateId;
806
+ workerImageVersion?: Version;
807
+ workerDaemonVersion?: Version;
808
+ };
809
+
810
+ /** @public */
811
+ export type OciNodeCreate = {
812
+ nodeId: NodeId;
813
+ tags: Map<string, string>;
814
+ availabilityDomain: OciRegion;
815
+ architecture: string;
816
+ shape: string;
817
+ subnet: string;
818
+ workerImageVersion?: Version;
819
+ workerDaemonVersion?: Version;
820
+ };
821
+
822
+ function toVersion(version: Version): ManagerPB.Version {
823
+ switch (version) {
824
+ case "latest":
825
+ return ManagerPB.Version.LATEST;
826
+ case "recommended":
827
+ return ManagerPB.Version.RECOMMENDED;
828
+ case "previousRecommended":
829
+ return ManagerPB.Version.PREVIOUS_RECOMMENDED;
830
+ case "LTS":
831
+ return ManagerPB.Version.LTS;
832
+ }
833
+ }
834
+
835
+ function fromVersion(version: ManagerPB.Version): Version {
836
+ switch (version) {
837
+ case ManagerPB.Version.LATEST:
838
+ return "latest";
839
+ case ManagerPB.Version.RECOMMENDED:
840
+ return "recommended";
841
+ case ManagerPB.Version.PREVIOUS_RECOMMENDED:
842
+ return "previousRecommended";
843
+ case ManagerPB.Version.LTS:
844
+ return "LTS";
845
+ }
846
+ }
847
+
848
+ function toAwsLaunchTemplate(
849
+ template: AwsLaunchTemplateName | AwsLaunchTemplateId
850
+ ): ManagerPB.AwsLaunchTemplate {
851
+ if (template instanceof AwsLaunchTemplateName) {
852
+ return provideFull(ManagerPB.AwsLaunchTemplate, {
853
+ template: { value: template.name, case: "name" },
854
+ version: template.version || "",
855
+ });
856
+ } else {
857
+ return provideFull(ManagerPB.AwsLaunchTemplate, {
858
+ template: { value: template.id, case: "id" },
859
+ version: template.version || "",
860
+ });
861
+ }
862
+ }
863
+
864
+ function toAwsNodeCreate(node: AwsNodeCreate): ManagerPB.CreateAwsNodeRequest {
865
+ return provideFull(ManagerPB.CreateAwsNodeRequest, {
866
+ nodeId: toNodeId(node.nodeId),
867
+ tags: Object.fromEntries(node.tags.entries()),
868
+ region: node.region,
869
+ instanceType: node.instanceType,
870
+ workerImageVersion: toVersion(node.workerImageVersion || "recommended"),
871
+ workerDaemonVersion: toVersion(node.workerDaemonVersion || "recommended"),
872
+ template: utils.mapOptional(toAwsLaunchTemplate, node.template),
873
+ });
874
+ }
875
+
876
+ function toOciNodeCreate(node: OciNodeCreate): ManagerPB.CreateOciNodeRequest {
877
+ return provideFull(ManagerPB.CreateOciNodeRequest, {
878
+ nodeId: toNodeId(node.nodeId),
879
+ tags: Object.fromEntries(node.tags.entries()),
880
+ workerImageVersion: toVersion(node.workerImageVersion || "recommended"),
881
+ workerDaemonVersion: toVersion(node.workerDaemonVersion || "recommended"),
882
+ availabilityDomain: node.availabilityDomain,
883
+ architecture: node.architecture,
884
+ shape: node.shape,
885
+ subnet: node.subnet,
886
+ });
887
+ }
888
+
889
+ /** @internal */
890
+ function fromNodeMetadata(nodeMetadata: CommonPB.NodeMetadata): NodeMetadata {
891
+ return {
892
+ nodeId: fromNodeId(utils.mandatory(nodeMetadata.nodeId)),
893
+ tags: new Map(Object.entries(nodeMetadata.tags)),
894
+ createdAt: nodeMetadata.createdAt?.toDate(),
895
+ providerMetadata: fromProviderMetadata(
896
+ utils.mandatory(nodeMetadata.providerMetadata)
897
+ ),
898
+ // daemonVersion: awsNode.daemonVersion,
899
+ };
900
+ }
901
+
902
+ function fromAwsMetadata(nodeDetails: CommonPB.AwsMetadata): AwsMetadata {
903
+ return {
904
+ provider: "aws",
905
+ instanceType: nodeDetails.instanceType,
906
+ region: nodeDetails.region,
907
+ };
908
+ }
909
+
910
+ function fromOciMetadata(_nodeDetails: CommonPB.OciMetadata): OciMetadata {
911
+ return {
912
+ provider: "oci",
913
+ };
914
+ }
915
+
916
+ function fromProviderMetadata(
917
+ provider: CommonPB.ProviderMetadata
918
+ ): ProviderMetadata {
919
+ const providerType = provider.metadata.case;
920
+ switch (providerType) {
921
+ case undefined:
922
+ throw Error();
923
+ case "aws":
924
+ return fromAwsMetadata(provider.metadata.value);
925
+ case "oci":
926
+ return fromOciMetadata(provider.metadata.value);
927
+ case "test":
928
+ throw Error();
929
+ default: {
930
+ const exhaustiveCheck: never = providerType;
931
+ throw new Error(`Unhandled case: ${exhaustiveCheck}`);
932
+ }
933
+ }
934
+ }
935
+
936
+ function fromRunningAwsMetadata(
937
+ metadata: CommonPB.RunningAwsMetadata
938
+ ): RunningAwsMetadata {
939
+ return { provider: "aws", instanceId: metadata.instanceId };
940
+ }
941
+
942
+ function fromRunningOciMetadata(
943
+ _metadata: CommonPB.RunningOciMetadata
944
+ ): RunningOciMetadata {
945
+ return { provider: "oci" };
946
+ }
947
+
948
+ function fromRunningNodeProviderMetadata(
949
+ provider: CommonPB.RunningNodeProviderMetadata
950
+ ): RunningNodeProviderMetadata {
951
+ const providerType = provider.instance.case;
952
+ switch (providerType) {
953
+ case undefined:
954
+ throw Error();
955
+ case "aws":
956
+ return fromRunningAwsMetadata(provider.instance.value);
957
+ case "oci":
958
+ return fromRunningOciMetadata(provider.instance.value);
959
+ case "test":
960
+ throw Error();
961
+ default: {
962
+ const exhaustiveCheck: never = providerType;
963
+ throw new Error(`Unhandled case: ${exhaustiveCheck}`);
964
+ }
965
+ }
966
+ }
967
+
968
+ /** @public */
969
+ export type RunningAwsMetadata = { provider: "aws"; instanceId: AwsInstanceId };
970
+
971
+ /** @public */
972
+ export type RunningOciMetadata = { provider: "oci" };
973
+
974
+ /** @public */
975
+ export type RunningNodeProviderMetadata =
976
+ | RunningAwsMetadata
977
+ | RunningOciMetadata;
978
+
979
+ /** @public */
980
+ export type RunningNodeMetadata = {
981
+ publicDnsName?: string;
982
+ privateDnsName: string;
983
+ privateIpAddress: string;
984
+ providerMetadata: RunningNodeProviderMetadata;
985
+ };
986
+
987
+ function fromRunningNodeMetadata(
988
+ runningNodeMetadata: CommonPB.RunningNodeMetadata
989
+ ): RunningNodeMetadata {
990
+ return {
991
+ publicDnsName: runningNodeMetadata.publicDnsName,
992
+ privateDnsName: runningNodeMetadata.privateDnsName,
993
+ privateIpAddress: runningNodeMetadata.privateIpAddress,
994
+ providerMetadata: fromRunningNodeProviderMetadata(
995
+ utils.mandatory(runningNodeMetadata.providerMetadata)
996
+ ),
997
+ };
998
+ }
999
+
1000
+ /** @public */
1001
+ export type AwsMetadata = {
1002
+ provider: "aws";
1003
+ instanceType: AwsInstanceType;
1004
+ region: AwsRegion;
1005
+ };
1006
+
1007
+ /** @public */
1008
+ export type OciMetadata = {
1009
+ provider: "oci";
1010
+ };
1011
+
1012
+ /** @public */
1013
+ export type ProviderMetadata = AwsMetadata | OciMetadata;
1014
+
1015
+ /** @public */
1016
+ export type NodeMetadata = {
1017
+ nodeId: NodeId;
1018
+ tags: Map<string, string>;
1019
+ createdAt?: Date;
1020
+ providerMetadata: ProviderMetadata;
1021
+ };
1022
+
1023
+ /** @public */
1024
+ export type PhysicalNode = {
1025
+ nodeType: "physical";
1026
+ id: PhysicalNodeId;
1027
+ tags: Map<string, string>;
1028
+ ipAddress: string;
1029
+ };
1030
+
1031
+ /** @internal */
1032
+ function fromPhysicalNode(physicalNode: ManagerPB.PhysicalNode): PhysicalNode {
1033
+ return {
1034
+ nodeType: "physical",
1035
+ id: physicalNode.id,
1036
+ tags: new Map(Object.entries(physicalNode.tags)),
1037
+ ipAddress: physicalNode.ipAddress,
1038
+ };
1039
+ }
1040
+
1041
+ /** @public */
1042
+ export type Log = {
1043
+ level:
1044
+ | "emergency"
1045
+ | "alert"
1046
+ | "critical"
1047
+ | "error"
1048
+ | "warning"
1049
+ | "notice"
1050
+ | "info"
1051
+ | "debug";
1052
+ timestamp: Date;
1053
+ message: string;
1054
+ };
1055
+
1056
+ /**
1057
+ * @public
1058
+ * Top level Norsk configuration
1059
+ */
1060
+ export interface NorskSettings {
1061
+ /**
1062
+ * Callback URL to listen on for gRPC session with Norsk Media
1063
+ *
1064
+ */
1065
+ url?: string;
1066
+ onAttemptingToConnect?: () => void;
1067
+ onConnecting?: () => void;
1068
+ onReady?: () => void;
1069
+ onFailedToConnect?: () => void;
1070
+ onShutdown?: () => void;
1071
+ onHello?: (version: CommonVersion) => void;
1072
+ onLogEvent?: (log: Log) => void;
1073
+ }
1074
+
1075
+ /** @public */
1076
+ export type RunningJob = { jobId: JobId; role: Role; nodeId: NodeId };
1077
+
1078
+ /** @internal */
1079
+ function fromRunningJob(runningJob: ManagerPB.RunningJob): RunningJob {
1080
+ const key = utils.mandatory(runningJob.jobKey);
1081
+ return {
1082
+ jobId: fromJobId(utils.mandatory(key.jobId)),
1083
+ role: key.role,
1084
+ nodeId: fromNodeId(utils.mandatory(runningJob.nodeId)),
1085
+ };
1086
+ }
1087
+
1088
+ /**
1089
+ * @public
1090
+ * Job is almost ready to start - "almost" defined by property passed into
1091
+ * the list functions - todo, sort out this description
1092
+ * */
1093
+ export type JobPending = {
1094
+ event: "jobPending";
1095
+ jobWithHistory: JobWithHistory;
1096
+ };
1097
+
1098
+ /** @internal */
1099
+ function fromJobPending(event: ManagerPB.JobPending): JobPending | undefined {
1100
+ return utils.mapOptional(
1101
+ (jobWithHistory) => ({
1102
+ event: "jobPending",
1103
+ jobWithHistory: fromJobWithHistory(jobWithHistory),
1104
+ }),
1105
+ event.jobWithHistory
1106
+ );
1107
+ }
1108
+
1109
+ /**
1110
+ * @public
1111
+ * Job details have been updated
1112
+ * */
1113
+ export type JobUpdated = {
1114
+ event: "jobUpdated";
1115
+ jobWithHistory: JobWithHistory;
1116
+ };
1117
+
1118
+ /** @internal */
1119
+ function fromJobUpdated(event: ManagerPB.JobUpdated): JobUpdated | undefined {
1120
+ return utils.mapOptional(
1121
+ (jobWithHistory) => ({
1122
+ event: "jobUpdated",
1123
+ jobWithHistory: fromJobWithHistory(jobWithHistory),
1124
+ }),
1125
+ event.jobWithHistory
1126
+ );
1127
+ }
1128
+
1129
+ /**
1130
+ * @public
1131
+ * A job has been updated such that it no longer matches the search criteria
1132
+ * */
1133
+ export type JobOutOfWindow = {
1134
+ event: "jobOutOfWindow";
1135
+ jobWithHistory: JobWithHistory;
1136
+ };
1137
+
1138
+ /** @internal */
1139
+ function fromJobOutOfWindow(
1140
+ event: ManagerPB.JobOutOfWindow
1141
+ ): JobOutOfWindow | undefined {
1142
+ return utils.mapOptional(
1143
+ (jobWithHistory) => ({
1144
+ event: "jobOutOfWindow",
1145
+ jobWithHistory: fromJobWithHistory(jobWithHistory),
1146
+ }),
1147
+ event.jobWithHistory
1148
+ );
1149
+ }
1150
+
1151
+ /**
1152
+ * @public
1153
+ * A job has been deleted
1154
+ * */
1155
+ export type JobDeleted = {
1156
+ event: "jobDeleted";
1157
+ jobId: JobId;
1158
+ };
1159
+
1160
+ /** @public */
1161
+ type JobInfoServiceStarted = {
1162
+ type: "serviceStarted";
1163
+ serviceName: string;
1164
+ };
1165
+
1166
+ /** @public */
1167
+ type JobInfoServiceRestarting = {
1168
+ type: "serviceRestarting";
1169
+ serviceName: string;
1170
+ };
1171
+
1172
+ /** @public */
1173
+ type JobInfoContainerEvent = {
1174
+ type: "containerEvent";
1175
+ serviceName: string;
1176
+ text: string;
1177
+ };
1178
+
1179
+ /** @public */
1180
+ type JobInfoComposeStage = "build" | "create" | "start" | "stop" | "down";
1181
+
1182
+ /** @public */
1183
+ type JobInfoComponseMessage = {
1184
+ type: "composeMessage";
1185
+ stage: JobInfoComposeStage;
1186
+ text: string;
1187
+ };
1188
+
1189
+ /** @public */
1190
+ type JobInfoMessage =
1191
+ | JobInfoServiceStarted
1192
+ | JobInfoServiceRestarting
1193
+ | JobInfoContainerEvent
1194
+ | JobInfoComponseMessage;
1195
+
1196
+ /**
1197
+ * @public
1198
+ * Informational messages about a job
1199
+ * */
1200
+ export type JobInfo = {
1201
+ event: "jobInfo";
1202
+ jobId: JobId;
1203
+ role: Role;
1204
+ timestamp: Date;
1205
+ message: JobInfoMessage;
1206
+ };
1207
+
1208
+ /** @internal */
1209
+ function fromJobInfo(event: CommonPB.JobInfoMessage): JobInfo | undefined {
1210
+ var message: JobInfoMessage;
1211
+
1212
+ switch (event.message.case) {
1213
+ case "serviceStarted":
1214
+ message = {
1215
+ type: "serviceStarted",
1216
+ serviceName: event.message.value.serviceName,
1217
+ };
1218
+ break;
1219
+ case "serviceRestarting":
1220
+ message = {
1221
+ type: "serviceRestarting",
1222
+ serviceName: event.message.value.serviceName,
1223
+ };
1224
+ break;
1225
+ case "containerEvent":
1226
+ message = {
1227
+ type: "containerEvent",
1228
+ serviceName: event.message.value.serviceName,
1229
+ text: event.message.value.text,
1230
+ };
1231
+ break;
1232
+ case "composeMessage": {
1233
+ var stage: JobInfoComposeStage;
1234
+ switch (event.message.value.stage) {
1235
+ case CommonPB.JobInfoComponseMessage_Stage.BUILD:
1236
+ stage = "build";
1237
+ break;
1238
+ case CommonPB.JobInfoComponseMessage_Stage.CREATE:
1239
+ stage = "create";
1240
+ break;
1241
+ case CommonPB.JobInfoComponseMessage_Stage.START:
1242
+ stage = "start";
1243
+ break;
1244
+ case CommonPB.JobInfoComponseMessage_Stage.STOP:
1245
+ stage = "stop";
1246
+ break;
1247
+ case CommonPB.JobInfoComponseMessage_Stage.DOWN:
1248
+ stage = "down";
1249
+ break;
1250
+ }
1251
+ message = {
1252
+ type: "composeMessage",
1253
+ stage,
1254
+ text: event.message.value.text,
1255
+ };
1256
+ break;
1257
+ }
1258
+ case undefined:
1259
+ return undefined;
1260
+ }
1261
+ return {
1262
+ event: "jobInfo",
1263
+ jobId: fromJobId(utils.mandatory(event.jobKey?.jobId)),
1264
+ role: utils.mandatory(event.jobKey).role,
1265
+ timestamp: utils.mandatory(event.timestamp).toDate(),
1266
+ message,
1267
+ };
1268
+ }
1269
+
1270
+ /** @internal */
1271
+ function fromJobDeleted(event: ManagerPB.JobDeleted): JobDeleted | undefined {
1272
+ return utils.mapOptional(
1273
+ (jobId) => ({
1274
+ event: "jobDeleted",
1275
+ jobId: fromJobId(jobId),
1276
+ }),
1277
+ event.jobId
1278
+ );
1279
+ }
1280
+
1281
+ /**
1282
+ * @public
1283
+ * A node is starting
1284
+ * */
1285
+ export type NodeStarting = {
1286
+ event: "nodeStarting";
1287
+ nodeMetadata: NodeMetadata;
1288
+ };
1289
+
1290
+ /** @internal */
1291
+ function fromNodeStarting(
1292
+ event: ManagerPB.NodeStarting
1293
+ ): NodeStarting | undefined {
1294
+ return utils.mapOptional(
1295
+ (nodeMetadata) => ({
1296
+ event: "nodeStarting",
1297
+ nodeMetadata: fromNodeMetadata(nodeMetadata),
1298
+ }),
1299
+ event.nodeMetadata
1300
+ );
1301
+ }
1302
+
1303
+ /**
1304
+ * @public
1305
+ * A node has started
1306
+ * */
1307
+ export type NodeStarted = {
1308
+ event: "nodeStarted";
1309
+ nodeMetadata: NodeMetadata;
1310
+ runningNodeMetadata: RunningNodeMetadata;
1311
+ };
1312
+
1313
+ /** @internal */
1314
+ function fromNodeStarted(
1315
+ event: ManagerPB.NodeStarted
1316
+ ): NodeStarted | undefined {
1317
+ if (event.nodeMetadata && event.runningNodeMetadata) {
1318
+ return {
1319
+ event: "nodeStarted",
1320
+ nodeMetadata: fromNodeMetadata(event.nodeMetadata),
1321
+ runningNodeMetadata: fromRunningNodeMetadata(event.runningNodeMetadata),
1322
+ };
1323
+ }
1324
+ return undefined;
1325
+ }
1326
+
1327
+ /**
1328
+ * @public
1329
+ * A node is stopping
1330
+ * */
1331
+ export type NodeStopping = {
1332
+ event: "nodeStopping";
1333
+ nodeId: NodeId;
1334
+ reason:
1335
+ | "createFailed"
1336
+ | "pendingTimeExceeded"
1337
+ | "startupTimeExceeded"
1338
+ | "initialisationHealthPingTimeExceeded"
1339
+ | "healthPingTimeExceeded"
1340
+ | "duplicateJob"
1341
+ | "providerStop"
1342
+ | "providerTerminate"
1343
+ | "userRequested"
1344
+ | "unknownNode";
1345
+ };
1346
+
1347
+ /** @internal */
1348
+ function fromNodeStopping(
1349
+ event: ManagerPB.NodeStopping
1350
+ ): NodeStopping | undefined {
1351
+ let reason: NodeStopping["reason"];
1352
+ switch (event.reason) {
1353
+ case ManagerPB.NodeStopping_Reason.CREATE_FAILED:
1354
+ reason = "createFailed";
1355
+ break;
1356
+ case ManagerPB.NodeStopping_Reason.PENDING_TIME_EXCEEDED:
1357
+ reason = "pendingTimeExceeded";
1358
+ break;
1359
+ case ManagerPB.NodeStopping_Reason.STARTUP_TIME_EXCEEDED:
1360
+ reason = "startupTimeExceeded";
1361
+ break;
1362
+ case ManagerPB.NodeStopping_Reason.INITIALISATION_HEALTH_PING_TIME_EXCEEDED:
1363
+ reason = "initialisationHealthPingTimeExceeded";
1364
+ break;
1365
+ case ManagerPB.NodeStopping_Reason.HEALTH_PING_TIME_EXCEEDED:
1366
+ reason = "healthPingTimeExceeded";
1367
+ break;
1368
+ case ManagerPB.NodeStopping_Reason.DUPLICATE_JOB:
1369
+ reason = "duplicateJob";
1370
+ break;
1371
+ case ManagerPB.NodeStopping_Reason.PROVIDER_STOP:
1372
+ reason = "providerStop";
1373
+ break;
1374
+ case ManagerPB.NodeStopping_Reason.PROVIDER_TERMINATE:
1375
+ reason = "providerTerminate";
1376
+ break;
1377
+ case ManagerPB.NodeStopping_Reason.USER_REQUESTED:
1378
+ reason = "userRequested";
1379
+ break;
1380
+ case ManagerPB.NodeStopping_Reason.UNKNOWN_NODE:
1381
+ reason = "unknownNode";
1382
+ break;
1383
+
1384
+ default: {
1385
+ const exhaustiveCheck: never = event.reason;
1386
+ throw new Error(`Unhandled case: ${exhaustiveCheck}`);
1387
+ }
1388
+ }
1389
+ return utils.mapOptional(
1390
+ (nodeId) => ({
1391
+ event: "nodeStopping",
1392
+ nodeId: fromNodeId(nodeId),
1393
+ reason: reason,
1394
+ }),
1395
+ event.nodeId
1396
+ );
1397
+ }
1398
+
1399
+ /** @public
1400
+ * A node has stopped
1401
+ * */
1402
+ export type NodeStopped = {
1403
+ event: "nodeStopped";
1404
+ nodeId: NodeId;
1405
+ };
1406
+
1407
+ /** @internal */
1408
+ function fromNodeStopped(
1409
+ event: ManagerPB.NodeStopped
1410
+ ): NodeStopped | undefined {
1411
+ return utils.mapOptional(
1412
+ (nodeId) => ({
1413
+ event: "nodeStopped",
1414
+ nodeId: fromNodeId(nodeId),
1415
+ }),
1416
+ event.nodeId
1417
+ );
1418
+ }
1419
+
1420
+ /**
1421
+ * @public
1422
+ * A node is running - used in initial state messages
1423
+ * */
1424
+ export type NodeRunning = {
1425
+ event: "nodeRunning";
1426
+ nodeMetadata: NodeMetadata;
1427
+ runningNodeMetadata: RunningNodeMetadata;
1428
+ instances: RunningJob[];
1429
+ };
1430
+
1431
+ /** @internal */
1432
+ function fromNodeRunning(
1433
+ event: ManagerPB.NodeRunning
1434
+ ): NodeRunning | undefined {
1435
+ if (event.nodeMetadata && event.runningNodeMetadata) {
1436
+ return {
1437
+ event: "nodeRunning",
1438
+ nodeMetadata: fromNodeMetadata(event.nodeMetadata),
1439
+ runningNodeMetadata: fromRunningNodeMetadata(event.runningNodeMetadata),
1440
+ instances: event.instances.map(fromRunningJob),
1441
+ };
1442
+ }
1443
+ return undefined;
1444
+ }
1445
+
1446
+ /**
1447
+ * @public
1448
+ * A physical node has connected
1449
+ * */
1450
+ export type PhysicalNodeConnected = {
1451
+ event: "physicalNodeConnected";
1452
+ node: PhysicalNode;
1453
+ instances: RunningJob[];
1454
+ };
1455
+
1456
+ /** @internal */
1457
+ function fromPhysicalNodeConnected(
1458
+ event: ManagerPB.PhysicalNodeConnected
1459
+ ): PhysicalNodeConnected | undefined {
1460
+ return utils.mapOptional(
1461
+ (node) => ({
1462
+ event: "physicalNodeConnected",
1463
+ node: fromPhysicalNode(node),
1464
+ instances: event.instances.map(fromRunningJob),
1465
+ }),
1466
+ event.node
1467
+ );
1468
+ }
1469
+
1470
+ /**
1471
+ * @public
1472
+ * The health status of a provider has changed
1473
+ * */
1474
+ export type ProviderHealthChange = {
1475
+ event: "providerHealthChange";
1476
+ health: ProviderHealth;
1477
+ };
1478
+
1479
+ /**
1480
+ * @public
1481
+ * The manager activity stream has closed
1482
+ * */
1483
+ export type EventStreamClosed = {
1484
+ event: "eventStreamClosed";
1485
+ error?: Error;
1486
+ };
1487
+
1488
+ /** @public */
1489
+ export type TagComparison =
1490
+ | { comparison: "eq"; value: string }
1491
+ | { comparison: "neq"; value: string }
1492
+ | { comparison: "re"; value: string }
1493
+ | { comparison: "exists"; value: boolean };
1494
+
1495
+ /** @public */
1496
+ export type TagFilter = {
1497
+ filterType: "tag";
1498
+ tagName: string;
1499
+ comparison: TagComparison;
1500
+ };
1501
+
1502
+ /** internal */
1503
+ function toTagFilter(filter: TagFilter): ManagerPB.TagFilter {
1504
+ let comparison: ManagerPB.TagFilter["comparison"];
1505
+
1506
+ switch (filter.comparison.comparison) {
1507
+ case "eq":
1508
+ comparison = utils.mkCase({ eq: filter.comparison.value });
1509
+ break;
1510
+ case "neq":
1511
+ comparison = utils.mkCase({ neq: filter.comparison.value });
1512
+ break;
1513
+ case "re":
1514
+ comparison = utils.mkCase({ re: filter.comparison.value });
1515
+ break;
1516
+ case "exists":
1517
+ comparison = utils.mkCase({ exists: filter.comparison.value });
1518
+ break;
1519
+ }
1520
+
1521
+ return provideFull(ManagerPB.TagFilter, {
1522
+ tagName: filter.tagName,
1523
+ comparison: comparison,
1524
+ });
1525
+ }
1526
+
1527
+ /** @public */
1528
+ export type IdComparison =
1529
+ | { comparison: "eq"; value: string }
1530
+ | { comparison: "neq"; value: string }
1531
+ | { comparison: "re"; value: string };
1532
+
1533
+ /** @public */
1534
+ export type IdFilter = {
1535
+ filterType: "id";
1536
+ comparison: IdComparison;
1537
+ };
1538
+
1539
+ /** internal */
1540
+ function toIdFilter(filter: IdFilter): ManagerPB.IdFilter {
1541
+ let comparison: ManagerPB.IdFilter["comparison"];
1542
+
1543
+ switch (filter.comparison.comparison) {
1544
+ case "eq":
1545
+ comparison = utils.mkCase({ eq: filter.comparison.value });
1546
+ break;
1547
+ case "neq":
1548
+ comparison = utils.mkCase({ neq: filter.comparison.value });
1549
+ break;
1550
+ case "re":
1551
+ comparison = utils.mkCase({ re: filter.comparison.value });
1552
+ break;
1553
+ }
1554
+ return provideFull(ManagerPB.IdFilter, {
1555
+ comparison: comparison,
1556
+ });
1557
+ }
1558
+
1559
+ /** @public */
1560
+ export type DateComparison =
1561
+ | { comparison: "eq"; value: Date }
1562
+ | { comparison: "lt"; value: Date }
1563
+ | { comparison: "lte"; value: Date }
1564
+ | { comparison: "gt"; value: Date }
1565
+ | { comparison: "gte"; value: Date }
1566
+ | { comparison: "between"; from: Date; to: Date };
1567
+
1568
+ /** @public */
1569
+ export type DateFilter = {
1570
+ filterType: "date";
1571
+ dateType: "start" | "end";
1572
+ comparison: DateComparison;
1573
+ };
1574
+
1575
+ /** internal */
1576
+ function toDateFilter(filter: DateFilter): ManagerPB.DateFilter {
1577
+ let comparison: ManagerPB.DateFilter["comparison"];
1578
+
1579
+ switch (filter.comparison.comparison) {
1580
+ case "eq":
1581
+ comparison = utils.mkCase({
1582
+ eq: Timestamp.fromDate(filter.comparison.value),
1583
+ });
1584
+ break;
1585
+ case "lt":
1586
+ comparison = utils.mkCase({
1587
+ lt: Timestamp.fromDate(filter.comparison.value),
1588
+ });
1589
+ break;
1590
+ case "lte":
1591
+ comparison = utils.mkCase({
1592
+ lte: Timestamp.fromDate(filter.comparison.value),
1593
+ });
1594
+ break;
1595
+ case "gt":
1596
+ comparison = utils.mkCase({
1597
+ gt: Timestamp.fromDate(filter.comparison.value),
1598
+ });
1599
+ break;
1600
+ case "gte":
1601
+ comparison = utils.mkCase({
1602
+ gte: Timestamp.fromDate(filter.comparison.value),
1603
+ });
1604
+ break;
1605
+ case "between":
1606
+ comparison = utils.mkCase({
1607
+ within: provideFull(ManagerPB.DateFilter_Within, {
1608
+ startDate: Timestamp.fromDate(filter.comparison.from),
1609
+ endDate: Timestamp.fromDate(filter.comparison.to),
1610
+ }),
1611
+ });
1612
+ break;
1613
+ }
1614
+ return provideFull(ManagerPB.DateFilter, {
1615
+ comparison: comparison,
1616
+ });
1617
+ }
1618
+
1619
+ /** @public */
1620
+ export type JobFilter = TagFilter | IdFilter | DateFilter;
1621
+
1622
+ /** internal */
1623
+ function toJobFilter(jobFilter: JobFilter): ManagerPB.JobFilter {
1624
+ switch (jobFilter.filterType) {
1625
+ case "tag":
1626
+ return provideFull(ManagerPB.JobFilter, {
1627
+ jobFilter: utils.mkCase({ tagFilter: toTagFilter(jobFilter) }),
1628
+ });
1629
+ case "id":
1630
+ return provideFull(ManagerPB.JobFilter, {
1631
+ jobFilter: utils.mkCase({ idFilter: toIdFilter(jobFilter) }),
1632
+ });
1633
+ case "date":
1634
+ return provideFull(ManagerPB.JobFilter, {
1635
+ jobFilter: utils.mkCase({ dateFilter: toDateFilter(jobFilter) }),
1636
+ });
1637
+ }
1638
+ }
1639
+
1640
+ /**
1641
+ * @public
1642
+ * Configuration for creating an event stream
1643
+ */
1644
+ export type EventStreamSettings = {
1645
+ pendingWindow: number; // Seconds from now that jobs are considered pending
1646
+ onEvent: (event: EventStreamEvent) => void;
1647
+ };
1648
+
1649
+ /**
1650
+ * @public
1651
+ */
1652
+ export type EventStreamInitialState = {
1653
+ nodesStarting: NodeStarting[];
1654
+ nodesRunning: NodeRunning[];
1655
+ activeJobs: JobWithHistory[];
1656
+ health: ProviderHealth[];
1657
+ closeStream: () => void;
1658
+ };
1659
+
1660
+ /**
1661
+ * @public
1662
+ */
1663
+ export type EventStreamEvent =
1664
+ | JobUpdated // Only for jobs that are active or pending - you don't get notification about a job 2 years in the future
1665
+ | JobPending
1666
+ | JobOutOfWindow
1667
+ | JobDeleted
1668
+ | JobInfo
1669
+ | NodeStarting
1670
+ | NodeStarted
1671
+ | NodeStopping
1672
+ | NodeStopped
1673
+ | PhysicalNodeConnected
1674
+ | ProviderHealthChange
1675
+ | EventStreamClosed;
1676
+
1677
+ /**
1678
+ * @public
1679
+ */
1680
+ export type JobSearchSettings = {
1681
+ filter: JobFilter[];
1682
+ };
1683
+
1684
+ /**
1685
+ * @public
1686
+ */
1687
+ export type CurrentJob = {
1688
+ job: Job;
1689
+ instances: RunningJob[];
1690
+ };
1691
+
1692
+ /** @internal */
1693
+ function fromCurrentJob(event: ManagerPB.CurrentJob): CurrentJob | undefined {
1694
+ return utils.mapOptional(
1695
+ (job) => ({
1696
+ job: fromJob(job),
1697
+ instances: event.instances.map(fromRunningJob),
1698
+ }),
1699
+ event.job
1700
+ );
1701
+ }
1702
+
1703
+ /**
1704
+ * @public
1705
+ * List of events that can be raised by the jobList API - TODO - link
1706
+ */
1707
+ export type JobListEvent = JobUpdated | JobPending;
1708
+
1709
+ /**
1710
+ * @public
1711
+ */
1712
+ export type Health = "healthy" | "unstable" | "failed";
1713
+
1714
+ /**
1715
+ * @public
1716
+ */
1717
+ export type AwsHealth = {
1718
+ provider: "aws";
1719
+ regions: Map<AwsRegion, Health>;
1720
+ };
1721
+
1722
+ /**
1723
+ * @public
1724
+ */
1725
+ export type OciHealth = {
1726
+ provider: "oci";
1727
+ };
1728
+
1729
+ /**
1730
+ * @public
1731
+ */
1732
+ export type ProviderHealth = AwsHealth | OciHealth;
1733
+
1734
+ /** @internal */
1735
+ function fromHealth(health: ManagerPB.Health): Health {
1736
+ switch (health) {
1737
+ case ManagerPB.Health.HEALTHY:
1738
+ return "healthy";
1739
+ case ManagerPB.Health.UNSTABLE:
1740
+ return "unstable";
1741
+ case ManagerPB.Health.FAILED:
1742
+ return "failed";
1743
+ }
1744
+ }
1745
+
1746
+ /** @internal */
1747
+ function fromAwsHealth(health: ManagerPB.AwsHealth): AwsHealth {
1748
+ const map: Map<AwsRegion, Health> = new Map();
1749
+ health.regionHealth.forEach((regionHealth) =>
1750
+ map.set(regionHealth.region, fromHealth(regionHealth.health))
1751
+ );
1752
+ return { provider: "aws", regions: map };
1753
+ }
1754
+
1755
+ /** @internal */
1756
+ function fromOciHealth(_health: ManagerPB.OciHealth): OciHealth {
1757
+ return { provider: "oci" };
1758
+ }
1759
+
1760
+ /** @internal */
1761
+ function fromProviderHealth(health: ManagerPB.ProviderHealth): ProviderHealth {
1762
+ const healthType = health.health.case;
1763
+ switch (healthType) {
1764
+ case undefined:
1765
+ throw Error();
1766
+ case "aws":
1767
+ return fromAwsHealth(health.health.value);
1768
+ case "oci":
1769
+ return fromOciHealth(health.health.value);
1770
+ case "test":
1771
+ throw Error();
1772
+ default: {
1773
+ const exhaustiveCheck: never = healthType;
1774
+ throw new Error(`Unhandled case: ${exhaustiveCheck}`);
1775
+ }
1776
+ }
1777
+ }
1778
+
1779
+ /**
1780
+ * @public
1781
+ * The entrypoint for all Norsk Manager applications
1782
+ *
1783
+ * @example
1784
+ * ```ts
1785
+ * const norsk = await Norsk.connect({ url: "localhost:6790" });
1786
+ * ```
1787
+ */
1788
+ export class NorskManager {
1789
+ /** @internal */
1790
+ closed: boolean = false;
1791
+ /** @internal */
1792
+ client: ManagerClient;
1793
+ /** @internal */
1794
+ settings: NorskSettings;
1795
+ /** @internal */
1796
+ connectivityState: number;
1797
+ /** @internal */
1798
+ statusStream?: grpc.ClientReadableStream<ManagerPB.NorskStatusEvent>;
1799
+ /** @internal */
1800
+ managerActivityStreams: (
1801
+ | grpc.ClientReadableStream<ManagerPB.EventStreamEvent>
1802
+ | undefined
1803
+ )[] = [];
1804
+ nextEventStreamId: number = 0;
1805
+ /* @internal */
1806
+ public initialised: Promise<void>;
1807
+ /* @internal */
1808
+ public resolveInitialised: () => void;
1809
+
1810
+ /** @public */
1811
+ public static async connect(settings: NorskSettings) {
1812
+ const norsk = new NorskManager(settings);
1813
+ await norsk.initialised;
1814
+ return norsk;
1815
+ }
1816
+
1817
+ /** @internal */
1818
+ constructor(norskSettings: NorskSettings) {
1819
+ this.connectivityState = 0;
1820
+ this.client = new ManagerClient(
1821
+ norskSettings.url ? norskSettings.url : norskHost() + ":" + norskPort(),
1822
+ grpc.credentials.createInsecure()
1823
+ );
1824
+ this.settings = norskSettings;
1825
+ this.connectivityStateWatcher();
1826
+ this.initialised = new Promise((resolve, _reject) => {
1827
+ this.resolveInitialised = resolve;
1828
+ });
1829
+ }
1830
+
1831
+ /**
1832
+ * @public
1833
+ * Norsk Runtime version information
1834
+ */
1835
+ public version: CommonVersion;
1836
+
1837
+ /**
1838
+ * @public
1839
+ * Close down the Norsk connection
1840
+ */
1841
+ public close() {
1842
+ this.statusStream?.cancel();
1843
+ for (const stream of this.managerActivityStreams) {
1844
+ stream!.cancel();
1845
+ }
1846
+ try {
1847
+ this.client.close();
1848
+ } catch { }
1849
+ this.closed = true;
1850
+ }
1851
+
1852
+ /**
1853
+ * @public - TODO - this can fail (duplicate id etc)
1854
+ */
1855
+ public async createJob(job: CreateJob): Promise<Job> {
1856
+ const fn = safeBind(promisifyUnary(this.client.createJob), this.client);
1857
+ const jobFull: Job = { ...job, state: "pre", currentHash: BigInt(0) };
1858
+ const newHash = await fn(toJob(jobFull));
1859
+ jobFull.currentHash = newHash.hash;
1860
+ return jobFull;
1861
+ }
1862
+
1863
+ /**
1864
+ * @public - TODO - this can fail (version check etc)
1865
+ */
1866
+ public async updateJob(job: Job): Promise<Job> {
1867
+ const fn = safeBind(promisifyUnary(this.client.updateJob), this.client);
1868
+ const newHash = await fn(toJob(job));
1869
+ job.currentHash = newHash.hash;
1870
+ return job;
1871
+ }
1872
+
1873
+ /**
1874
+ * @public - TODO - can it fail?
1875
+ */
1876
+ public async deleteJob(jobId: JobId): Promise<void> {
1877
+ const fn = safeBind(promisifyUnary(this.client.deleteJob), this.client);
1878
+ await fn(toJobId(jobId));
1879
+ }
1880
+
1881
+ /**
1882
+ * @public - TODO - can it fail?
1883
+ */
1884
+ public async completeJob(jobId: JobId): Promise<void> {
1885
+ const fn = safeBind(promisifyUnary(this.client.completeJob), this.client);
1886
+ await fn(toJobId(jobId));
1887
+ }
1888
+
1889
+ /**
1890
+ * @public - TODO
1891
+ */
1892
+ public async createAwsNode(node: AwsNodeCreate): Promise<void> {
1893
+ const fn = safeBind(promisifyUnary(this.client.createAwsNode), this.client);
1894
+ await fn(toAwsNodeCreate(node));
1895
+ return;
1896
+ }
1897
+
1898
+ /**
1899
+ * @public - TODO
1900
+ */
1901
+ public async createOciNode(node: OciNodeCreate): Promise<void> {
1902
+ const fn = safeBind(promisifyUnary(this.client.createOciNode), this.client);
1903
+ await fn(toOciNodeCreate(node));
1904
+ return;
1905
+ }
1906
+
1907
+
1908
+ /**
1909
+ * @public - TODO
1910
+ */
1911
+ public async updatePhysicalNode(
1912
+ nodeId: PhysicalNodeId,
1913
+ daemonVersion: string
1914
+ ): Promise<void> {
1915
+ const fn = safeBind(
1916
+ promisifyUnary(this.client.updatePhysicalNode),
1917
+ this.client
1918
+ );
1919
+ await fn(
1920
+ provideFull(ManagerPB.UpdatePhysicalNodeRequest, {
1921
+ id: nodeId,
1922
+ daemonVersion: daemonVersion,
1923
+ })
1924
+ );
1925
+ }
1926
+
1927
+ /**
1928
+ * @public - TODO
1929
+ */
1930
+ public async terminateNode(nodeId: NodeId): Promise<void> {
1931
+ const fn = safeBind(promisifyUnary(this.client.terminateNode), this.client);
1932
+ await fn(toNodeId(nodeId));
1933
+ }
1934
+
1935
+ /**
1936
+ * @public - TODO
1937
+ */
1938
+ public async startJob(
1939
+ jobId: JobId,
1940
+ role: Role,
1941
+ nodeId: NodeId
1942
+ ): Promise<void> {
1943
+ const fn = safeBind(promisifyUnary(this.client.startJob), this.client);
1944
+ await fn(
1945
+ provideFull(ManagerPB.StartJobRequest, {
1946
+ jobId: toJobId(jobId),
1947
+ role: role,
1948
+ nodeId: toNodeId(nodeId),
1949
+ })
1950
+ );
1951
+ }
1952
+
1953
+ /**
1954
+ * @public - TODO
1955
+ */
1956
+ public async stopJob(
1957
+ jobId: JobId,
1958
+ role: Role,
1959
+ nodeId: NodeId
1960
+ ): Promise<void> {
1961
+ const fn = safeBind(promisifyUnary(this.client.stopJob), this.client);
1962
+ await fn(
1963
+ provideFull(ManagerPB.StopJobRequest, {
1964
+ jobId: toJobId(jobId),
1965
+ role: role,
1966
+ nodeId: toNodeId(nodeId),
1967
+ })
1968
+ );
1969
+ }
1970
+
1971
+ /**
1972
+ * @public - TODO - check name
1973
+ * provides a stream of activity that is essential for the creation of client-side
1974
+ * management logic.
1975
+ */
1976
+ public async eventStream(
1977
+ settings: EventStreamSettings
1978
+ ): Promise<[EventStreamInitialState, () => void]> {
1979
+ const stream = this.client.eventStream(
1980
+ provideFull(ManagerPB.EventStreamRequest, {
1981
+ pendingWindowS: settings.pendingWindow,
1982
+ })
1983
+ );
1984
+
1985
+ this.managerActivityStreams[this.nextEventStreamId] = stream;
1986
+
1987
+ const stop = () => {
1988
+ this.managerActivityStreams[this.nextEventStreamId]?.cancel;
1989
+ delete this.managerActivityStreams[this.nextEventStreamId];
1990
+ };
1991
+
1992
+ this.nextEventStreamId++;
1993
+
1994
+ return new Promise((resolve, reject) => {
1995
+ let resolved: boolean = false;
1996
+ const nodesStarting: NodeStarting[] = [];
1997
+ const nodesRunning: NodeRunning[] = [];
1998
+ const activeJobs: JobWithHistory[] = [];
1999
+
2000
+ stream.on("close", () => {
2001
+ if (resolved) {
2002
+ settings.onEvent({ event: "eventStreamClosed" });
2003
+ } else {
2004
+ reject();
2005
+ }
2006
+ });
2007
+
2008
+ stream.on("error", (err: grpc.ServiceError) => {
2009
+ if (err.code == grpc.status.CANCELLED) {
2010
+ // close will get called next...
2011
+ return;
2012
+ }
2013
+
2014
+ if (resolved) {
2015
+ settings.onEvent({ event: "eventStreamClosed", error: err });
2016
+ } else {
2017
+ reject();
2018
+ }
2019
+ });
2020
+
2021
+ stream.on("data", (data: ManagerPB.EventStreamEvent) => {
2022
+ const messageCase = data.event.case;
2023
+ switch (messageCase) {
2024
+ case undefined:
2025
+ break;
2026
+ case "initialDataComplete":
2027
+ resolve([
2028
+ {
2029
+ nodesRunning,
2030
+ nodesStarting,
2031
+ activeJobs,
2032
+ health: data.event.value.health.map(fromProviderHealth),
2033
+ closeStream: () => {
2034
+ stream.cancel();
2035
+ },
2036
+ },
2037
+ stop,
2038
+ ]);
2039
+ resolved = true;
2040
+ break;
2041
+ case "jobActive":
2042
+ // should only get jobActive prior to initialDataLoad completing
2043
+ if (!resolved && data.event.value.jobWithHistory) {
2044
+ const jobWithHistory = fromJobWithHistory(
2045
+ data.event.value.jobWithHistory
2046
+ );
2047
+ if (jobWithHistory !== undefined) {
2048
+ activeJobs.push(jobWithHistory);
2049
+ }
2050
+ }
2051
+ break;
2052
+ case "jobUpdated":
2053
+ utils.mapOptional(
2054
+ settings.onEvent,
2055
+ fromJobUpdated(data.event.value)
2056
+ );
2057
+ break;
2058
+ case "jobPending":
2059
+ utils.mapOptional(
2060
+ settings.onEvent,
2061
+ fromJobPending(data.event.value)
2062
+ );
2063
+ break;
2064
+ case "jobDeleted":
2065
+ utils.mapOptional(
2066
+ settings.onEvent,
2067
+ fromJobDeleted(data.event.value)
2068
+ );
2069
+ break;
2070
+ case "jobOutOfWindow":
2071
+ utils.mapOptional(
2072
+ settings.onEvent,
2073
+ fromJobOutOfWindow(data.event.value)
2074
+ );
2075
+ break;
2076
+ case "nodeRunning":
2077
+ // should only get nodeRunning prior to initialDataLoad completing
2078
+ if (!resolved) {
2079
+ const nodeRunningEvent = fromNodeRunning(data.event.value);
2080
+ if (nodeRunningEvent !== undefined) {
2081
+ nodesRunning.push(nodeRunningEvent);
2082
+ }
2083
+ }
2084
+ break;
2085
+ case "nodeStarting":
2086
+ {
2087
+ const nodeStartingEvent = fromNodeStarting(data.event.value);
2088
+ if (nodeStartingEvent) {
2089
+ if (resolved) {
2090
+ settings.onEvent(nodeStartingEvent);
2091
+ } else {
2092
+ nodesStarting.push(nodeStartingEvent);
2093
+ }
2094
+ }
2095
+ }
2096
+ break;
2097
+ case "nodeStarted":
2098
+ utils.mapOptional(
2099
+ settings.onEvent,
2100
+ fromNodeStarted(data.event.value)
2101
+ );
2102
+ break;
2103
+ case "nodeStopping":
2104
+ utils.mapOptional(
2105
+ settings.onEvent,
2106
+ fromNodeStopping(data.event.value)
2107
+ );
2108
+ break;
2109
+ case "nodeStopped":
2110
+ utils.mapOptional(
2111
+ settings.onEvent,
2112
+ fromNodeStopped(data.event.value)
2113
+ );
2114
+ break;
2115
+ case "physicalNodeConnected":
2116
+ utils.mapOptional(
2117
+ settings.onEvent,
2118
+ fromPhysicalNodeConnected(data.event.value)
2119
+ );
2120
+ break;
2121
+ case "providerHealthChange":
2122
+ utils.mapOptional(settings.onEvent, {
2123
+ event: "providerHealthChange",
2124
+ health: fromProviderHealth(data.event.value),
2125
+ });
2126
+ break;
2127
+ case "jobInfo":
2128
+ utils.mapOptional(settings.onEvent, fromJobInfo(data.event.value));
2129
+ break;
2130
+ default:
2131
+ utils.exhaustiveCheck(messageCase);
2132
+ }
2133
+ });
2134
+ });
2135
+ }
2136
+
2137
+ /**
2138
+ * @public
2139
+ * search for jobs by id
2140
+ */
2141
+ public async jobById(id: JobId): Promise<CurrentJob | undefined> {
2142
+ const jobSearchResults = await this.jobSearch({
2143
+ filter: [
2144
+ { filterType: "id", comparison: { comparison: "eq", value: id } },
2145
+ ],
2146
+ });
2147
+
2148
+ if (jobSearchResults.length != 1) {
2149
+ return undefined;
2150
+ } else {
2151
+ return jobSearchResults[0];
2152
+ }
2153
+ }
2154
+
2155
+ /**
2156
+ * @public
2157
+ * search for jobs by tag / date etc
2158
+ */
2159
+ public async jobSearch(settings: JobSearchSettings): Promise<CurrentJob[]> {
2160
+ const stream = this.client.jobSearch(
2161
+ provideFull(ManagerPB.JobSearchRequest, {
2162
+ jobFilter: settings.filter.map(toJobFilter),
2163
+ })
2164
+ );
2165
+
2166
+ return new Promise((resolve, reject) => {
2167
+ const jobs: CurrentJob[] = [];
2168
+ // TODO - handle stream errors...
2169
+
2170
+ stream.on("data", (data: ManagerPB.CurrentJob) => {
2171
+ const job = fromCurrentJob(data);
2172
+ if (job !== undefined) {
2173
+ jobs.push(job);
2174
+ }
2175
+ });
2176
+ stream.on("close", () => resolve(jobs));
2177
+ stream.on("error", (err) => reject(err));
2178
+ });
2179
+ }
2180
+
2181
+ /** @internal */
2182
+ handleStatusEvent(data: ManagerPB.NorskStatusEvent) {
2183
+ const messageCase = data.message.case;
2184
+ switch (messageCase) {
2185
+ case undefined:
2186
+ break;
2187
+ case "hello": {
2188
+ debuglog(
2189
+ "Norsk status channel connected: %s",
2190
+ data.message.value.version
2191
+ );
2192
+ if (data.message.value.version === undefined) {
2193
+ throw new Error("Norsk version is undefined");
2194
+ } else {
2195
+ this.version = data.message.value.version;
2196
+ }
2197
+ this.resolveInitialised();
2198
+ this.settings.onHello &&
2199
+ data.message.value.version &&
2200
+ this.settings.onHello(data.message.value.version);
2201
+ break;
2202
+ }
2203
+ case "logEvent": {
2204
+ let level:
2205
+ | "emergency"
2206
+ | "alert"
2207
+ | "critical"
2208
+ | "error"
2209
+ | "warning"
2210
+ | "notice"
2211
+ | "info"
2212
+ | "debug";
2213
+
2214
+ switch (data.message.value.level) {
2215
+ case Log_Level.EMERGENCY:
2216
+ level = "emergency";
2217
+ break;
2218
+ case Log_Level.ALERT:
2219
+ level = "alert";
2220
+ break;
2221
+ case Log_Level.CRITICAL:
2222
+ level = "critical";
2223
+ break;
2224
+ case Log_Level.ERROR:
2225
+ level = "error";
2226
+ break;
2227
+ case Log_Level.WARNING:
2228
+ level = "warning";
2229
+ break;
2230
+ case Log_Level.NOTICE:
2231
+ level = "notice";
2232
+ break;
2233
+ case Log_Level.INFO:
2234
+ level = "info";
2235
+ break;
2236
+ case Log_Level.DEBUG:
2237
+ level = "debug";
2238
+ break;
2239
+ }
2240
+ const log: Log = {
2241
+ level: level,
2242
+ message: data.message.value.msg,
2243
+ timestamp: new Date(Number(data.message.value.timestamp) / 1000),
2244
+ };
2245
+ if (this.settings.onLogEvent) {
2246
+ this.settings.onLogEvent(log);
2247
+ } else {
2248
+ debuglog("Norsk log event: %o", log);
2249
+ }
2250
+ break;
2251
+ }
2252
+ default: {
2253
+ const exhaustiveCheck: never = messageCase;
2254
+ throw new Error(`Unhandled case: ${exhaustiveCheck}`);
2255
+ }
2256
+ }
2257
+ }
2258
+
2259
+ /** @internal */
2260
+ connectivityStateWatcher() {
2261
+ if (this.closed) {
2262
+ return;
2263
+ }
2264
+ const channel = this.client.getChannel();
2265
+ const connectivityState = channel.getConnectivityState(true);
2266
+ let stateStr: string = connectivityState.toString();
2267
+
2268
+ switch (connectivityState) {
2269
+ case 0: {
2270
+ if (this.connectivityState == 1 || this.connectivityState == 2) {
2271
+ this.settings.onShutdown && this.settings.onShutdown();
2272
+ }
2273
+ // Idle
2274
+ stateStr = "idle";
2275
+ this.settings.onAttemptingToConnect &&
2276
+ this.settings.onAttemptingToConnect();
2277
+ break;
2278
+ }
2279
+ case 1: {
2280
+ // Connecting
2281
+ stateStr = "connecting";
2282
+ this.settings.onConnecting && this.settings.onConnecting();
2283
+ break;
2284
+ }
2285
+ case 2: {
2286
+ // Ready
2287
+ stateStr = "ready";
2288
+ this.settings.onReady && this.settings.onReady();
2289
+ this.statusStream = this.client.createStatusChannel(new Empty());
2290
+ this.statusStream.on("data", this.handleStatusEvent.bind(this));
2291
+ this.statusStream.on("error", () => {
2292
+ return;
2293
+ });
2294
+ break;
2295
+ }
2296
+ case 3: {
2297
+ // Transient failure
2298
+ stateStr = "transient failure";
2299
+ this.settings.onFailedToConnect && this.settings.onFailedToConnect();
2300
+ break;
2301
+ }
2302
+ case 4: {
2303
+ // Shutdown
2304
+ stateStr = "shutdown";
2305
+ this.settings.onShutdown && this.settings.onShutdown();
2306
+ break;
2307
+ }
2308
+ }
2309
+ debuglog(
2310
+ "Channel connectivity state change: %d / %s",
2311
+ connectivityState,
2312
+ stateStr
2313
+ );
2314
+
2315
+ this.connectivityState = connectivityState;
2316
+ channel.watchConnectivityState(connectivityState, Infinity, () => {
2317
+ this.connectivityStateWatcher();
2318
+ });
2319
+ }
2320
+ }
2321
+
2322
+ // This is basically AsyncIterable<EventStreamEvent>, except that allows an `any` for the return type of next
2323
+ interface EventStreamAsyncIterable {
2324
+ [Symbol.asyncIterator](): AsyncIterator<EventStreamEvent, EventStreamEvent>;
2325
+ }
2326
+
2327
+ export async function eventStream(
2328
+ norsk: NorskManager,
2329
+ options: {
2330
+ pendingWindow: number;
2331
+ }
2332
+ ): Promise<[EventStreamInitialState, EventStreamAsyncIterable]> {
2333
+ let nextEventToReturn = 0;
2334
+ let nextEventToArrive = 0;
2335
+
2336
+ const resolves: ((arg: {
2337
+ value: EventStreamEvent;
2338
+ done: boolean;
2339
+ }) => void)[] = [];
2340
+ const promises: (Promise<IteratorResult<EventStreamEvent>> | undefined)[] =
2341
+ [];
2342
+
2343
+ const iter = {
2344
+ [Symbol.asyncIterator]() {
2345
+ return {
2346
+ next() {
2347
+ const mPromise = promises[nextEventToReturn];
2348
+ if (mPromise) {
2349
+ // If we have a promise available, then just return it
2350
+ delete promises[nextEventToReturn];
2351
+ nextEventToReturn++;
2352
+ return mPromise;
2353
+ } else {
2354
+ const promise: Promise<IteratorResult<EventStreamEvent>> =
2355
+ new Promise((r) => (resolves[nextEventToReturn] = r));
2356
+ nextEventToReturn++;
2357
+ return promise;
2358
+ }
2359
+ },
2360
+ };
2361
+ },
2362
+ };
2363
+
2364
+ const [initialState, _stop] = await norsk.eventStream({
2365
+ pendingWindow: options.pendingWindow,
2366
+ onEvent: (event: EventStreamEvent) => {
2367
+ const done: boolean = event.event == "eventStreamClosed";
2368
+ if (resolves[nextEventToArrive]) {
2369
+ resolves[nextEventToArrive]({ value: event, done: done });
2370
+ delete resolves[nextEventToArrive];
2371
+ nextEventToArrive++;
2372
+ } else {
2373
+ promises[nextEventToArrive] = new Promise((r) =>
2374
+ r({ value: event, done: done })
2375
+ );
2376
+ nextEventToArrive++;
2377
+ }
2378
+ },
2379
+ });
2380
+
2381
+ return [initialState, iter];
2382
+ }
2383
+
2384
+ /** @private */
2385
+ function norskHost(): string {
2386
+ if (process.env.NORSK_HOST) return process.env.NORSK_HOST;
2387
+ return "localhost";
2388
+ }
2389
+
2390
+ /** @private */
2391
+ function norskPort(): string {
2392
+ if (process.env.NORSK_PORT) return process.env.NORSK_PORT;
2393
+ return "6789";
2394
+ }
2395
+
2396
+ /** @private */
2397
+ function promisifyUnary<T, TResult>(requestFn: {
2398
+ (
2399
+ request: T,
2400
+ callback: (error: grpc.ServiceError | null, response: TResult) => void
2401
+ ): SurfaceCall;
2402
+ (
2403
+ request: T,
2404
+ metadata: grpc.Metadata,
2405
+ callback: (error: grpc.ServiceError | null, response: TResult) => void
2406
+ ): SurfaceCall;
2407
+ (
2408
+ request: T,
2409
+ metadata: grpc.Metadata,
2410
+ options: Partial<grpc.CallOptions>,
2411
+ callback: (error: grpc.ServiceError | null, response: TResult) => void
2412
+ ): SurfaceCall;
2413
+ }): (request: T) => Promise<TResult> {
2414
+ return util.promisify(requestFn);
2415
+ }
2416
+
2417
+ /** @private */
2418
+ function safeBind<T, TResult>(
2419
+ fn: (arg: T) => TResult,
2420
+ obj: unknown
2421
+ ): (arg: T) => TResult {
2422
+ return fn.bind(obj);
2423
+ }