node-datachannel 0.10.1 → 0.12.0

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 (48) hide show
  1. package/.github/workflows/build-linux.yml +7 -2
  2. package/.github/workflows/build-win.yml +3 -3
  3. package/.gitmodules +3 -0
  4. package/CMakeLists.txt +1 -1
  5. package/README.md +18 -4
  6. package/cmake/toolchain/ci.cmake +1 -1
  7. package/examples/client-server/client-benchmark.js +7 -9
  8. package/examples/client-server/client-periodic.js +7 -9
  9. package/examples/client-server/client.js +7 -9
  10. package/examples/client-server/package-lock.json +0 -27
  11. package/examples/client-server/package.json +0 -1
  12. package/examples/client-server/signaling-server.js +21 -10
  13. package/examples/electron-demo/README.md +1 -1
  14. package/examples/electron-demo/package-lock.json +316 -372
  15. package/examples/electron-demo/package.json +2 -1
  16. package/examples/electron-demo/webpack.main.config.js +10 -0
  17. package/examples/websocket/websocket-client.js +20 -0
  18. package/examples/websocket/websocket-server.js +22 -0
  19. package/jest.config.cjs +1 -1
  20. package/lib/index.cjs +52 -9
  21. package/lib/index.js +22 -15
  22. package/lib/node-datachannel.js +7 -0
  23. package/lib/websocket-server.js +34 -0
  24. package/package.json +3 -3
  25. package/polyfill/Events.js +5 -3
  26. package/polyfill/Exception.js +23 -0
  27. package/polyfill/README.md +4 -0
  28. package/polyfill/RTCDataChannel.js +19 -11
  29. package/polyfill/RTCError.d.ts +11 -0
  30. package/polyfill/RTCError.js +65 -0
  31. package/polyfill/RTCIceCandidate.js +22 -23
  32. package/polyfill/RTCPeerConnection.js +157 -42
  33. package/polyfill/index.cjs +350 -82
  34. package/polyfill/index.js +3 -0
  35. package/src/peer-connection-wrapper.cpp +11 -3
  36. package/src/web-socket-server-wrapper.cpp +23 -23
  37. package/src/web-socket-server-wrapper.h +8 -6
  38. package/src/web-socket-wrapper.cpp +7 -1
  39. package/test/connectivity.js +1 -23
  40. package/test/wpt-tests/README.md +46 -0
  41. package/test/wpt-tests/chrome-failed-tests.js +42 -0
  42. package/test/wpt-tests/index.js +96 -0
  43. package/test/wpt-tests/last-test-results.md +301 -0
  44. package/test/wpt-tests/package-lock.json +1712 -0
  45. package/test/wpt-tests/package.json +19 -0
  46. package/test/wpt-tests/wpt-test-list.js +139 -0
  47. package/test/wpt-tests/wpt.js +131 -0
  48. package/test/wpt.js +0 -50
@@ -27,6 +27,7 @@
27
27
  "@electron-forge/plugin-webpack": "^6.4.2",
28
28
  "@electron/rebuild": "^3.3.0",
29
29
  "@vercel/webpack-asset-relocator-loader": "^1.7.3",
30
+ "copy-webpack-plugin": "^12.0.2",
30
31
  "css-loader": "^6.8.1",
31
32
  "electron": "26.2.4",
32
33
  "node-loader": "^2.0.0",
@@ -40,4 +41,4 @@
40
41
  "runtime": "electron",
41
42
  "runtimeVersion": "26.2.0"
42
43
  }
43
- }
44
+ }
@@ -1,3 +1,5 @@
1
+ const CopyPlugin = require('copy-webpack-plugin');
2
+
1
3
  module.exports = {
2
4
  /**
3
5
  * This is the main entry point for your application, it's the first file
@@ -8,4 +10,12 @@ module.exports = {
8
10
  module: {
9
11
  rules: require('./webpack.rules'),
10
12
  },
13
+ externals: {
14
+ 'node-datachannel': 'node-datachannel',
15
+ },
16
+ plugins: [
17
+ new CopyPlugin({
18
+ patterns: [{ from: 'node_modules/node-datachannel', to: 'node_modules/node-datachannel' }],
19
+ }),
20
+ ],
11
21
  };
@@ -0,0 +1,20 @@
1
+ import * as nodeDataChannel from '../../lib/index.js';
2
+
3
+ const clientSocket = new nodeDataChannel.WebSocket();
4
+
5
+ clientSocket.open('ws://127.0.0.1:3000');
6
+
7
+ clientSocket.onOpen(() => {
8
+ console.log('Client socket opened');
9
+ clientSocket.sendMessage('Echo this!');
10
+ });
11
+
12
+ clientSocket.onMessage((message) => {
13
+ console.log('Message from server: ' + message);
14
+ });
15
+
16
+ clientSocket.onClosed(() => {
17
+ console.log('Client socket closed');
18
+ });
19
+
20
+ // clientSocket.close();
@@ -0,0 +1,22 @@
1
+ import * as nodeDataChannel from '../../lib/index.js';
2
+
3
+ const ws = new nodeDataChannel.WebSocketServer({ bindAddress: '127.0.0.1', port: 3000 });
4
+
5
+ ws.onClient((clientSocket) => {
6
+ clientSocket.onOpen(() => {
7
+ console.log('Client socket opened');
8
+ clientSocket.sendMessage('Hello from server');
9
+ });
10
+
11
+ clientSocket.onMessage((message) => {
12
+ console.log('Message from client: ' + message);
13
+ clientSocket.sendMessage(message);
14
+ });
15
+
16
+ clientSocket.onClosed(() => {
17
+ console.log('Client socket closed');
18
+ });
19
+ });
20
+
21
+ // When done, close the server
22
+ // ws.stop();
package/jest.config.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /** @type {import('jest').Config} */
2
2
  const config = {
3
3
  verbose: true,
4
- testPathIgnorePatterns: ['<rootDir>/node_modules/', 'multiple-run.test'],
4
+ testPathIgnorePatterns: ['node_modules', 'multiple-run.test', 'wpt-tests'],
5
5
  };
6
6
 
7
7
  module.exports = config;
package/lib/index.cjs CHANGED
@@ -4,8 +4,14 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var module$1 = require('module');
6
6
  var stream = require('stream');
7
+ var events = require('events');
7
8
 
8
9
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
10
+ // createRequire is native in node version >= 12
11
+ const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
12
+
13
+ const nodeDataChannel = require$1('../build/Release/node_datachannel.node');
14
+
9
15
  /**
10
16
  * Turns a node-datachannel DataChannel into a real Node.js stream, complete with buffering,
11
17
  * backpressure (up to a point - if the buffer fills up, messages are dropped), and
@@ -101,10 +107,37 @@ class DataChannelStream extends stream.Duplex {
101
107
  }
102
108
  }
103
109
 
104
- // createRequire is native in node version >= 12
105
- const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
110
+ class WebSocketServer extends events.EventEmitter {
111
+ #server;
112
+ #clients = [];
106
113
 
107
- const nodeDataChannel = require$1('../build/Release/node_datachannel.node');
114
+ constructor(options) {
115
+ super();
116
+ this.#server = new nodeDataChannel.WebSocketServer(options);
117
+
118
+ this.#server.onClient((client) => {
119
+ this.emit('client', client);
120
+ this.#clients.push(client);
121
+ });
122
+ }
123
+
124
+ port() {
125
+ return this.#server?.port() || 0;
126
+ }
127
+
128
+ stop() {
129
+ this.#clients.forEach((client) => {
130
+ client?.close();
131
+ });
132
+ this.#server?.stop();
133
+ this.#server = null;
134
+ this.removeAllListeners();
135
+ }
136
+
137
+ onClient(cb) {
138
+ if (this.#server) this.on('client', cb);
139
+ }
140
+ }
108
141
 
109
142
  const {
110
143
  initLogger,
@@ -118,14 +151,8 @@ const {
118
151
  DataChannel,
119
152
  PeerConnection,
120
153
  WebSocket,
121
- WebSocketServer,
122
154
  } = nodeDataChannel;
123
155
 
124
- var index = {
125
- ...nodeDataChannel,
126
- DataChannelStream,
127
- };
128
-
129
156
  const DescriptionType = {
130
157
  Unspec: 'unspec',
131
158
  Offer: 'offer',
@@ -134,6 +161,22 @@ const DescriptionType = {
134
161
  Rollback: 'rollback',
135
162
  };
136
163
 
164
+ var index = {
165
+ initLogger,
166
+ cleanup,
167
+ preload,
168
+ setSctpSettings,
169
+ RtcpReceivingSession,
170
+ Track,
171
+ Video,
172
+ Audio,
173
+ DataChannel,
174
+ PeerConnection,
175
+ WebSocket,
176
+ WebSocketServer,
177
+ DataChannelStream,
178
+ };
179
+
137
180
  exports.Audio = Audio;
138
181
  exports.DataChannel = DataChannel;
139
182
  exports.DataChannelStream = DataChannelStream;
package/lib/index.js CHANGED
@@ -1,9 +1,6 @@
1
- // createRequire is native in node version >= 12
2
- import { createRequire } from 'module';
3
- const require = createRequire(import.meta.url);
4
-
5
- const nodeDataChannel = require('../build/Release/node_datachannel.node');
1
+ import nodeDataChannel from './node-datachannel.js';
6
2
  import DataChannelStream from './datachannel-stream.js';
3
+ import WebSocketServer from './websocket-server.js';
7
4
 
8
5
  const {
9
6
  initLogger,
@@ -17,9 +14,16 @@ const {
17
14
  DataChannel,
18
15
  PeerConnection,
19
16
  WebSocket,
20
- WebSocketServer,
21
17
  } = nodeDataChannel;
22
18
 
19
+ export const DescriptionType = {
20
+ Unspec: 'unspec',
21
+ Offer: 'offer',
22
+ Answer: 'answer',
23
+ Pranswer: 'pranswer',
24
+ Rollback: 'rollback',
25
+ };
26
+
23
27
  export {
24
28
  initLogger,
25
29
  cleanup,
@@ -38,14 +42,17 @@ export {
38
42
  };
39
43
 
40
44
  export default {
41
- ...nodeDataChannel,
45
+ initLogger,
46
+ cleanup,
47
+ preload,
48
+ setSctpSettings,
49
+ RtcpReceivingSession,
50
+ Track,
51
+ Video,
52
+ Audio,
53
+ DataChannel,
54
+ PeerConnection,
55
+ WebSocket,
56
+ WebSocketServer,
42
57
  DataChannelStream,
43
58
  };
44
-
45
- export const DescriptionType = {
46
- Unspec: 'unspec',
47
- Offer: 'offer',
48
- Answer: 'answer',
49
- Pranswer: 'pranswer',
50
- Rollback: 'rollback',
51
- };
@@ -0,0 +1,7 @@
1
+ // createRequire is native in node version >= 12
2
+ import { createRequire } from 'module';
3
+ const require = createRequire(import.meta.url);
4
+
5
+ const nodeDataChannel = require('../build/Release/node_datachannel.node');
6
+
7
+ export default nodeDataChannel;
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from 'events';
2
+ import nodeDataChannel from './node-datachannel.js';
3
+
4
+ export default class WebSocketServer extends EventEmitter {
5
+ #server;
6
+ #clients = [];
7
+
8
+ constructor(options) {
9
+ super();
10
+ this.#server = new nodeDataChannel.WebSocketServer(options);
11
+
12
+ this.#server.onClient((client) => {
13
+ this.emit('client', client);
14
+ this.#clients.push(client);
15
+ });
16
+ }
17
+
18
+ port() {
19
+ return this.#server?.port() || 0;
20
+ }
21
+
22
+ stop() {
23
+ this.#clients.forEach((client) => {
24
+ client?.close();
25
+ });
26
+ this.#server?.stop();
27
+ this.#server = null;
28
+ this.removeAllListeners();
29
+ }
30
+
31
+ onClient(cb) {
32
+ if (this.#server) this.on('client', cb);
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-datachannel",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "libdatachannel node bindings",
5
5
  "type": "module",
6
6
  "types": "./lib/index.d.ts",
@@ -93,8 +93,8 @@
93
93
  "node-addon-api": "^7.0.0",
94
94
  "prebuild": "^12.0.0",
95
95
  "prettier": "^2.8.8",
96
- "typescript": "^5.3.2",
97
- "rollup": "^4.14.1"
96
+ "rollup": "^4.14.1",
97
+ "typescript": "^5.3.2"
98
98
  },
99
99
  "dependencies": {
100
100
  "node-domexception": "^2.0.1",
@@ -15,10 +15,12 @@ export class RTCPeerConnectionIceEvent extends Event {
15
15
  export class RTCDataChannelEvent extends Event {
16
16
  #channel;
17
17
 
18
- constructor(channel) {
19
- super('datachannel');
18
+ constructor(type, eventInitDict) {
19
+ super(type);
20
20
 
21
- this.#channel = channel;
21
+ if (type && !eventInitDict.channel) throw new TypeError('channel member is required');
22
+
23
+ this.#channel = eventInitDict?.channel;
22
24
  }
23
25
 
24
26
  get channel() {
@@ -0,0 +1,23 @@
1
+ export const InvalidStateError = (msg) => {
2
+ return new DOMException(msg, 'InvalidStateError');
3
+ };
4
+
5
+ export const InvalidAccessError = (msg) => {
6
+ return new DOMException(msg, 'InvalidAccessError');
7
+ };
8
+
9
+ export const NotFoundError = (msg) => {
10
+ return new DOMException(msg, 'NotFoundError');
11
+ };
12
+
13
+ export const OperationError = (msg) => {
14
+ return new DOMException(msg, 'OperationError');
15
+ };
16
+
17
+ export const TypeError = (msg) => {
18
+ return new DOMException(msg, 'TypeError');
19
+ };
20
+
21
+ export const SyntaxError = (msg) => {
22
+ return new DOMException(msg, 'SyntaxError');
23
+ };
@@ -2,6 +2,10 @@
2
2
 
3
3
  WebRTC polyfills to be used for libraries like `simple-peer`.
4
4
 
5
+ # web-platform-tests
6
+
7
+ Please check actual situation [here](/test/wpt-tests/)
8
+
5
9
  ## Example Usage
6
10
 
7
11
  > For a native usage example please check test folder.
@@ -1,4 +1,5 @@
1
- import DOMException from 'node-domexception';
1
+ import 'node-domexception';
2
+ import * as exceptions from './Exception.js';
2
3
 
3
4
  export default class _RTCDataChannel extends EventTarget {
4
5
  #dataChannel;
@@ -10,6 +11,8 @@ export default class _RTCDataChannel extends EventTarget {
10
11
  #negotiated;
11
12
  #ordered;
12
13
 
14
+ #closeRequested = false;
15
+
13
16
  onbufferedamountlow;
14
17
  onclose;
15
18
  onclosing;
@@ -32,12 +35,20 @@ export default class _RTCDataChannel extends EventTarget {
32
35
  // forward dataChannel events
33
36
  this.#dataChannel.onOpen(() => {
34
37
  this.#readyState = 'open';
35
- this.dispatchEvent(new Event('open'));
38
+ this.dispatchEvent(new Event('open', { channel: this }));
36
39
  });
37
40
 
38
41
  this.#dataChannel.onClosed(() => {
39
- this.#readyState = 'closed';
40
- this.dispatchEvent(new Event('close'));
42
+ // Simulate closing event
43
+ if (!this.#closeRequested) {
44
+ this.#readyState = 'closing';
45
+ this.dispatchEvent(new Event('closing', { channel: this }));
46
+ }
47
+
48
+ setImmediate(() => {
49
+ this.#readyState = 'closed';
50
+ this.dispatchEvent(new Event('close', { channel: this }));
51
+ });
41
52
  });
42
53
 
43
54
  this.#dataChannel.onError((msg) => {
@@ -54,7 +65,7 @@ export default class _RTCDataChannel extends EventTarget {
54
65
  });
55
66
 
56
67
  this.#dataChannel.onBufferedAmountLow(() => {
57
- this.dispatchEvent(new Event('bufferedamountlow'));
68
+ this.dispatchEvent(new Event('bufferedamountlow', { channel: this }));
58
69
  });
59
70
 
60
71
  this.#dataChannel.onMessage((data) => {
@@ -62,7 +73,7 @@ export default class _RTCDataChannel extends EventTarget {
62
73
  data = data.buffer;
63
74
  }
64
75
 
65
- this.dispatchEvent(new MessageEvent('message', { data }));
76
+ this.dispatchEvent(new MessageEvent('message', { data, channel: this }));
66
77
  });
67
78
 
68
79
  // forward events to properties
@@ -148,9 +159,8 @@ export default class _RTCDataChannel extends EventTarget {
148
159
 
149
160
  send(data) {
150
161
  if (this.#readyState !== 'open') {
151
- throw new DOMException(
162
+ throw exceptions.InvalidStateError(
152
163
  "Failed to execute 'send' on 'RTCDataChannel': RTCDataChannel.readyState is not 'open'",
153
- 'InvalidStateError',
154
164
  );
155
165
  }
156
166
 
@@ -167,9 +177,7 @@ export default class _RTCDataChannel extends EventTarget {
167
177
  }
168
178
 
169
179
  close() {
170
- this.#readyState = 'closing';
171
- this.dispatchEvent(new Event('closing'));
172
-
180
+ this.#closeRequested = true;
173
181
  this.#dataChannel.close();
174
182
  }
175
183
  }
@@ -0,0 +1,11 @@
1
+ /// <reference lib="dom" />
2
+
3
+ export class _RTCError extends DOMException implements RTCError {
4
+ constructor(init: RTCErrorInit, message?: string);
5
+ // type RTCErrorDetailType = "data-channel-failure" | "dtls-failure" | "fingerprint-failure" | "hardware-encoder-error" | "hardware-encoder-not-available" | "sctp-failure" | "sdp-syntax-error";
6
+ readonly errorDetail: RTCErrorDetailType;
7
+ readonly receivedAlert: number | null;
8
+ readonly sctpCauseCode: number | null;
9
+ readonly sdpLineNumber: number | null;
10
+ readonly sentAlert: number | null;
11
+ }
@@ -0,0 +1,65 @@
1
+ export default class RTCError extends DOMException {
2
+ constructor(init, message = '') {
3
+ super(message, 'OperationError');
4
+
5
+ if (!init || !init.errorDetail) throw new TypeError('Cannot construct RTCError, errorDetail is required');
6
+ if (
7
+ [
8
+ 'data-channel-failure',
9
+ 'dtls-failure',
10
+ 'fingerprint-failure',
11
+ 'hardware-encoder-error',
12
+ 'hardware-encoder-not-available',
13
+ 'sctp-failure',
14
+ 'sdp-syntax-error',
15
+ ].indexOf(init.errorDetail) === -1
16
+ )
17
+ throw new TypeError('Cannot construct RTCError, errorDetail is invalid');
18
+
19
+ this._errorDetail = init.errorDetail;
20
+ this._receivedAlert = init.receivedAlert ?? null;
21
+ this._sctpCauseCode = init.sctpCauseCode ?? null;
22
+ this._sdpLineNumber = init.sdpLineNumber ?? null;
23
+ this._sentAlert = init.sentAlert ?? null;
24
+ }
25
+
26
+ get errorDetail() {
27
+ return this._errorDetail;
28
+ }
29
+
30
+ set errorDetail(value) {
31
+ throw new TypeError('Cannot set errorDetail, it is read-only');
32
+ }
33
+
34
+ get receivedAlert() {
35
+ return this._receivedAlert;
36
+ }
37
+
38
+ set receivedAlert(value) {
39
+ throw new TypeError('Cannot set receivedAlert, it is read-only');
40
+ }
41
+
42
+ get sctpCauseCode() {
43
+ return this._sctpCauseCode;
44
+ }
45
+
46
+ set sctpCauseCode(value) {
47
+ throw new TypeError('Cannot set sctpCauseCode, it is read-only');
48
+ }
49
+
50
+ get sdpLineNumber() {
51
+ return this._sdpLineNumber;
52
+ }
53
+
54
+ set sdpLineNumber(value) {
55
+ throw new TypeError('Cannot set sdpLineNumber, it is read-only');
56
+ }
57
+
58
+ get sentAlert() {
59
+ return this._sentAlert;
60
+ }
61
+
62
+ set sentAlert(value) {
63
+ throw new TypeError('Cannot set sentAlert, it is read-only');
64
+ }
65
+ }
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Example: candidate:123456 1 UDP 123456 192.168.1.1 12345 typ host raddr=10.0.0.1 rport=54321 generation 0
4
4
 
5
- import DOMException from 'node-domexception';
5
+ import 'node-domexception';
6
6
 
7
7
  export default class _RTCIceCandidate {
8
8
  #address;
@@ -19,41 +19,40 @@ export default class _RTCIceCandidate {
19
19
  #tcpType;
20
20
  #type;
21
21
  #usernameFragment;
22
- #ip;
23
22
 
24
23
  constructor({ candidate, sdpMLineIndex, sdpMid, usernameFragment }) {
25
- if (candidate == null) {
26
- throw new DOMException('candidate must be specified');
27
- }
24
+ if (sdpMLineIndex == null && sdpMid == null)
25
+ throw new TypeError('At least one of sdpMLineIndex or sdpMid must be specified');
28
26
 
29
- this.#candidate = candidate;
30
- this.#sdpMLineIndex = sdpMLineIndex || null;
31
- this.#sdpMid = sdpMid || null;
32
- this.#usernameFragment = usernameFragment || null;
27
+ this.#candidate = candidate === null ? 'null' : candidate ?? '';
28
+ this.#sdpMLineIndex = sdpMLineIndex ?? null;
29
+ this.#sdpMid = sdpMid ?? null;
30
+ this.#usernameFragment = usernameFragment ?? null;
33
31
 
34
32
  if (candidate) {
35
33
  const fields = candidate.split(' ');
36
- this.#foundation = fields[0];
34
+ this.#foundation = fields[0].replace('candidate:', ''); // remove text candidate:
37
35
  this.#component = fields[1] == '1' ? 'rtp' : 'rtcp';
38
36
  this.#protocol = fields[2];
39
37
  this.#priority = parseInt(fields[3], 10);
40
- this.#ip = fields[4];
38
+ this.#address = fields[4];
41
39
  this.#port = parseInt(fields[5], 10);
42
40
  this.#type = fields[7];
43
- if (fields[6] === 'typ') {
44
- this.#tcpType = null;
45
- } else if (fields[6] === 'tcp') {
46
- this.#tcpType = fields[7];
47
- this.#type = fields[8];
48
- }
41
+ this.#tcpType = null;
42
+ this.#relatedAddress = null;
43
+ this.#relatedPort = null;
49
44
 
50
45
  // Parse the candidate string to extract relatedPort and relatedAddress
51
- for (let i = 9; i < fields.length; i++) {
46
+ for (let i = 8; i < fields.length; i++) {
52
47
  const field = fields[i];
53
- if (field.startsWith('raddr')) {
54
- this.#relatedAddress = field.split('=')[1];
55
- } else if (field.startsWith('rport')) {
56
- this.#relatedPort = parseInt(field.split('=')[1], 10);
48
+ if (field === 'raddr') {
49
+ this.#relatedAddress = fields[i + 1];
50
+ } else if (field === 'rport') {
51
+ this.#relatedPort = parseInt(fields[i + 1], 10);
52
+ }
53
+
54
+ if (this.#protocol === 'tcp' && field === 'tcptype') {
55
+ this.#tcpType = fields[i + 1];
57
56
  }
58
57
  }
59
58
  }
@@ -64,7 +63,7 @@ export default class _RTCIceCandidate {
64
63
  }
65
64
 
66
65
  get candidate() {
67
- return this.#candidate || '';
66
+ return this.#candidate;
68
67
  }
69
68
 
70
69
  get component() {