Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/blue-lies-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@lynx-js/web-worker-runtime": patch
"@lynx-js/web-constants": patch
"@lynx-js/web-core": patch
---

feat: allow multi lynx-view to share bts worker

Now we allow users to enable so-called "shared-context" feature on the Web Platform.

Similar to the same feature for Lynx iOS/Android, this feature let multi lynx cards to share one js context.

The `lynx.getSharedData` and `lynx.setSharedData` are also supported in this commit.

To enable this feature, set property `lynxGroupId` or attribute `lynx-group-id` before a lynx-view starts rendering. Those card with same context id will share one web worker for the bts scripts.
3 changes: 3 additions & 0 deletions packages/web-platform/web-constants/src/types/NativeApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,7 @@ export interface NativeApp {
createJSObjectDestructionObserver(
callback: (...args: unknown[]) => unknown,
): {};

setSharedData<T>(dataKey: string, dataVal: T): void;
getSharedData<T = unknown>(dataKey: string): T | undefined;
}
20 changes: 20 additions & 0 deletions packages/web-platform/web-core/src/apis/LynxView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type INapiModulesCall = (
* @property {NapiModulesMap} napiModulesMap [optional] the napiModule which is called in lynx-core. key is module-name, value is esm url.
* @property {INapiModulesCall} onNapiModulesCall [optional] the NapiModule value handler.
* @property {"false" | "true" | null} injectHeadLinks [optional] @default true set it to "false" to disable injecting the <link href="" ref="stylesheet"> styles into shadowroot
* @property {number} lynxGroupId [optional] (attribute: "lynx-group-id") the background shared context id, which is used to share webworker between different lynx cards
*
* @event error lynx card fired an error
*
Expand Down Expand Up @@ -200,6 +201,23 @@ export class LynxView extends HTMLElement {
};
}

/**
* @param
* @property
*/
get lynxGroupId(): number | undefined {
return this.getAttribute('lynx-group-id')
? Number(this.getAttribute('lynx-group-id')!)
: undefined;
}
set lynxGroupId(val: number | undefined) {
if (val) {
this.setAttribute('lynx-group-id', val.toString());
} else {
this.removeAttribute('lynx-group-id');
}
}

/**
* @public
* @method
Expand Down Expand Up @@ -304,6 +322,7 @@ export class LynxView extends HTMLElement {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
}
const lynxGroupId = this.lynxGroupId;
const lynxView = createLynxView({
tagMap,
shadowRoot: this.shadowRoot!,
Expand All @@ -312,6 +331,7 @@ export class LynxView extends HTMLElement {
initData: this.#initData,
nativeModulesMap: this.#nativeModulesMap,
napiModulesMap: this.#napiModulesMap,
lynxGroupId,
callbacks: {
nativeModulesCall: (
...args: [name: string, data: any, moduleName: string]
Expand Down
5 changes: 4 additions & 1 deletion packages/web-platform/web-core/src/apis/createLynxView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export interface LynxViewConfigs {
initData: Cloneable;
globalProps: Cloneable;
shadowRoot: ShadowRoot;
callbacks: Parameters<typeof startUIThread>[3];
callbacks: Parameters<typeof startUIThread>[4];
nativeModulesMap: NativeModulesMap;
napiModulesMap: NapiModulesMap;
tagMap: Record<string, string>;
lynxGroupId: number | undefined;
}

export interface LynxView {
Expand All @@ -43,6 +44,7 @@ export function createLynxView(configs: LynxViewConfigs): LynxView {
nativeModulesMap,
napiModulesMap,
tagMap,
lynxGroupId,
} = configs;
return startUIThread(
templateUrl,
Expand All @@ -55,6 +57,7 @@ export function createLynxView(configs: LynxViewConfigs): LynxView {
browserConfig: {},
},
shadowRoot,
lynxGroupId,
callbacks,
);
}
41 changes: 34 additions & 7 deletions packages/web-platform/web-core/src/uiThread/bootWorkers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,48 @@ interface LynxViewRpc {
terminateWorkers: () => void;
}

const backgroundWorkerContextCount: number[] = [];
const contextIdToBackgroundWorker: (Worker | undefined)[] = [];

let preHeatedMainWorker = createMainWorker();

export function bootWorkers(): LynxViewRpc {
export function bootWorkers(
lynxGroupId: number | undefined,
): LynxViewRpc {
const curMainWorker = preHeatedMainWorker;
preHeatedMainWorker = createMainWorker();
const curBackgroundWorker = createBackgroundWorker(
lynxGroupId,
curMainWorker.channelMainThreadWithBackground,
);
if (lynxGroupId !== undefined) {
if (backgroundWorkerContextCount[lynxGroupId]) {
backgroundWorkerContextCount[lynxGroupId]++;
} else {
backgroundWorkerContextCount[lynxGroupId] = 1;
}
}

preHeatedMainWorker = createMainWorker();
return {
mainThreadRpc: curMainWorker.mainThreadRpc,
backgroundRpc: curBackgroundWorker.backgroundRpc,
terminateWorkers: () => {
curMainWorker.mainThreadWorker.terminate();
curBackgroundWorker.backgroundThreadWorker.terminate();
if (lynxGroupId === undefined) {
curBackgroundWorker.backgroundThreadWorker.terminate();
} else if (backgroundWorkerContextCount[lynxGroupId] === 1) {
curBackgroundWorker.backgroundThreadWorker.terminate();
backgroundWorkerContextCount[lynxGroupId] = 0;
contextIdToBackgroundWorker[lynxGroupId] = undefined;
}
},
};
}

function createMainWorker() {
const channelToMainThread = new MessageChannel();
const channelMainThreadWithBackground = new MessageChannel();
const mainThreadWorker = createWebWorker();
const mainThreadWorker = createWebWorker('lynx-main');
const mainThreadMessage: WorkerStartMessage = {
mode: 'main',
toUIThread: channelToMainThread.port2,
Expand All @@ -54,10 +73,18 @@ function createMainWorker() {
}

function createBackgroundWorker(
lynxGroupId: number | undefined,
channelMainThreadWithBackground: MessageChannel,
) {
const channelToBackground = new MessageChannel();
const backgroundThreadWorker = createWebWorker();
let backgroundThreadWorker: Worker;
if (lynxGroupId) {
backgroundThreadWorker = contextIdToBackgroundWorker[lynxGroupId]
?? createWebWorker('lynx-bg');
contextIdToBackgroundWorker[lynxGroupId] = backgroundThreadWorker;
} else {
backgroundThreadWorker = createWebWorker('lynx-bg');
}
const backgroundThreadMessage: WorkerStartMessage = {
mode: 'background',
toUIThread: channelToBackground.port2,
Expand All @@ -72,12 +99,12 @@ function createBackgroundWorker(
return { backgroundRpc, backgroundThreadWorker };
}

function createWebWorker(): Worker {
function createWebWorker(name: string): Worker {
return new Worker(
new URL('@lynx-js/web-worker-runtime', import.meta.url),
{
type: 'module',
name: `lynx-web`,
name,
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function startUIThread(
templateUrl: string,
configs: Omit<MainThreadStartConfigs, 'template'>,
shadowRoot: ShadowRoot,
lynxGroupId: number | undefined,
callbacks: {
nativeModulesCall: NativeModulesCall;
napiModulesCall: NapiModulesCall;
Expand All @@ -41,7 +42,7 @@ export function startUIThread(
mainThreadRpc,
backgroundRpc,
terminateWorkers,
} = bootWorkers();
} = bootWorkers(lynxGroupId);
const sendGlobalEvent = backgroundRpc.createCall(sendGlobalEventEndpoint);
const mainThreadStart = mainThreadRpc.createCall(mainThreadStartEndpoint);
const markTiming = backgroundRpc.createCall(markTimingEndpoint);
Expand Down
17 changes: 15 additions & 2 deletions packages/web-platform/web-tests/shell-project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ const nativeModulesMap = {

const searchParams = new URLSearchParams(document.location.search);
const casename = searchParams.get('casename');
const casename2 = searchParams.get('casename2');
const hasdir = searchParams.get('hasdir') === 'true';

if (casename) {
const dir = `/dist/${casename}${hasdir ? `/${casename}` : ''}`;
const lepusjs = `${dir}/index.web.json`;
const dir2 = `/dist/${casename2}${hasdir ? `/${casename2}` : ''}`;
lynxViewTests(lynxView => {
lynxView.setAttribute('url', lepusjs);
lynxView.setAttribute('url', `${dir}/index.web.json`);
lynxView.nativeModulesMap = nativeModulesMap;
lynxView.id = 'lynxview1';
if (casename2) {
lynxView.setAttribute('lynx-group-id', '2');
}
lynxView.onNativeModulesCall = (name, data, moduleName) => {
if (name === 'getColor' && moduleName === 'CustomModule') {
return data.color;
Expand All @@ -41,6 +46,14 @@ if (casename) {
}
};
});
if (casename2) {
lynxViewTests(lynxView2 => {
lynxView2.id = 'lynxview2';
lynxView2.setAttribute('url', `${dir2}/index.web.json`);
lynxView2.nativeModulesMap = nativeModulesMap;
lynxView2.setAttribute('lynx-group-id', '2');
});
}
} else {
console.warn('cannot find casename');
}
61 changes: 49 additions & 12 deletions packages/web-platform/web-tests/tests/react.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

const expectHasText = async (page: Page, text: string) => {
const hasText = (await page.getByText(text).count()) === 1;
await expect(hasText).toBe(true);

Check failure on line 37 in packages/web-platform/web-tests/tests/react.spec.ts

View workflow job for this annotation

GitHub Actions / playwright-linux / check

[webkit] › tests/react.spec.ts:116:5 › reactlynx3 tests › basic › basic-setstate-in-constructor

3) [webkit] › tests/react.spec.ts:116:5 › reactlynx3 tests › basic › basic-setstate-in-constructor Error: expect(received).toBe(expected) // Object.is equality Expected: true Received: false 35 | const expectHasText = async (page: Page, text: string) => { 36 | const hasText = (await page.getByText(text).count()) === 1; > 37 | await expect(hasText).toBe(true); | ^ 38 | }; 39 | 40 | const expectNoText = async (page: Page, text: string) => { at expectHasText (/home/runner/_work/lynx-stack/lynx-stack/packages/web-platform/web-tests/tests/react.spec.ts:37:25) at /home/runner/_work/lynx-stack/lynx-stack/packages/web-platform/web-tests/tests/react.spec.ts:119:7

Check failure on line 37 in packages/web-platform/web-tests/tests/react.spec.ts

View workflow job for this annotation

GitHub Actions / playwright-linux / check

[webkit] › tests/react.spec.ts:121:5 › reactlynx3 tests › basic › basic-setsate-with-cb

4) [webkit] › tests/react.spec.ts:121:5 › reactlynx3 tests › basic › basic-setsate-with-cb ─────── Error: expect(received).toBe(expected) // Object.is equality Expected: true Received: false 35 | const expectHasText = async (page: Page, text: string) => { 36 | const hasText = (await page.getByText(text).count()) === 1; > 37 | await expect(hasText).toBe(true); | ^ 38 | }; 39 | 40 | const expectNoText = async (page: Page, text: string) => { at expectHasText (/home/runner/_work/lynx-stack/lynx-stack/packages/web-platform/web-tests/tests/react.spec.ts:37:25) at /home/runner/_work/lynx-stack/lynx-stack/packages/web-platform/web-tests/tests/react.spec.ts:124:7
};

const expectNoText = async (page: Page, text: string) => {
Expand All @@ -42,8 +42,20 @@
await expect(hasText).toBe(false);
};

const goto = async (page: Page, testname: string, hasDir?: boolean) => {
await page.goto(`/?casename=${testname}${hasDir ? '&hasdir=true' : ''}`, {
const goto = async (
page: Page,
testname: string,
testname2?: string,
hasDir?: boolean,
) => {
let url = `/?casename=${testname}`;
if (hasDir) {
url += '&hasdir=true';
}
if (testname2) {
url += `&casename2=${testname2}`;
}
await page.goto(url, {
waitUntil: 'load',
});
await page.evaluate(() => document.fonts.ready);
Expand Down Expand Up @@ -79,7 +91,7 @@
await wait(100);
const target = page.locator('#target');
await target.click();
await expect(await target.getAttribute('style')).toContain('green');

Check failure on line 94 in packages/web-platform/web-tests/tests/react.spec.ts

View workflow job for this annotation

GitHub Actions / playwright-linux / check

[webkit] › tests/react.spec.ts:89:5 › reactlynx3 tests › basic › basic-bindtap

2) [webkit] › tests/react.spec.ts:89:5 › reactlynx3 tests › basic › basic-bindtap ──────────────── Error: expect(received).toContain(expected) // indexOf Expected substring: "green" Received string: "height: 100px; width: 100px; background: pink;" 92 | const target = page.locator('#target'); 93 | await target.click(); > 94 | await expect(await target.getAttribute('style')).toContain('green'); | ^ 95 | await target.click(); 96 | await expect(await target.getAttribute('style')).toContain('pink'); 97 | }); at /home/runner/_work/lynx-stack/lynx-stack/packages/web-platform/web-tests/tests/react.spec.ts:94:56
await target.click();
await expect(await target.getAttribute('style')).toContain('pink');
});
Expand Down Expand Up @@ -531,6 +543,31 @@
expect(page.workers().length).toBe(5);
});

test('api-setSharedData', async ({ page }, { title }) => {
await goto(page, title);
await wait(100);
await expect(page.locator('#target')).toHaveCSS(
'background-color',
'rgb(0, 128, 0)',
); // green
});

test('api-shared-context', async ({ page }) => {
await goto(page, 'api-setSharedData', 'api-getSharedData');
await wait(100);
await page.locator('#lynxview2').locator('#target').click();
await expect(page.locator('#lynxview2').locator('#target')).toHaveCSS(
'background-color',
'rgb(0, 128, 0)',
); // green
});

test('api-shared-context-worker-count', async ({ page }) => {
await goto(page, 'api-setSharedData', 'api-getSharedData');
await wait(100);
expect(page.workers().length).toBeLessThanOrEqual(4);
});

test.describe('api-exposure', () => {
const module = 'exposure';
test(
Expand All @@ -539,9 +576,9 @@
await goto(page, title);
await diffScreenShot(page, module, title, 'initial');
await page.evaluate(() => {
// @ts-expect-error
document.querySelector('lynx-view')!.shadowRoot!.querySelector(
'#x',
// @ts-expect-error
)!.scrollTo({ offset: 200 });
});
await wait(200);
Expand All @@ -553,9 +590,9 @@
);
await wait(200);
await page.evaluate(() => {
// @ts-expect-error
document.querySelector('lynx-view')!.shadowRoot!.querySelector(
'#x',
// @ts-expect-error
)!.scrollTo({ offset: 400 });
});
await diffScreenShot(page, module, title, 'scroll-200-green');
Expand All @@ -574,8 +611,8 @@
});
await wait(100);
await page.evaluate(() => {
// @ts-expect-error
document.querySelector('lynx-view')!.shadowRoot!.querySelector('#x')!
// @ts-expect-error
.scrollTo({ offset: 600 });
});
await wait(100);
Expand All @@ -584,8 +621,8 @@
});
await wait(100);
await page.evaluate(() => {
// @ts-expect-error
document.querySelector('lynx-view')!.shadowRoot!.querySelector('#x')!
// @ts-expect-error
.scrollTo({ offset: 0 });
});
await wait(100);
Expand All @@ -594,8 +631,8 @@
});
await wait(100);
await page.evaluate(() => {
// @ts-expect-error
document.querySelector('lynx-view')!.shadowRoot!.querySelector('#y')!
// @ts-expect-error
.scrollTo({ offset: 50 });
});
await wait(100);
Expand All @@ -604,8 +641,8 @@
});
await wait(100);
await page.evaluate(() => {
// @ts-expect-error
document.querySelector('lynx-view')!.shadowRoot!.querySelector('#y')!
// @ts-expect-error
.scrollTo({ offset: 0 });
});
await wait(100);
Expand Down Expand Up @@ -964,7 +1001,7 @@
test(
'config-splitchunk-single-vendor',
async ({ page }, { title }) => {
await goto(page, title, true);
await goto(page, title, undefined, true);
await wait(1500);
const target = page.locator('#target');
await expect(target).toHaveCSS('background-color', 'rgb(0, 128, 0)'); // green
Expand All @@ -973,7 +1010,7 @@
test(
'config-splitchunk-split-by-experience',
async ({ page }, { title }) => {
await goto(page, title, true);
await goto(page, title, undefined, true);
await wait(1500);
const target = page.locator('#target');
await expect(target).toHaveCSS('background-color', 'rgb(0, 128, 0)'); // green
Expand All @@ -982,19 +1019,19 @@
test(
'config-splitchunk-split-by-module',
async ({ page }, { title }) => {
await goto(page, title, true);
await goto(page, title, undefined, true);
await wait(1500);
const target = page.locator('#target');
await expect(target).toHaveCSS('background-color', 'rgb(0, 128, 0)'); // green
},
);

test('config-mode-dev-with-all-in-one', async ({ page }, { title }) => {
await goto(page, title, true);
await goto(page, title, undefined, true);
await wait(100);
const target = page.locator('#target');
await target.click();
await expect(await target.getAttribute('style')).toContain('green');

Check failure on line 1034 in packages/web-platform/web-tests/tests/react.spec.ts

View workflow job for this annotation

GitHub Actions / playwright-linux / check

[webkit] › tests/react.spec.ts:1029:5 › reactlynx3 tests › configs › config-mode-dev-with-all-in-one

5) [webkit] › tests/react.spec.ts:1029:5 › reactlynx3 tests › configs › config-mode-dev-with-all-in-one Error: expect(received).toContain(expected) // indexOf Expected substring: "green" Received string: "height: 100px; width: 100px; background: pink;" 1032 | const target = page.locator('#target'); 1033 | await target.click(); > 1034 | await expect(await target.getAttribute('style')).toContain('green'); | ^ 1035 | await target.click(); 1036 | await expect(await target.getAttribute('style')).toContain('pink'); 1037 | }); at /home/runner/_work/lynx-stack/lynx-stack/packages/web-platform/web-tests/tests/react.spec.ts:1034:56
await target.click();
await expect(await target.getAttribute('style')).toContain('pink');
});
Expand Down Expand Up @@ -1234,7 +1271,7 @@
await wait(500);
const count = (await page.getByText('the count is:1').count())
+ (await await page.getByText('the count is:2').count());
expect(count).toBe(1);

Check failure on line 1274 in packages/web-platform/web-tests/tests/react.spec.ts

View workflow job for this annotation

GitHub Actions / playwright-linux / check

[webkit] › tests/react.spec.ts:1267:7 › reactlynx3 tests › elements › text › basic-element-text-set-native-props-text

6) [webkit] › tests/react.spec.ts:1267:7 › reactlynx3 tests › elements › text › basic-element-text-set-native-props-text Error: expect(received).toBe(expected) // Object.is equality Expected: 1 Received: 0 1272 | const count = (await page.getByText('the count is:1').count()) 1273 | + (await await page.getByText('the count is:2').count()); > 1274 | expect(count).toBe(1); | ^ 1275 | }, 1276 | ); 1277 | at /home/runner/_work/lynx-stack/lynx-stack/packages/web-platform/web-tests/tests/react.spec.ts:1274:25
},
);

Expand Down Expand Up @@ -1273,7 +1310,7 @@
await wait(300);
// --initialtextinitial
let count = await page.getByText('--').count();
expect(count).toBe(1);

Check failure on line 1313 in packages/web-platform/web-tests/tests/react.spec.ts

View workflow job for this annotation

GitHub Actions / playwright-linux / check

[webkit] › tests/react.spec.ts:1306:7 › reactlynx3 tests › elements › text › basic-element-text-set-native-props-with-setData

7) [webkit] › tests/react.spec.ts:1306:7 › reactlynx3 tests › elements › text › basic-element-text-set-native-props-with-setData Error: expect(received).toBe(expected) // Object.is equality Expected: 1 Received: 0 1311 | // --initialtextinitial 1312 | let count = await page.getByText('--').count(); > 1313 | expect(count).toBe(1); | ^ 1314 | count = await page.getByText('initial').count(); 1315 | expect(count).toBeGreaterThanOrEqual(1); 1316 | await page.locator('#target').click(); at /home/runner/_work/lynx-stack/lynx-stack/packages/web-platform/web-tests/tests/react.spec.ts:1313:25
count = await page.getByText('initial').count();
expect(count).toBeGreaterThanOrEqual(1);
await page.locator('#target').click();
Expand Down
Loading
Loading