@vouchfor/embeds 0.0.0-experiment.b77073f → 0.0.0-experiment.b7c103f
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/es/components/Embed/controllers/tracking.d.ts +2 -0
- package/dist/es/components/Embed/index.d.ts +3 -1
- package/dist/es/embeds.js +894 -592
- package/dist/es/embeds.js.map +1 -1
- package/dist/es/utils/env.d.ts +2 -8
- package/dist/iife/embeds.iife.js +220 -211
- package/dist/iife/embeds.iife.js.map +1 -1
- package/package.json +4 -4
- package/src/components/Embed/Embed.stories.ts +3 -3
- package/src/components/Embed/controllers/fetcher.ts +53 -11
- package/src/components/Embed/controllers/tracking.ts +73 -28
- package/src/components/Embed/index.ts +3 -0
- package/src/utils/env.ts +15 -29
- package/dist/es/components/Embed/controllers/event-forwarder.d.ts +0 -14
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@vouchfor/embeds",
|
3
|
-
"version": "0.0.0-experiment.
|
3
|
+
"version": "0.0.0-experiment.b7c103f",
|
4
4
|
"license": "MIT",
|
5
5
|
"author": "Aaron Williams",
|
6
6
|
"main": "dist/es/embeds.js",
|
@@ -36,11 +36,11 @@
|
|
36
36
|
},
|
37
37
|
"dependencies": {
|
38
38
|
"@lit/task": "^1.0.0",
|
39
|
-
"@vouchfor/media-player": "0.0.0-experiment.
|
39
|
+
"@vouchfor/media-player": "0.0.0-experiment.b7c103f",
|
40
40
|
"uuid": "^9.0.1"
|
41
41
|
},
|
42
42
|
"peerDependencies": {
|
43
|
-
"lit": "^3.0
|
43
|
+
"lit": "^3.1.0"
|
44
44
|
},
|
45
45
|
"devDependencies": {
|
46
46
|
"@esm-bundle/chai": "^4.3.4-fix.0",
|
@@ -62,7 +62,7 @@
|
|
62
62
|
"eslint": "^8.50.0",
|
63
63
|
"eslint-plugin-import": "^2.28.1",
|
64
64
|
"lint-staged": "^14.0.1",
|
65
|
-
"lit": "^
|
65
|
+
"lit": "^3.1.0",
|
66
66
|
"prettier": "^3.0.3",
|
67
67
|
"react": "^18.2.0",
|
68
68
|
"react-dom": "^18.2.0",
|
@@ -39,10 +39,10 @@ type Story = StoryObj<EmbedArgs>;
|
|
39
39
|
|
40
40
|
const Embed: Story = {
|
41
41
|
args: {
|
42
|
-
env: '
|
42
|
+
env: 'local',
|
43
43
|
apiKey: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
|
44
44
|
vouchId: '6JQEIPeStt',
|
45
|
-
templateId: '
|
45
|
+
templateId: '357fc118-e179-4171-9446-ff2b8e9d1b29',
|
46
46
|
aspectRatio: 0,
|
47
47
|
preload: 'none',
|
48
48
|
autoplay: false
|
@@ -50,7 +50,7 @@ const Embed: Story = {
|
|
50
50
|
argTypes: {
|
51
51
|
env: {
|
52
52
|
control: 'radio',
|
53
|
-
options: ['
|
53
|
+
options: ['local', 'dev', 'staging', 'prod']
|
54
54
|
},
|
55
55
|
preload: {
|
56
56
|
control: 'radio',
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { Task } from '@lit/task';
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
2
3
|
|
3
4
|
import type { Embed, EmbedProps } from '..';
|
4
5
|
import type { ReactiveControllerHost } from 'lit';
|
@@ -31,26 +32,67 @@ class FetcherController {
|
|
31
32
|
return this._fetching;
|
32
33
|
}
|
33
34
|
|
34
|
-
private getVouch = (env: Environment, apiKey: string, vouchId: string) => {
|
35
|
+
private getVouch = async (env: Environment, apiKey: string, vouchId: string) => {
|
35
36
|
const { embedApiUrl } = getEnvUrls(env);
|
36
37
|
|
37
|
-
|
38
|
+
const cacheCheck = uuidv4();
|
39
|
+
const res = await fetch(`${embedApiUrl}/vouches/${vouchId}`, {
|
38
40
|
method: 'GET',
|
39
|
-
headers: [
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
headers: [
|
42
|
+
['X-Api-Key', apiKey],
|
43
|
+
['X-Cache-Check', cacheCheck]
|
44
|
+
]
|
43
45
|
});
|
46
|
+
|
47
|
+
const vouch = await res.json();
|
48
|
+
this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
|
49
|
+
|
50
|
+
// HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
|
51
|
+
// so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
|
52
|
+
// API call with the `Cache-Control` header which will re-fill the cache
|
53
|
+
const resCacheCheck = res?.headers?.get('X-Cache-Check');
|
54
|
+
if (resCacheCheck && resCacheCheck !== cacheCheck) {
|
55
|
+
fetch(`${embedApiUrl}/vouches/${vouchId}`, {
|
56
|
+
method: 'GET',
|
57
|
+
headers: [
|
58
|
+
['X-Api-Key', apiKey],
|
59
|
+
['Cache-Control', 'max-age=0']
|
60
|
+
]
|
61
|
+
});
|
62
|
+
}
|
63
|
+
|
64
|
+
return vouch;
|
44
65
|
};
|
45
66
|
|
46
|
-
private async
|
67
|
+
private getTemplate = async (env: Environment, apiKey: string, templateId: string) => {
|
47
68
|
const { embedApiUrl } = getEnvUrls(env);
|
48
69
|
|
49
|
-
|
70
|
+
const cacheCheck = uuidv4();
|
71
|
+
const res = await fetch(`${embedApiUrl}/templates/${templateId}`, {
|
50
72
|
method: 'GET',
|
51
|
-
headers: [
|
52
|
-
|
53
|
-
|
73
|
+
headers: [
|
74
|
+
['X-Api-Key', apiKey],
|
75
|
+
['X-Cache-Check', cacheCheck]
|
76
|
+
]
|
77
|
+
});
|
78
|
+
const template = await res.json();
|
79
|
+
|
80
|
+
// HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
|
81
|
+
// so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
|
82
|
+
// API call with the `Cache-Control` header which will re-fill the cache
|
83
|
+
const resCacheCheck = res?.headers?.get('X-Cache-Check');
|
84
|
+
if (resCacheCheck && resCacheCheck !== cacheCheck) {
|
85
|
+
fetch(`${embedApiUrl}/templates/${templateId}`, {
|
86
|
+
method: 'GET',
|
87
|
+
headers: [
|
88
|
+
['X-Api-Key', apiKey],
|
89
|
+
['Cache-Control', 'max-age=0']
|
90
|
+
]
|
91
|
+
});
|
92
|
+
}
|
93
|
+
|
94
|
+
return template;
|
95
|
+
};
|
54
96
|
|
55
97
|
constructor(host: EmbedHost) {
|
56
98
|
this.host = host;
|
@@ -1,3 +1,4 @@
|
|
1
|
+
/* eslint-disable max-lines */
|
1
2
|
import { v4 as uuidv4 } from 'uuid';
|
2
3
|
|
3
4
|
import type { Embed } from '..';
|
@@ -6,7 +7,7 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
6
7
|
|
7
8
|
import { getEnvUrls } from '~/utils/env';
|
8
9
|
|
9
|
-
const STREAMED_THROTTLE =
|
10
|
+
const STREAMED_THROTTLE = 10000;
|
10
11
|
|
11
12
|
type EmbedHost = ReactiveControllerHost & Embed;
|
12
13
|
|
@@ -37,6 +38,7 @@ class TrackingController implements ReactiveController {
|
|
37
38
|
private _hasLoaded: BooleanMap = {};
|
38
39
|
private _answersViewed: BooleanMap = {};
|
39
40
|
private _streamedTime: TimeMap = {};
|
41
|
+
private _streamLatestTime: TimeMap = {};
|
40
42
|
private _streamedPrevTimestamp: TimeMap = {};
|
41
43
|
|
42
44
|
constructor(host: EmbedHost) {
|
@@ -45,11 +47,11 @@ class TrackingController implements ReactiveController {
|
|
45
47
|
}
|
46
48
|
|
47
49
|
private _findVouchId() {
|
48
|
-
if (this.host.
|
49
|
-
if ('uuid' in this.host.
|
50
|
-
return this.host.
|
50
|
+
if (this.host.vouch) {
|
51
|
+
if ('uuid' in this.host.vouch) {
|
52
|
+
return this.host.vouch.uuid;
|
51
53
|
}
|
52
|
-
return this.host.
|
54
|
+
return this.host.vouch.id;
|
53
55
|
}
|
54
56
|
}
|
55
57
|
|
@@ -132,7 +134,10 @@ class TrackingController implements ReactiveController {
|
|
132
134
|
const { publicApiUrl } = getEnvUrls(this.host.env);
|
133
135
|
const { client, tab, request, visitor } = this._getUids();
|
134
136
|
|
135
|
-
|
137
|
+
if (this.host.disableTracking) {
|
138
|
+
return;
|
139
|
+
}
|
140
|
+
|
136
141
|
navigator.sendBeacon(
|
137
142
|
`${publicApiUrl}/api/events`,
|
138
143
|
JSON.stringify({
|
@@ -180,63 +185,101 @@ class TrackingController implements ReactiveController {
|
|
180
185
|
}
|
181
186
|
};
|
182
187
|
|
183
|
-
private _handleVideoPlay = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
|
188
|
+
private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
|
184
189
|
const vouchId = this._findVouchId();
|
190
|
+
|
185
191
|
if (!vouchId) {
|
186
192
|
return;
|
187
193
|
}
|
194
|
+
|
188
195
|
// Only increment play count once per session
|
189
|
-
if (!this._answersViewed[
|
196
|
+
if (!this._answersViewed[key]) {
|
190
197
|
this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
|
191
198
|
vouchId,
|
192
199
|
answerId: id
|
193
200
|
});
|
194
|
-
this._answersViewed[
|
201
|
+
this._answersViewed[key] = true;
|
195
202
|
}
|
196
|
-
|
197
|
-
this.
|
203
|
+
|
204
|
+
this._streamedTime[key] = node.currentTime;
|
205
|
+
this._streamLatestTime[key] = node.currentTime;
|
206
|
+
this._streamedPrevTimestamp[key] = Date.now();
|
198
207
|
};
|
199
208
|
|
200
|
-
private
|
209
|
+
private _handleVideoSeeking = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
|
201
210
|
const vouchId = this._findVouchId();
|
211
|
+
|
202
212
|
if (!vouchId) {
|
203
213
|
return;
|
204
214
|
}
|
215
|
+
|
216
|
+
if (this._streamLatestTime[key]) {
|
217
|
+
this._sendTrackingEvent('VIDEO_STREAMED', {
|
218
|
+
vouchId,
|
219
|
+
answerId: id,
|
220
|
+
streamStart: this._streamedTime[key],
|
221
|
+
streamEnd: this._streamLatestTime[key]
|
222
|
+
});
|
223
|
+
}
|
224
|
+
|
225
|
+
delete this._streamedTime[key];
|
226
|
+
delete this._streamLatestTime[key];
|
227
|
+
delete this._streamedPrevTimestamp[key];
|
228
|
+
};
|
229
|
+
|
230
|
+
private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
|
231
|
+
const vouchId = this._findVouchId();
|
232
|
+
|
233
|
+
if (!vouchId) {
|
234
|
+
return;
|
235
|
+
}
|
236
|
+
|
205
237
|
const currentTimestamp = Date.now();
|
206
238
|
if (
|
239
|
+
node.currentTime &&
|
240
|
+
!node.paused &&
|
207
241
|
!this.host.paused &&
|
208
242
|
// Only fire the video seeked event when this video is the active one
|
209
243
|
id === this.host.scene?.video?.id &&
|
210
244
|
// Throttle the frequency that we send streamed events while playing
|
211
|
-
currentTimestamp - this._streamedPrevTimestamp[
|
245
|
+
currentTimestamp - this._streamedPrevTimestamp[key] > STREAMED_THROTTLE
|
212
246
|
) {
|
213
247
|
this._sendTrackingEvent('VIDEO_STREAMED', {
|
214
248
|
vouchId,
|
215
249
|
answerId: id,
|
216
|
-
streamStart: this._streamedTime[
|
250
|
+
streamStart: this._streamedTime[key],
|
217
251
|
streamEnd: node.currentTime
|
218
252
|
});
|
219
|
-
this._streamedTime[
|
220
|
-
this._streamedPrevTimestamp[
|
253
|
+
this._streamedTime[key] = node.currentTime;
|
254
|
+
this._streamedPrevTimestamp[key] = currentTimestamp;
|
221
255
|
}
|
256
|
+
|
257
|
+
this._streamLatestTime[key] = node.currentTime;
|
222
258
|
};
|
223
259
|
|
224
|
-
private _handleVideoPause = ({ detail: { id, node } }: CustomEvent<VideoEventDetail>) => {
|
260
|
+
private _handleVideoPause = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
|
225
261
|
const vouchId = this._findVouchId();
|
262
|
+
|
226
263
|
if (!vouchId) {
|
227
264
|
return;
|
228
265
|
}
|
229
|
-
|
230
|
-
//
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
266
|
+
|
267
|
+
// Don't send a tracking event if the video pauses when seeking backwards
|
268
|
+
if (node.currentTime > this._streamedTime[key]) {
|
269
|
+
// Send a video streamed event any time the video pauses then reset the streamed state
|
270
|
+
// We do this to capture the last bit of time that the video was played between the previous
|
271
|
+
// stream event and the video being paused manually or stopping because it ended
|
272
|
+
this._sendTrackingEvent('VIDEO_STREAMED', {
|
273
|
+
vouchId,
|
274
|
+
answerId: id,
|
275
|
+
streamStart: this._streamedTime[key],
|
276
|
+
streamEnd: node.currentTime
|
277
|
+
});
|
278
|
+
}
|
279
|
+
|
280
|
+
delete this._streamedTime[key];
|
281
|
+
delete this._streamLatestTime[key];
|
282
|
+
delete this._streamedPrevTimestamp[key];
|
240
283
|
};
|
241
284
|
|
242
285
|
hostConnected() {
|
@@ -244,6 +287,7 @@ class TrackingController implements ReactiveController {
|
|
244
287
|
this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
|
245
288
|
this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
|
246
289
|
this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
|
290
|
+
this.host.mediaPlayer?.addEventListener('video:seeking', this._handleVideoSeeking);
|
247
291
|
this.host.mediaPlayer?.addEventListener('video:pause', this._handleVideoPause);
|
248
292
|
this.host.mediaPlayer?.addEventListener('video:timeupdate', this._handleVideoTimeUpdate);
|
249
293
|
});
|
@@ -253,6 +297,7 @@ class TrackingController implements ReactiveController {
|
|
253
297
|
this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
|
254
298
|
this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
|
255
299
|
this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
|
300
|
+
this.host.mediaPlayer?.removeEventListener('video:seeking', this._handleVideoSeeking);
|
256
301
|
this.host.mediaPlayer?.removeEventListener('video:pause', this._handleVideoPause);
|
257
302
|
this.host.mediaPlayer?.removeEventListener('video:timeupdate', this._handleVideoTimeUpdate);
|
258
303
|
}
|
@@ -17,6 +17,7 @@ import '@vouchfor/media-player';
|
|
17
17
|
type EmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'autoplay' | 'controls'> & {
|
18
18
|
env: Environment;
|
19
19
|
apiKey: string;
|
20
|
+
disableTracking?: boolean;
|
20
21
|
trackingSource?: string;
|
21
22
|
vouchId?: string;
|
22
23
|
templateId?: string;
|
@@ -32,6 +33,7 @@ class Embed extends LitElement {
|
|
32
33
|
|
33
34
|
@property({ type: String }) env: EmbedProps['env'] = 'prod';
|
34
35
|
@property({ type: String }) apiKey: EmbedProps['apiKey'] = '';
|
36
|
+
@property({ type: Boolean }) disableTracking: EmbedProps['disableTracking'] = false;
|
35
37
|
@property({ type: String }) trackingSource: EmbedProps['trackingSource'] = 'embed';
|
36
38
|
|
37
39
|
@property({ type: Array }) controls: EmbedProps['controls'];
|
@@ -57,6 +59,7 @@ class Embed extends LitElement {
|
|
57
59
|
'waiting',
|
58
60
|
|
59
61
|
'video:loadeddata',
|
62
|
+
'video:seeking',
|
60
63
|
'video:seeked',
|
61
64
|
'video:play',
|
62
65
|
'video:playing',
|
package/src/utils/env.ts
CHANGED
@@ -1,15 +1,11 @@
|
|
1
|
-
type Environment = 'dev' | 'staging' | 'prod';
|
1
|
+
type Environment = 'local' | 'dev' | 'staging' | 'prod';
|
2
2
|
|
3
3
|
type GetEnvUrlsReturn = {
|
4
|
-
marketingUrl: string;
|
5
4
|
videoUrl: string;
|
6
5
|
publicApiUrl: string;
|
7
6
|
embedApiUrl: string;
|
8
|
-
publicRecorderUrl: string;
|
9
7
|
};
|
10
8
|
|
11
|
-
const marketingUrl = 'https://vouchfor.com';
|
12
|
-
|
13
9
|
const devVideoUrl = 'https://d2rxhdlm2q91uk.cloudfront.net';
|
14
10
|
const stagingVideoUrl = 'https://d1ix11aj5kfygl.cloudfront.net';
|
15
11
|
const prodVideoUrl = 'https://d157jlwnudd93d.cloudfront.net';
|
@@ -18,61 +14,51 @@ const devPublicApiUrl = 'https://bshyfw4h5a.execute-api.ap-southeast-2.amazonaws
|
|
18
14
|
const stagingPublicApiUrl = 'https://gyzw7rpbq3.execute-api.ap-southeast-2.amazonaws.com/staging';
|
19
15
|
const prodPublicApiUrl = 'https://vfcjuim1l3.execute-api.ap-southeast-2.amazonaws.com/prod';
|
20
16
|
|
17
|
+
const localEmbedApiUrl = 'http://localhost:6060/v2';
|
21
18
|
const devEmbedApiUrl = 'https://embed-dev.vouchfor.com/v2';
|
22
19
|
const stagingEmbedApiUrl = 'https://embed-staging.vouchfor.com/v2';
|
23
20
|
const prodEmbedApiUrl = 'https://embed.vouchfor.com/v2';
|
24
21
|
|
25
|
-
const devPublicRecorderUrl = 'https://dev.vouchfor.com';
|
26
|
-
const stagingPublicRecorderUrl = 'https://staging.vouchfor.com';
|
27
|
-
const prodPublicRecorderUrl = 'https://app.vouchfor.com';
|
28
|
-
|
29
22
|
// We are handling the case where env is an unknown string so the ts error is a lie
|
30
23
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
31
24
|
// @ts-ignore
|
32
25
|
function getEnvUrls(env: Environment): GetEnvUrlsReturn {
|
33
|
-
if (!['dev', 'staging', 'prod'].includes(env)) {
|
26
|
+
if (!['local', 'dev', 'staging', 'prod'].includes(env)) {
|
34
27
|
throw new Error(`Unknown environment: ${env}`);
|
35
28
|
}
|
36
29
|
|
30
|
+
if (env === 'local') {
|
31
|
+
return {
|
32
|
+
videoUrl: devVideoUrl,
|
33
|
+
publicApiUrl: devPublicApiUrl,
|
34
|
+
embedApiUrl: localEmbedApiUrl
|
35
|
+
};
|
36
|
+
}
|
37
|
+
|
37
38
|
if (env === 'dev') {
|
38
39
|
return {
|
39
|
-
marketingUrl,
|
40
40
|
videoUrl: devVideoUrl,
|
41
41
|
publicApiUrl: devPublicApiUrl,
|
42
|
-
embedApiUrl: devEmbedApiUrl
|
43
|
-
publicRecorderUrl: devPublicRecorderUrl
|
42
|
+
embedApiUrl: devEmbedApiUrl
|
44
43
|
};
|
45
44
|
}
|
46
45
|
|
47
46
|
if (env === 'staging') {
|
48
47
|
return {
|
49
|
-
marketingUrl,
|
50
48
|
videoUrl: stagingVideoUrl,
|
51
49
|
publicApiUrl: stagingPublicApiUrl,
|
52
|
-
embedApiUrl: stagingEmbedApiUrl
|
53
|
-
publicRecorderUrl: stagingPublicRecorderUrl
|
50
|
+
embedApiUrl: stagingEmbedApiUrl
|
54
51
|
};
|
55
52
|
}
|
56
53
|
|
57
54
|
if (env === 'prod') {
|
58
55
|
return {
|
59
|
-
marketingUrl,
|
60
56
|
videoUrl: prodVideoUrl,
|
61
57
|
publicApiUrl: prodPublicApiUrl,
|
62
|
-
embedApiUrl: prodEmbedApiUrl
|
63
|
-
publicRecorderUrl: prodPublicRecorderUrl
|
58
|
+
embedApiUrl: prodEmbedApiUrl
|
64
59
|
};
|
65
60
|
}
|
66
61
|
}
|
67
62
|
|
68
|
-
export {
|
69
|
-
marketingUrl,
|
70
|
-
devEmbedApiUrl,
|
71
|
-
stagingEmbedApiUrl,
|
72
|
-
prodEmbedApiUrl,
|
73
|
-
devPublicRecorderUrl,
|
74
|
-
stagingPublicRecorderUrl,
|
75
|
-
prodPublicRecorderUrl,
|
76
|
-
getEnvUrls
|
77
|
-
};
|
63
|
+
export { devEmbedApiUrl, stagingEmbedApiUrl, prodEmbedApiUrl, getEnvUrls };
|
78
64
|
export type { Environment };
|
@@ -1,14 +0,0 @@
|
|
1
|
-
import type { Embed } from '..';
|
2
|
-
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
3
|
-
type EmbedHost = ReactiveControllerHost & Embed;
|
4
|
-
declare class EventForwardController implements ReactiveController {
|
5
|
-
host: EmbedHost;
|
6
|
-
private _events;
|
7
|
-
private _cleanup;
|
8
|
-
private _forwardElementRef;
|
9
|
-
constructor(host: EmbedHost, events: string[]);
|
10
|
-
register(): import("lit-html/directive.js").DirectiveResult<typeof import("lit-html/directives/ref.js").RefDirective>;
|
11
|
-
hostConnected(): void;
|
12
|
-
hostDisconnected(): void;
|
13
|
-
}
|
14
|
-
export { EventForwardController };
|