react-native-update 10.38.3 → 10.38.4

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.
@@ -0,0 +1,8 @@
1
+ #ifndef _DOWNLOAD_TASK_H_
2
+ #define _DOWNLOAD_TASK_H_
3
+
4
+ #include <napi/native_api.h>
5
+
6
+ napi_value HdiffPatch(napi_env env, napi_callback_info info);
7
+
8
+ #endif // _DOWNLOAD_TASK_H_
@@ -0,0 +1,610 @@
1
+ import http from '@ohos.net.http';
2
+ import fileIo from '@ohos.file.fs';
3
+ import common from '@ohos.app.ability.common';
4
+ import { zlib } from '@kit.BasicServicesKit';
5
+ import { EventHub } from './EventHub';
6
+ import { DownloadTaskParams } from './DownloadTaskParams';
7
+ import { saveFileToSandbox } from './SaveFile';
8
+ import { util } from '@kit.ArkTS';
9
+ import NativePatchCore, {
10
+ ARCHIVE_PATCH_TYPE_FROM_PACKAGE,
11
+ ARCHIVE_PATCH_TYPE_FROM_PPK,
12
+ CopyGroupResult,
13
+ } from './NativePatchCore';
14
+
15
+ interface PatchManifestArrays {
16
+ copyFroms: string[];
17
+ copyTos: string[];
18
+ deletes: string[];
19
+ }
20
+
21
+ const DIFF_MANIFEST_ENTRY = '__diff.json';
22
+ const HARMONY_BUNDLE_PATCH_ENTRY = 'bundle.harmony.js.patch';
23
+ const TEMP_ORIGIN_BUNDLE_ENTRY = '.origin.bundle.harmony.js';
24
+ const FILE_COPY_BUFFER_SIZE = 64 * 1024;
25
+
26
+ export class DownloadTask {
27
+ private context: common.Context;
28
+ private hash: string;
29
+ private eventHub: EventHub;
30
+
31
+ constructor(context: common.Context) {
32
+ this.context = context;
33
+ this.eventHub = EventHub.getInstance();
34
+ }
35
+
36
+ private async removeDirectory(path: string): Promise<void> {
37
+ try {
38
+ const res = fileIo.accessSync(path);
39
+ if (res) {
40
+ const stat = await fileIo.stat(path);
41
+ if (stat.isDirectory()) {
42
+ const files = await fileIo.listFile(path);
43
+ for (const file of files) {
44
+ if (file === '.' || file === '..') {
45
+ continue;
46
+ }
47
+ await this.removeDirectory(`${path}/${file}`);
48
+ }
49
+ await fileIo.rmdir(path);
50
+ } else {
51
+ await fileIo.unlink(path);
52
+ }
53
+ }
54
+ } catch (error) {
55
+ console.error('Failed to delete directory:', error);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ private async ensureDirectory(path: string): Promise<void> {
61
+ if (!path || fileIo.accessSync(path)) {
62
+ return;
63
+ }
64
+
65
+ const parentPath = path.substring(0, path.lastIndexOf('/'));
66
+ if (parentPath && parentPath !== path) {
67
+ await this.ensureDirectory(parentPath);
68
+ }
69
+
70
+ if (!fileIo.accessSync(path)) {
71
+ await fileIo.mkdir(path);
72
+ }
73
+ }
74
+
75
+ private async ensureParentDirectory(filePath: string): Promise<void> {
76
+ const parentPath = filePath.substring(0, filePath.lastIndexOf('/'));
77
+ if (!parentPath) {
78
+ return;
79
+ }
80
+ await this.ensureDirectory(parentPath);
81
+ }
82
+
83
+ private async recreateDirectory(path: string): Promise<void> {
84
+ await this.removeDirectory(path);
85
+ await this.ensureDirectory(path);
86
+ }
87
+
88
+ private async readFileContent(filePath: string): Promise<ArrayBuffer> {
89
+ const stat = await fileIo.stat(filePath);
90
+ const reader = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
91
+ const content = new ArrayBuffer(stat.size);
92
+
93
+ try {
94
+ await fileIo.read(reader.fd, content);
95
+ return content;
96
+ } finally {
97
+ await fileIo.close(reader);
98
+ }
99
+ }
100
+
101
+ private async listEntryNames(directory: string): Promise<string[]> {
102
+ const entryNames: string[] = [];
103
+ const files = await fileIo.listFile(directory);
104
+
105
+ for (const file of files) {
106
+ if (file === '.' || file === '..') {
107
+ continue;
108
+ }
109
+
110
+ const filePath = `${directory}/${file}`;
111
+ const stat = await fileIo.stat(filePath);
112
+ if (!stat.isDirectory()) {
113
+ entryNames.push(file);
114
+ }
115
+ }
116
+
117
+ return entryNames;
118
+ }
119
+
120
+ private async writeFileContent(
121
+ targetFile: string,
122
+ content: ArrayBuffer | Uint8Array,
123
+ ): Promise<void> {
124
+ const payload =
125
+ content instanceof Uint8Array ? content : new Uint8Array(content);
126
+ await this.ensureParentDirectory(targetFile);
127
+ if (fileIo.accessSync(targetFile)) {
128
+ await fileIo.unlink(targetFile);
129
+ }
130
+
131
+ let writer: fileIo.File | null = null;
132
+ try {
133
+ writer = await fileIo.open(
134
+ targetFile,
135
+ fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
136
+ );
137
+ const chunkSize = FILE_COPY_BUFFER_SIZE;
138
+ let bytesWritten = 0;
139
+
140
+ while (bytesWritten < payload.byteLength) {
141
+ const chunk = payload.subarray(bytesWritten, bytesWritten + chunkSize);
142
+ await fileIo.write(writer.fd, chunk);
143
+ bytesWritten += chunk.byteLength;
144
+ }
145
+ } finally {
146
+ if (writer) {
147
+ await fileIo.close(writer);
148
+ }
149
+ }
150
+ }
151
+
152
+ private parseJsonEntry(content: ArrayBuffer): Record<string, any> {
153
+ return JSON.parse(
154
+ new util.TextDecoder().decodeToString(new Uint8Array(content)),
155
+ ) as Record<string, any>;
156
+ }
157
+
158
+ private async readManifestArrays(
159
+ directory: string,
160
+ normalizeResourceCopies: boolean,
161
+ ): Promise<PatchManifestArrays> {
162
+ const manifestPath = `${directory}/${DIFF_MANIFEST_ENTRY}`;
163
+ if (!fileIo.accessSync(manifestPath)) {
164
+ return {
165
+ copyFroms: [],
166
+ copyTos: [],
167
+ deletes: [],
168
+ };
169
+ }
170
+
171
+ return this.manifestToArrays(
172
+ this.parseJsonEntry(await this.readFileContent(manifestPath)),
173
+ normalizeResourceCopies,
174
+ );
175
+ }
176
+
177
+ private manifestToArrays(
178
+ manifest: Record<string, any>,
179
+ normalizeResourceCopies: boolean,
180
+ ): PatchManifestArrays {
181
+ const copyFroms: string[] = [];
182
+ const copyTos: string[] = [];
183
+ const deletesValue = manifest.deletes;
184
+ const deletes = Array.isArray(deletesValue)
185
+ ? deletesValue.map(item => String(item))
186
+ : deletesValue && typeof deletesValue === 'object'
187
+ ? Object.keys(deletesValue)
188
+ : [];
189
+
190
+ const copies = (manifest.copies || {}) as Record<string, string>;
191
+ for (const [to, rawFrom] of Object.entries(copies)) {
192
+ let from = String(rawFrom || '');
193
+ if (normalizeResourceCopies) {
194
+ from = from.replace('resources/rawfile/', '');
195
+ if (!from) {
196
+ from = to;
197
+ }
198
+ }
199
+ copyFroms.push(from);
200
+ copyTos.push(to);
201
+ }
202
+
203
+ return {
204
+ copyFroms,
205
+ copyTos,
206
+ deletes,
207
+ };
208
+ }
209
+
210
+ private async applyBundlePatchFromFileSource(
211
+ originContent: ArrayBuffer,
212
+ workingDirectory: string,
213
+ bundlePatchPath: string,
214
+ outputFile: string,
215
+ ): Promise<void> {
216
+ const originBundlePath = `${workingDirectory}/${TEMP_ORIGIN_BUNDLE_ENTRY}`;
217
+ try {
218
+ await this.writeFileContent(originBundlePath, originContent);
219
+ NativePatchCore.applyPatchFromFileSource({
220
+ copyFroms: [],
221
+ copyTos: [],
222
+ deletes: [],
223
+ sourceRoot: workingDirectory,
224
+ targetRoot: workingDirectory,
225
+ originBundlePath,
226
+ bundlePatchPath,
227
+ bundleOutputPath: outputFile,
228
+ enableMerge: false,
229
+ });
230
+ } catch (error) {
231
+ error.message = `Failed to process bundle patch: ${error.message}`;
232
+ throw error;
233
+ } finally {
234
+ if (fileIo.accessSync(originBundlePath)) {
235
+ await fileIo.unlink(originBundlePath);
236
+ }
237
+ }
238
+ }
239
+
240
+ private async copySandboxFile(
241
+ sourceFile: string,
242
+ targetFile: string,
243
+ ): Promise<void> {
244
+ let reader: fileIo.File | null = null;
245
+ let writer: fileIo.File | null = null;
246
+ const buffer = new ArrayBuffer(FILE_COPY_BUFFER_SIZE);
247
+ let offset = 0;
248
+
249
+ try {
250
+ reader = await fileIo.open(sourceFile, fileIo.OpenMode.READ_ONLY);
251
+ await this.ensureParentDirectory(targetFile);
252
+ if (fileIo.accessSync(targetFile)) {
253
+ await fileIo.unlink(targetFile);
254
+ }
255
+ writer = await fileIo.open(
256
+ targetFile,
257
+ fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
258
+ );
259
+
260
+ while (true) {
261
+ const readLength = await fileIo.read(reader.fd, buffer, {
262
+ offset,
263
+ length: FILE_COPY_BUFFER_SIZE,
264
+ });
265
+ if (readLength <= 0) {
266
+ break;
267
+ }
268
+
269
+ await fileIo.write(writer.fd, new Uint8Array(buffer, 0, readLength));
270
+ offset += readLength;
271
+
272
+ if (readLength < FILE_COPY_BUFFER_SIZE) {
273
+ break;
274
+ }
275
+ }
276
+ } finally {
277
+ if (reader) {
278
+ await fileIo.close(reader);
279
+ }
280
+ if (writer) {
281
+ await fileIo.close(writer);
282
+ }
283
+ }
284
+ }
285
+
286
+ private async downloadFile(params: DownloadTaskParams): Promise<void> {
287
+ const httpRequest = http.createHttp();
288
+ this.hash = params.hash;
289
+ let writer: fileIo.File | null = null;
290
+ let contentLength = 0;
291
+ let received = 0;
292
+ let writeError: Error | null = null;
293
+ let writeQueue = Promise.resolve();
294
+
295
+ const closeWriter = async () => {
296
+ if (writer) {
297
+ await fileIo.close(writer);
298
+ writer = null;
299
+ }
300
+ };
301
+
302
+ const dataEndPromise = new Promise<void>((resolve, reject) => {
303
+ httpRequest.on('dataEnd', () => {
304
+ writeQueue
305
+ .then(async () => {
306
+ if (writeError) {
307
+ throw writeError;
308
+ }
309
+ await closeWriter();
310
+ resolve();
311
+ })
312
+ .catch(async error => {
313
+ await closeWriter();
314
+ reject(error);
315
+ });
316
+ });
317
+ });
318
+
319
+ try {
320
+ let exists = fileIo.accessSync(params.targetFile);
321
+ if (exists) {
322
+ await fileIo.unlink(params.targetFile);
323
+ } else {
324
+ await this.ensureParentDirectory(params.targetFile);
325
+ }
326
+
327
+ writer = await fileIo.open(
328
+ params.targetFile,
329
+ fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE,
330
+ );
331
+
332
+ httpRequest.on('headersReceive', (header: Record<string, string>) => {
333
+ if (!header) {
334
+ return;
335
+ }
336
+ const lengthKey = Object.keys(header).find(
337
+ key => key.toLowerCase() === 'content-length',
338
+ );
339
+ if (!lengthKey) {
340
+ return;
341
+ }
342
+ const length = parseInt(header[lengthKey], 10);
343
+ if (!Number.isNaN(length)) {
344
+ contentLength = length;
345
+ }
346
+ });
347
+
348
+ httpRequest.on('dataReceive', (data: ArrayBuffer) => {
349
+ if (writeError) {
350
+ return;
351
+ }
352
+ received += data.byteLength;
353
+ writeQueue = writeQueue.then(async () => {
354
+ if (!writer || writeError) {
355
+ return;
356
+ }
357
+ try {
358
+ await fileIo.write(writer.fd, data);
359
+ } catch (error) {
360
+ writeError = error as Error;
361
+ }
362
+ });
363
+ this.onProgressUpdate(received, contentLength);
364
+ });
365
+
366
+ httpRequest.on(
367
+ 'dataReceiveProgress',
368
+ (data: http.DataReceiveProgressInfo) => {
369
+ if (data.totalSize > 0) {
370
+ contentLength = data.totalSize;
371
+ }
372
+ if (data.receiveSize > received) {
373
+ received = data.receiveSize;
374
+ }
375
+ this.onProgressUpdate(received, contentLength);
376
+ },
377
+ );
378
+
379
+ const responseCode = await httpRequest.requestInStream(params.url, {
380
+ method: http.RequestMethod.GET,
381
+ readTimeout: 60000,
382
+ connectTimeout: 60000,
383
+ header: {
384
+ 'Content-Type': 'application/octet-stream',
385
+ },
386
+ });
387
+ if (responseCode > 299) {
388
+ throw Error(`Server error: ${responseCode}`);
389
+ }
390
+
391
+ await dataEndPromise;
392
+ const stats = await fileIo.stat(params.targetFile);
393
+ const fileSize = stats.size;
394
+ if (contentLength > 0 && fileSize !== contentLength) {
395
+ throw Error(
396
+ `Download incomplete: expected ${contentLength} bytes but got ${stats.size} bytes`,
397
+ );
398
+ }
399
+ } catch (error) {
400
+ console.error('Download failed:', error);
401
+ throw error;
402
+ } finally {
403
+ try {
404
+ await closeWriter();
405
+ } catch (closeError) {
406
+ console.error('Failed to close file:', closeError);
407
+ }
408
+ httpRequest.off('headersReceive');
409
+ httpRequest.off('dataReceive');
410
+ httpRequest.off('dataReceiveProgress');
411
+ httpRequest.off('dataEnd');
412
+ httpRequest.destroy();
413
+ }
414
+ }
415
+
416
+ private onProgressUpdate(received: number, total: number): void {
417
+ this.eventHub.emit('RCTPushyDownloadProgress', {
418
+ received,
419
+ total,
420
+ hash: this.hash,
421
+ });
422
+ }
423
+
424
+ private async doFullPatch(params: DownloadTaskParams): Promise<void> {
425
+ await this.downloadFile(params);
426
+ await this.recreateDirectory(params.unzipDirectory);
427
+ await zlib.decompressFile(params.targetFile, params.unzipDirectory);
428
+ }
429
+
430
+ private async doPatchFromApp(params: DownloadTaskParams): Promise<void> {
431
+ await this.downloadFile(params);
432
+ await this.recreateDirectory(params.unzipDirectory);
433
+
434
+ await zlib.decompressFile(params.targetFile, params.unzipDirectory);
435
+ const entryNames = await this.listEntryNames(params.unzipDirectory);
436
+ const manifestArrays = await this.readManifestArrays(
437
+ params.unzipDirectory,
438
+ true,
439
+ );
440
+
441
+ NativePatchCore.buildArchivePatchPlan(
442
+ ARCHIVE_PATCH_TYPE_FROM_PACKAGE,
443
+ entryNames,
444
+ manifestArrays.copyFroms,
445
+ manifestArrays.copyTos,
446
+ manifestArrays.deletes,
447
+ HARMONY_BUNDLE_PATCH_ENTRY,
448
+ );
449
+
450
+ const bundlePatchPath = `${params.unzipDirectory}/${HARMONY_BUNDLE_PATCH_ENTRY}`;
451
+ if (!fileIo.accessSync(bundlePatchPath)) {
452
+ throw Error('bundle patch not found');
453
+ }
454
+ const resourceManager = this.context.resourceManager;
455
+ const originContent = await resourceManager.getRawFileContent(
456
+ 'bundle.harmony.js',
457
+ );
458
+ await this.applyBundlePatchFromFileSource(
459
+ originContent,
460
+ params.unzipDirectory,
461
+ bundlePatchPath,
462
+ `${params.unzipDirectory}/bundle.harmony.js`,
463
+ );
464
+ await this.copyFromResource(
465
+ NativePatchCore.buildCopyGroups(
466
+ manifestArrays.copyFroms,
467
+ manifestArrays.copyTos,
468
+ ),
469
+ params.unzipDirectory,
470
+ );
471
+ }
472
+
473
+ private async doPatchFromPpk(params: DownloadTaskParams): Promise<void> {
474
+ await this.downloadFile(params);
475
+ await this.recreateDirectory(params.unzipDirectory);
476
+
477
+ await zlib.decompressFile(params.targetFile, params.unzipDirectory);
478
+ const entryNames = await this.listEntryNames(params.unzipDirectory);
479
+ const manifestArrays = await this.readManifestArrays(
480
+ params.unzipDirectory,
481
+ false,
482
+ );
483
+
484
+ const plan = NativePatchCore.buildArchivePatchPlan(
485
+ ARCHIVE_PATCH_TYPE_FROM_PPK,
486
+ entryNames,
487
+ manifestArrays.copyFroms,
488
+ manifestArrays.copyTos,
489
+ manifestArrays.deletes,
490
+ HARMONY_BUNDLE_PATCH_ENTRY,
491
+ );
492
+ NativePatchCore.applyPatchFromFileSource({
493
+ copyFroms: manifestArrays.copyFroms,
494
+ copyTos: manifestArrays.copyTos,
495
+ deletes: manifestArrays.deletes,
496
+ sourceRoot: params.originDirectory,
497
+ targetRoot: params.unzipDirectory,
498
+ originBundlePath: `${params.originDirectory}/bundle.harmony.js`,
499
+ bundlePatchPath: `${params.unzipDirectory}/${HARMONY_BUNDLE_PATCH_ENTRY}`,
500
+ bundleOutputPath: `${params.unzipDirectory}/bundle.harmony.js`,
501
+ mergeSourceSubdir: plan.mergeSourceSubdir,
502
+ enableMerge: plan.enableMerge,
503
+ });
504
+ console.info('Patch from PPK completed');
505
+ }
506
+
507
+ private async copyFromResource(
508
+ copyGroups: CopyGroupResult[],
509
+ targetRoot: string,
510
+ ): Promise<void> {
511
+ let currentFrom = '';
512
+ try {
513
+ const resourceManager = this.context.resourceManager;
514
+
515
+ for (const group of copyGroups) {
516
+ currentFrom = group.from;
517
+ const targets = group.toPaths.map(path => `${targetRoot}/${path}`);
518
+ if (targets.length === 0) {
519
+ continue;
520
+ }
521
+
522
+ if (currentFrom.startsWith('resources/base/media/')) {
523
+ const mediaName = currentFrom
524
+ .replace('resources/base/media/', '')
525
+ .split('.')[0];
526
+ const mediaBuffer = await resourceManager.getMediaByName(mediaName);
527
+ const [firstTarget, ...restTargets] = targets;
528
+ await this.writeFileContent(firstTarget, mediaBuffer.buffer);
529
+ for (const target of restTargets) {
530
+ await this.copySandboxFile(firstTarget, target);
531
+ }
532
+ continue;
533
+ }
534
+ const fromContent = await resourceManager.getRawFd(currentFrom);
535
+ const [firstTarget, ...restTargets] = targets;
536
+ await this.ensureParentDirectory(firstTarget);
537
+ if (fileIo.accessSync(firstTarget)) {
538
+ await fileIo.unlink(firstTarget);
539
+ }
540
+ saveFileToSandbox(fromContent, firstTarget);
541
+ for (const target of restTargets) {
542
+ await this.copySandboxFile(firstTarget, target);
543
+ }
544
+ }
545
+ } catch (error) {
546
+ error.message =
547
+ 'Copy from resource failed:' +
548
+ currentFrom +
549
+ ',' +
550
+ error.code +
551
+ ',' +
552
+ error.message;
553
+ console.error(error);
554
+ throw error;
555
+ }
556
+ }
557
+
558
+ private async doCleanUp(params: DownloadTaskParams): Promise<void> {
559
+ try {
560
+ NativePatchCore.cleanupOldEntries(
561
+ params.unzipDirectory,
562
+ params.hash || '',
563
+ params.originHash || '',
564
+ 7,
565
+ );
566
+ } catch (error) {
567
+ error.message = 'Cleanup failed:' + error.message;
568
+ console.error(error);
569
+ throw error;
570
+ }
571
+ }
572
+
573
+ public async execute(params: DownloadTaskParams): Promise<void> {
574
+ try {
575
+ switch (params.type) {
576
+ case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
577
+ await this.doFullPatch(params);
578
+ break;
579
+ case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP:
580
+ await this.doPatchFromApp(params);
581
+ break;
582
+ case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
583
+ await this.doPatchFromPpk(params);
584
+ break;
585
+ case DownloadTaskParams.TASK_TYPE_CLEANUP:
586
+ await this.doCleanUp(params);
587
+ break;
588
+ case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
589
+ await this.downloadFile(params);
590
+ break;
591
+ default:
592
+ throw Error(`Unknown task type: ${params.type}`);
593
+ }
594
+ } catch (error) {
595
+ console.error('Task execution failed:', error.message);
596
+ if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
597
+ try {
598
+ if (params.type === DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD) {
599
+ await fileIo.unlink(params.targetFile);
600
+ } else {
601
+ await this.removeDirectory(params.unzipDirectory);
602
+ }
603
+ } catch (cleanupError) {
604
+ console.error('Cleanup after error failed:', cleanupError.message);
605
+ }
606
+ }
607
+ throw error;
608
+ }
609
+ }
610
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 下载任务参数类
3
+ */
4
+ export class DownloadTaskParams {
5
+ // 任务类型常量
6
+ static readonly TASK_TYPE_CLEANUP: number = 0; // 保留hash和originHash
7
+ static readonly TASK_TYPE_PATCH_FULL: number = 1; // 全量补丁
8
+ static readonly TASK_TYPE_PATCH_FROM_APP: number = 2; // 从APP补丁
9
+ static readonly TASK_TYPE_PATCH_FROM_PPK: number = 3; // 从PPK补丁
10
+ static readonly TASK_TYPE_PLAIN_DOWNLOAD: number = 4; // 普通下载
11
+
12
+ type: number; // 任务类型
13
+ url: string; // 下载URL
14
+ hash: string; // 文件哈希值
15
+ originHash: string; // 原始文件哈希值
16
+ targetFile: string; // 目标文件路径
17
+ unzipDirectory: string; // 解压目录路径
18
+ originDirectory: string; // 原始文件目录路径
19
+ }
@@ -0,0 +1,39 @@
1
+ type EventCallback = (data: any) => void;
2
+
3
+ export class EventHub {
4
+ private static instance: EventHub;
5
+ private listeners: Map<string, Set<EventCallback>>;
6
+ private rnInstance: any;
7
+
8
+ private constructor() {
9
+ this.listeners = new Map();
10
+ }
11
+
12
+ public static getInstance(): EventHub {
13
+ if (!EventHub.instance) {
14
+ EventHub.instance = new EventHub();
15
+ }
16
+ return EventHub.instance;
17
+ }
18
+
19
+ public on(event: string, callback: EventCallback): void {
20
+ if (!this.listeners.has(event)) {
21
+ this.listeners.set(event, new Set());
22
+ }
23
+ this.listeners.get(event)?.add(callback);
24
+ }
25
+
26
+ public off(event: string, callback: EventCallback): void {
27
+ this.listeners.get(event)?.delete(callback);
28
+ }
29
+
30
+ public emit(event: string, data: any): void {
31
+ if (this.rnInstance) {
32
+ this.rnInstance.emitDeviceEvent(event, data);
33
+ }
34
+ }
35
+
36
+ setRNInstance(instance: any) {
37
+ this.rnInstance = instance;
38
+ }
39
+ }