@jolibox/implement 1.1.10 → 1.1.11-beta.10

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 (40) hide show
  1. package/.rush/temp/package-deps_build.json +22 -18
  2. package/.rush/temp/shrinkwrap-deps.json +1 -1
  3. package/dist/common/ads/anti-cheating.d.ts +24 -51
  4. package/dist/common/ads/anti-cheating.test.d.ts +1 -0
  5. package/dist/common/ads/index.d.ts +4 -9
  6. package/dist/common/context/index.d.ts +4 -0
  7. package/dist/common/context/url-parse.d.ts +8 -1
  8. package/dist/index.js +3 -3
  9. package/dist/index.native.js +110 -4
  10. package/dist/native/api/index.d.ts +1 -0
  11. package/dist/native/api/navigate.d.ts +1 -0
  12. package/dist/native/bootstrap/bridge.d.ts +1 -1
  13. package/dist/native/js-bridge/const.d.ts +1 -0
  14. package/dist/native/js-bridge/publish.d.ts +1 -0
  15. package/dist/native/js-bridge/subscribe.d.ts +2 -0
  16. package/dist/native/js-bridge/types.d.ts +4 -0
  17. package/dist/native/js-core/jolibox-js-core.d.ts +8 -3
  18. package/dist/native/ui/retention.d.ts +1 -0
  19. package/implement.build.log +2 -2
  20. package/package.json +4 -3
  21. package/src/common/ads/anti-cheating.test.ts +79 -0
  22. package/src/common/ads/anti-cheating.ts +228 -139
  23. package/src/common/ads/index.ts +33 -20
  24. package/src/common/context/index.ts +19 -3
  25. package/src/common/context/url-parse.ts +24 -1
  26. package/src/native/api/ads.ts +7 -0
  27. package/src/native/api/index.ts +1 -0
  28. package/src/native/api/lifecycle.ts +16 -4
  29. package/src/native/api/navigate.ts +61 -0
  30. package/src/native/bootstrap/bridge.ts +10 -1
  31. package/src/native/bootstrap/index.ts +32 -2
  32. package/src/native/js-bridge/const.ts +2 -0
  33. package/src/native/js-bridge/js-bridge.ts +7 -2
  34. package/src/native/js-bridge/publish.ts +44 -0
  35. package/src/native/js-bridge/subscribe.ts +25 -1
  36. package/src/native/js-bridge/types.ts +10 -0
  37. package/src/native/js-core/jolibox-js-core.ts +30 -26
  38. package/src/native/types/global.d.ts +1 -0
  39. package/src/native/types/native-method-map.d.ts +29 -0
  40. package/src/native/ui/retention.ts +152 -0
@@ -1,9 +1,10 @@
1
1
  import { AdsActionDetection } from './ads-action-detection';
2
2
  import { AdsAntiCheating } from './anti-cheating';
3
- import { Track } from '../report';
4
3
  import { context } from '../context';
5
4
  import { ChannelPolicy } from './channel-policy';
6
5
  import { EventEmitter } from '@jolibox/common';
6
+ import type { Track } from '../report';
7
+ import type { IHttpClient } from '../http';
7
8
 
8
9
  declare global {
9
10
  interface Window {
@@ -254,10 +255,6 @@ interface IJoliboxAdsResponse {
254
255
  };
255
256
  }
256
257
 
257
- interface HttpClient {
258
- get<T>(url: string, options?: { query?: Record<string, string> }): Promise<T>;
259
- }
260
-
261
258
  export const adEventEmitter = new EventEmitter<{
262
259
  isAdShowing: [boolean];
263
260
  }>();
@@ -278,8 +275,8 @@ export class JoliboxAdsImpl {
278
275
  /**
279
276
  * Internal constructor, should not be called directly
280
277
  */
281
- constructor(readonly track: Track, readonly httpClient: HttpClient, readonly checkNetwork: () => boolean) {
282
- this.antiCheating = new AdsAntiCheating(checkNetwork);
278
+ constructor(readonly track: Track, readonly httpClient: IHttpClient, readonly checkNetwork: () => boolean) {
279
+ this.antiCheating = new AdsAntiCheating(track, httpClient, checkNetwork);
283
280
  this.adsActionDetection = new AdsActionDetection(this.track);
284
281
  this.channelPolicy = new ChannelPolicy(httpClient);
285
282
  }
@@ -400,7 +397,10 @@ export class JoliboxAdsImpl {
400
397
 
401
398
  const beforeAd = () => {
402
399
  adEventEmitter.emit('isAdShowing', true);
403
- this.track('CallBeforeAd', {});
400
+ this.track('CallBeforeAd', {
401
+ type: params.type,
402
+ name: params.name ?? ''
403
+ });
404
404
  if (originBeforeAd) {
405
405
  originBeforeAd();
406
406
  }
@@ -408,7 +408,10 @@ export class JoliboxAdsImpl {
408
408
 
409
409
  const afterAd = () => {
410
410
  adEventEmitter.emit('isAdShowing', false);
411
- this.track('CallAfterAd', {});
411
+ this.track('CallAfterAd', {
412
+ type: params.type,
413
+ name: params.name ?? ''
414
+ });
412
415
  if (originAfterAd) {
413
416
  originAfterAd();
414
417
  }
@@ -478,36 +481,46 @@ export class JoliboxAdsImpl {
478
481
  }
479
482
  }
480
483
 
481
- const adsDisplayPermission = this.antiCheating.checkAdsDisplayPermission(
484
+ const { reason, info } = this.antiCheating.checkAdsDisplayPermission(
482
485
  params.type === 'reward' ? 'reward' : 'interstitial'
483
486
  );
484
487
 
485
- switch (adsDisplayPermission) {
488
+ switch (reason) {
486
489
  case 'NETWORK_NOT_OK':
487
490
  case 'BANNED_FOR_SESSION':
488
- console.warn('Ads not allowed', adsDisplayPermission);
491
+ // console.warn('Ads not allowed', reason);
489
492
  params.adBreakDone?.({
490
493
  breakType: params.type,
491
- breakName: adsDisplayPermission,
494
+ breakName: reason,
492
495
  breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
493
496
  breakStatus: 'noAdPreloaded'
494
497
  });
495
- this.track('PreventAdsCheating', {
496
- reason: adsDisplayPermission
497
- });
498
+ this.antiCheating.report(reason);
498
499
  return;
499
500
  case 'BLOCK_INITIAL':
500
501
  case 'BANNED_FOR_TIME':
501
502
  case 'WAITING_BANNED_RELEASE':
502
- console.warn('Ads not allowed', adsDisplayPermission);
503
+ // console.warn('Ads not allowed', adsDisplayPermission);
503
504
  params.adBreakDone?.({
504
505
  breakType: params.type,
505
506
  breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
506
- breakStatus: 'viewed' // TODO: need to confirm
507
+ breakStatus: 'frequencyCapped'
507
508
  });
508
- this.track('PreventAdsCheating', {
509
- reason: adsDisplayPermission
509
+ this.antiCheating.report(reason);
510
+ return;
511
+ case 'HOLDING_HIGH_FREQ':
512
+ // follow google's style to forbid high freq request
513
+ params.adBreakDone?.({
514
+ breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
515
+ breakName: '',
516
+ breakStatus: (info?.count ?? 0) > 6 ? 'other' : 'frequencyCapped',
517
+ breakType: params.type
510
518
  });
519
+ // every 3 hit, report to anti-cheating system
520
+ // if a user click 1 time every 1 second, it will cause 20 reports for 1 minute
521
+ if (info?.count && info.count !== 0 && info.count % 3 === 0) {
522
+ this.antiCheating.report(reason, info?.count);
523
+ }
511
524
  return;
512
525
  default:
513
526
  // allowed
@@ -1,7 +1,7 @@
1
1
  import { mergeArray, mergeWith } from '@jolibox/common';
2
2
  import { DeviceInfo, HostInfo, HostUserInfo, SdkInfo } from './types';
3
3
  import { Env } from '@jolibox/types';
4
- import { parseUrlQuery } from './url-parse';
4
+ import { parseUrlQuery, encodeJoliSourceQuery, QueryParams } from './url-parse';
5
5
  import { getAppVersion } from '../http/xua';
6
6
  import { getDeviceId } from '@jolibox/common';
7
7
 
@@ -36,12 +36,13 @@ const env = Object.assign({}, nativeEnv?.() ?? defaultEnv);
36
36
  type MPType = 'game' | 'miniApp';
37
37
 
38
38
  const wrapContext = () => {
39
- const { payloadJson, headerJson } = env.schema.length ? parseUrlQuery(env.schema) : {};
39
+ const { payloadJson, headerJson, signature } = env.schema.length ? parseUrlQuery(env.schema) : {};
40
40
  const defaultSessionId = `${env.deviceInfo.did}-${new Date().getTime()}`;
41
41
  const url = new URL(env.schema.length ? env.schema : window.location.href);
42
42
  const urlParams = url.searchParams;
43
43
  const defaultGameID = urlParams.get('mpId') ?? urlParams.get('appId') ?? urlParams.get('gameId') ?? '';
44
- const sessionId = payloadJson?.sessionId ?? urlParams.get('sessionId') ?? defaultSessionId;
44
+ const sessionId =
45
+ env.clientSessionId ?? payloadJson?.sessionId ?? urlParams.get('sessionId') ?? defaultSessionId;
45
46
  const testAdsMode = !!(payloadJson?.testAdsMode ?? urlParams.get('testAdsMode') === 'true');
46
47
  const joliboxEnv = payloadJson?.joliboxEnv ?? urlParams.get('joliboxEnv') ?? 'production';
47
48
  const testMode = joliboxEnv === 'staging';
@@ -93,8 +94,23 @@ const wrapContext = () => {
93
94
  get webviewId(): number {
94
95
  return env.webviewId ?? -1;
95
96
  },
97
+ get shouldInterupt(): boolean | undefined {
98
+ return payloadJson?.__shouldInterupt;
99
+ },
100
+ get from(): number | undefined {
101
+ return payloadJson?.__from;
102
+ },
96
103
  onEnvConfigChanged: (newConfig: Partial<Env>) => {
97
104
  mergeWith(env, newConfig, mergeArray);
105
+ },
106
+ encodeJoliSourceQuery: (newPayloadJson: QueryParams['payloadJson']) => {
107
+ if (headerJson && signature) {
108
+ return encodeJoliSourceQuery(
109
+ { ...payloadJson, ...newPayloadJson },
110
+ urlParams.get('joliSource') ?? ''
111
+ );
112
+ }
113
+ return urlParams.get('joliSource') ?? '';
98
114
  }
99
115
  };
100
116
  };
@@ -14,6 +14,12 @@ interface PayloadJson {
14
14
  joliboxEnv?: 'staging' | 'production';
15
15
  sessionId?: string;
16
16
  __mpType?: 'game' | 'miniApp';
17
+ __orientation?: 'HORIZONTAL' | 'VERTICAL';
18
+ __transparent?: boolean;
19
+ __entryPath?: string;
20
+ __showStatusBar?: boolean;
21
+ __shouldInterupt?: boolean;
22
+ __from?: number; // 从哪个小程序打开的
17
23
  }
18
24
 
19
25
  interface Signature {
@@ -21,7 +27,7 @@ interface Signature {
21
27
  payloadSig?: string;
22
28
  }
23
29
 
24
- interface QueryParams {
30
+ export interface QueryParams {
25
31
  headerJson: HeaderJson;
26
32
  payloadJson: PayloadJson;
27
33
  signature: Signature;
@@ -38,6 +44,12 @@ const base64UrlDecode = <T>(input: string): T => {
38
44
  }
39
45
  };
40
46
 
47
+ const base64UrlEncode = <T>(input: T): string => {
48
+ const jsonStr = JSON.stringify(input);
49
+ const base64 = btoa(jsonStr);
50
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
51
+ };
52
+
41
53
  export const parseUrlQuery = (url: string): QueryParams => {
42
54
  try {
43
55
  const urlObj = new URL(url);
@@ -63,3 +75,14 @@ export const parseUrlQuery = (url: string): QueryParams => {
63
75
  };
64
76
  }
65
77
  };
78
+
79
+ export const encodeJoliSourceQuery = (payloadJson: PayloadJson, originJoliSource: string): string => {
80
+ const joli_source = originJoliSource.split('.');
81
+ if (joli_source && joli_source.length === 3) {
82
+ const headerJsonStr = joli_source[0];
83
+ const payloadJsonStr = base64UrlEncode(payloadJson);
84
+ const signatureJsonStr = joli_source[2];
85
+ return `${headerJsonStr}.${payloadJsonStr}.${signatureJsonStr}`;
86
+ }
87
+ return originJoliSource;
88
+ };
@@ -22,6 +22,13 @@ const ads = new JoliboxAdsImpl(
22
22
  responseType: 'json',
23
23
  appendHostCookie: true,
24
24
  ...options
25
+ }).then((res) => res.response.data as T),
26
+ post: <T>(url: string, data: any, options?: any) =>
27
+ fetch<T>(url, {
28
+ method: 'POST',
29
+ responseType: 'json',
30
+ appendHostCookie: true,
31
+ ...options
25
32
  }).then((res) => res.response.data as T)
26
33
  },
27
34
  checkNetworkStatus
@@ -6,3 +6,4 @@ import './keyboard';
6
6
  import './task';
7
7
  import './login';
8
8
  import './ads';
9
+ import './navigate';
@@ -1,8 +1,9 @@
1
- import { BaseError, createCommands, wrapUserFunction, hostEmitter } from '@jolibox/common';
1
+ import { BaseError, createCommands, wrapUserFunction, hostEmitter, isBoolean } from '@jolibox/common';
2
2
  import { createAPI, createSyncAPI, registerCanIUse, t } from './base';
3
3
  import { reportError } from '@/common/report/errors/report';
4
- import { applyNative, onNative } from '../bootstrap/bridge';
4
+ import { applyNative, onNative, publish } from '../bootstrap/bridge';
5
5
  import { nativeTaskEmitter } from '../report';
6
+ import { context } from '@/common/context';
6
7
 
7
8
  const EXIT_GAME = 'exitGame';
8
9
  const ON_READY = 'onReady';
@@ -13,11 +14,22 @@ const commands = createCommands();
13
14
 
14
15
  const safeCallbackWrapper = wrapUserFunction(reportError as (err: Error | BaseError) => void);
15
16
  const exitGame = createAPI(EXIT_GAME, {
16
- paramsSchema: t.tuple(t.function()),
17
- implement: async (onBeforeExit: () => void) => {
17
+ paramsSchema: t.tuple(t.function(), t.boolean().optional().default(false)),
18
+ implement: async (onBeforeExit: () => void, shouldStay = false) => {
18
19
  const safeCallback = safeCallbackWrapper(onBeforeExit);
19
20
  // 集中上报
20
21
  safeCallback.call(this);
22
+ // 透传context.shouldInterupt 且值为false时,为内部小程序。广播onRetentionResult
23
+ if (isBoolean(context.shouldInterupt) && !context.shouldInterupt && context.from) {
24
+ publish(
25
+ 'onRetentionResult',
26
+ {
27
+ shouldStay
28
+ },
29
+ context.from,
30
+ true
31
+ );
32
+ }
21
33
  await applyNative('exitAppAsync');
22
34
  }
23
35
  });
@@ -0,0 +1,61 @@
1
+ import { createCommands, UserCustomError } from '@jolibox/common';
2
+ import { invokeNative } from '../bootstrap/bridge';
3
+ import { createSyncAPI, t, registerCanIUse } from './base';
4
+ import { context } from '@/common/context';
5
+
6
+ const commands = createCommands();
7
+
8
+ const openSchemaSync = createSyncAPI('openSchemaSync', {
9
+ paramsSchema: t.tuple(t.string()),
10
+ implement: (schema) => {
11
+ // Add compatibility logic for incomplete URLs
12
+ let finalSchema = schema;
13
+ if (!schema.match(/^https?:\/\//)) {
14
+ finalSchema = `https://${context.mpId}.app.jolibox.com${schema.startsWith('/') ? '' : '/'}${schema}`;
15
+ }
16
+
17
+ const res = invokeNative('openSchemaSync', {
18
+ schema: finalSchema
19
+ });
20
+ if (res.errNo !== 0) {
21
+ throw new UserCustomError(res.errMsg, res.errNo);
22
+ }
23
+ }
24
+ });
25
+
26
+ const openPageSync = createSyncAPI('openPageSync', {
27
+ paramsSchema: t.tuple(t.string()),
28
+ implement: (page) => {
29
+ const { errNo, errMsg, data } = invokeNative('openPageSync', { url: page });
30
+ if (errNo !== 0 || !data) {
31
+ throw new UserCustomError(errMsg, errNo);
32
+ }
33
+ return { webviewId: data.webviewId };
34
+ }
35
+ });
36
+
37
+ const closePageSync = createSyncAPI('closePageSync', {
38
+ paramsSchema: t.tuple(t.number()),
39
+ implement: (webviewId) => {
40
+ const { errNo, errMsg } = invokeNative('closePageSync', { webviewId });
41
+ if (errNo !== 0) {
42
+ throw new UserCustomError(errMsg, errNo);
43
+ }
44
+ }
45
+ });
46
+
47
+ commands.registerCommand('RouterSDK.openSchema', openSchemaSync);
48
+ commands.registerCommand('RouterSDK.openPage', openPageSync);
49
+ commands.registerCommand('RouterSDK.closePage', closePageSync);
50
+
51
+ registerCanIUse('router.openSchema', {
52
+ version: '1.1.10'
53
+ });
54
+
55
+ registerCanIUse('router.openPage', {
56
+ version: '1.1.10'
57
+ });
58
+
59
+ registerCanIUse('router.closePage', {
60
+ version: '1.1.10'
61
+ });
@@ -38,7 +38,16 @@ const bridge = createBridge(core);
38
38
 
39
39
  const { invokeHandler } = bridge;
40
40
 
41
- export const { applyNative, invokeNative, onNative, offNative, subscribeHandler } = bridge;
41
+ export const {
42
+ applyNative,
43
+ invokeNative,
44
+ onNative,
45
+ offNative,
46
+ subscribeHandler,
47
+ publish,
48
+ subscribe,
49
+ unsubscribe
50
+ } = bridge;
42
51
 
43
52
  export const onNativeWithError: On = (event, handler) => {
44
53
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1,26 +1,56 @@
1
- import { invokeNative, onNative, RuntimeLoader } from './bridge';
1
+ import { invokeNative, onNative, RuntimeLoader, subscribe } from './bridge';
2
2
  import { joliboxJSCore } from '../js-core';
3
3
  import { context } from '@/common/context';
4
4
  import { hostEmitter, isBoolean } from '@jolibox/common';
5
5
  import { taskTracker, track } from '../report';
6
6
  import { initializeNativeEnv } from './init-env';
7
7
  import { adEventEmitter } from '@/common/ads';
8
+ import { openRetentionSchema } from '../ui/retention';
8
9
 
9
10
  let cleanStyles: () => void;
10
11
  RuntimeLoader.onReady(() => {
11
12
  // TODO: merge some env config
12
13
  });
13
14
 
14
- RuntimeLoader.doExit(() => {
15
+ const doActualExit = () => {
15
16
  //埋点上报
16
17
  track('onBeforeExit', {
17
18
  timestamp: Date.now()
18
19
  });
19
20
  cleanStyles?.();
20
21
  taskTracker.close(Date.now() - start_timestamp);
22
+ };
23
+
24
+ /**
25
+ * 退出逻辑
26
+ * 1. 如果正在展示广告,则禁止退出
27
+ * 2. 如果指定退出挽留逻辑,则按照指定逻辑运行
28
+ * 3. 如果退出挽留逻辑返回 true,则按照指定逻辑运行
29
+ * 4. 否则,按照默认逻辑运行
30
+ */
31
+ RuntimeLoader.doExit(async () => {
21
32
  if (isAdShowing) {
22
33
  return true; // Forbid exit on watching ads
23
34
  }
35
+
36
+ // 指定退出挽留逻辑,则按照指定逻辑运行
37
+ if (isBoolean(context.shouldInterupt)) {
38
+ // 不需要打断退出,上报埋点
39
+ if (!context.shouldInterupt) {
40
+ doActualExit();
41
+ }
42
+ return context.shouldInterupt;
43
+ }
44
+
45
+ const stay = await openRetentionSchema();
46
+ if (stay) {
47
+ // 挽留成功,打断退出
48
+ return true;
49
+ }
50
+
51
+ // 退出,对应上报
52
+ //埋点上报
53
+ doActualExit();
24
54
  return false;
25
55
  });
26
56
 
@@ -9,3 +9,5 @@ export const BUFFER_METHODS: string[] = [];
9
9
  export const BACKGROUND_FORBIDDEN_METHODS: string[] = [];
10
10
 
11
11
  export const SYNC_METHODS: string[] = ['env', 'createRequestTask', 'login'];
12
+
13
+ export const BATCH_EVENT = '__batch_event__';
@@ -1,15 +1,17 @@
1
1
  import { createInvoke } from './invoke';
2
2
  import { createSubscribe } from './subscribe';
3
3
  import { JSBridge } from './types';
4
+ import { createPublish } from './publish';
4
5
 
5
6
  /**
6
7
  * build js-bridge
7
8
  * @param jsCore jsCore function inject by native
8
9
  */
9
10
  export function createBridge(jsCore: jsb.JSCore): JSBridge {
10
- const { subscribeHandler, onNative, offNative } = createSubscribe(jsCore);
11
+ const { subscribeHandler, onNative, offNative, subscribe, unsubscribe } = createSubscribe(jsCore);
11
12
 
12
13
  const { invokeNative, invokeHandler, applyNative } = createInvoke(jsCore, onNative);
14
+ const publish = createPublish(jsCore);
13
15
 
14
16
  return {
15
17
  // 宿主调用
@@ -18,6 +20,9 @@ export function createBridge(jsCore: jsb.JSCore): JSBridge {
18
20
  applyNative,
19
21
  invokeNative,
20
22
  onNative,
21
- offNative
23
+ offNative,
24
+ publish,
25
+ subscribe,
26
+ unsubscribe
22
27
  };
23
28
  }
@@ -0,0 +1,44 @@
1
+ import { CUSTOM_EVENT_PREFIX, BATCH_EVENT } from './const';
2
+
3
+ type BatchEvent = [event: string, data: unknown];
4
+ type BatchEvents = BatchEvent[];
5
+
6
+ export function createPublish(jsCore: jsb.JSCore) {
7
+ const eventsMap = new Map<number | undefined, BatchEvents>();
8
+ let batchTask: Promise<void> | undefined;
9
+ const publish = (event: string, data: Record<string, unknown>, webviewId?: number, force?: boolean) => {
10
+ if (force) {
11
+ const ids = webviewId ? [webviewId] : '*';
12
+ jsCore.publish(`${CUSTOM_EVENT_PREFIX}${event}`, data, ids);
13
+ return;
14
+ }
15
+
16
+ if (!batchTask) {
17
+ batchTask = Promise.resolve().then(() => {
18
+ eventsMap.forEach((data, webviewId) => {
19
+ try {
20
+ const ids = webviewId ? [webviewId] : '*';
21
+ jsCore.publish(BATCH_EVENT, data, ids);
22
+ } catch {
23
+ // 避免一组 webview publish 的报错影响其他 webview publish
24
+ }
25
+ });
26
+
27
+ // reset
28
+ batchTask = undefined;
29
+ eventsMap.clear();
30
+ });
31
+ }
32
+
33
+ let events = eventsMap.get(webviewId);
34
+
35
+ if (!events) {
36
+ events = [];
37
+ eventsMap.set(webviewId, events);
38
+ }
39
+
40
+ events.push([`${CUSTOM_EVENT_PREFIX}${event}`, data]);
41
+ };
42
+
43
+ return publish;
44
+ }
@@ -1,17 +1,21 @@
1
- import { EventEmitter } from '@jolibox/common';
1
+ import { EventEmitter, logger } from '@jolibox/common';
2
2
  import { DataObj, unpack } from './utils';
3
3
  import { Off, On } from './types';
4
4
  import { AnyFunction } from '@jolibox/types';
5
5
  import { MetricsMonitor } from './report';
6
+ import { BATCH_EVENT, CUSTOM_EVENT_PREFIX } from './const';
6
7
 
7
8
  export interface Subscribes {
8
9
  onNative: On;
9
10
  offNative: Off;
10
11
  subscribeHandler: AnyFunction;
12
+ subscribe: On;
13
+ unsubscribe: Off;
11
14
  }
12
15
 
13
16
  export function createSubscribe(jsCore: jsb.JSCore): Subscribes {
14
17
  const nativeEmitter = new EventEmitter();
18
+ const customEmitter = new EventEmitter();
15
19
  const publishMonitor = new MetricsMonitor({
16
20
  eventName: 'jolibox_publish',
17
21
  tagNameOrder: ['type', 'event'],
@@ -28,6 +32,8 @@ export function createSubscribe(jsCore: jsb.JSCore): Subscribes {
28
32
  return {
29
33
  onNative: nativeEmitter.on.bind(nativeEmitter),
30
34
  offNative: nativeEmitter.off.bind(nativeEmitter),
35
+ subscribe: customEmitter.on.bind(customEmitter),
36
+ unsubscribe: customEmitter.off.bind(customEmitter),
31
37
  subscribeHandler(event, data, webviewId) {
32
38
  // ios: jsc 端基于系统方法,前端接受到的 data 总是 string 类型, webview 基于 evaluateJavascript,前端接受到的 data 总是 object 类型
33
39
  // android: 传入为 string 则接收到 string,传入为可序列化成功的 object,则接收到 object
@@ -44,6 +50,24 @@ export function createSubscribe(jsCore: jsb.JSCore): Subscribes {
44
50
  } else {
45
51
  originalParams = unpackedData;
46
52
  }
53
+
54
+ if (event === BATCH_EVENT) {
55
+ const list = originalParams as [event: string, data: unknown][];
56
+ list.forEach((item) => {
57
+ const [_event, _data] = item;
58
+ try {
59
+ customEmitter.emit(_event.slice(CUSTOM_EVENT_PREFIX.length), _data, webviewId);
60
+ } catch {
61
+ // 忽略
62
+ }
63
+ });
64
+ return;
65
+ }
66
+
67
+ if (event.startsWith(CUSTOM_EVENT_PREFIX)) {
68
+ customEmitter.emit(event.slice(CUSTOM_EVENT_PREFIX.length), originalParams, webviewId);
69
+ return;
70
+ }
47
71
  nativeEmitter.emit(event, originalParams, webviewId);
48
72
  }
49
73
  };
@@ -7,6 +7,13 @@ export type Off = <T extends string>(event: T, handler: Listener) => void;
7
7
 
8
8
  export type InvokeHandler = (callbackId: string | number, data: string | Record<string, unknown>) => void;
9
9
 
10
+ export type Publish = (
11
+ event: string,
12
+ data: Record<string, unknown>,
13
+ webviewId?: number,
14
+ force?: boolean
15
+ ) => void;
16
+
10
17
  export type SubscribeHandler = (
11
18
  event: string,
12
19
  data: string | Record<string, unknown>,
@@ -23,4 +30,7 @@ export interface JSBridge {
23
30
  applyNative: jsb.service.ApplyNative;
24
31
  onNative: jsb.service.OnNative;
25
32
  offNative: jsb.service.OffNative;
33
+ publish: Publish;
34
+ subscribe: On;
35
+ unsubscribe: Off;
26
36
  }