@lynx-js/react 0.110.1 → 0.111.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 (80) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/components/lib/DeferredListItem.d.ts +7 -0
  3. package/components/lib/DeferredListItem.jsx +40 -0
  4. package/components/lib/DeferredListItem.jsx.map +1 -0
  5. package/components/lib/index.d.ts +1 -0
  6. package/components/lib/index.js +1 -0
  7. package/components/lib/index.js.map +1 -1
  8. package/components/src/DeferredListItem.tsx +56 -0
  9. package/components/src/index.ts +1 -0
  10. package/package.json +1 -1
  11. package/refresh/.turbo/turbo-build.log +1 -1
  12. package/runtime/lazy/react-lepus.js +1 -0
  13. package/runtime/lazy/react.js +1 -0
  14. package/runtime/lepus/index.d.ts +1 -1
  15. package/runtime/lepus/index.js +44 -0
  16. package/runtime/lib/backgroundSnapshot.d.ts +2 -1
  17. package/runtime/lib/backgroundSnapshot.js +62 -40
  18. package/runtime/lib/backgroundSnapshot.js.map +1 -1
  19. package/runtime/lib/compat/initData.js +10 -0
  20. package/runtime/lib/compat/initData.js.map +1 -1
  21. package/runtime/lib/index.d.ts +2 -2
  22. package/runtime/lib/index.js +2 -2
  23. package/runtime/lib/index.js.map +1 -1
  24. package/runtime/lib/lifecycle/patch/commit.js +5 -5
  25. package/runtime/lib/lifecycle/patch/commit.js.map +1 -1
  26. package/runtime/lib/lifecycle/patch/snapshotPatch.d.ts +9 -9
  27. package/runtime/lib/lifecycle/patch/snapshotPatch.js +9 -10
  28. package/runtime/lib/lifecycle/patch/snapshotPatch.js.map +1 -1
  29. package/runtime/lib/lifecycle/patch/updateMainThread.js +7 -8
  30. package/runtime/lib/lifecycle/patch/updateMainThread.js.map +1 -1
  31. package/runtime/lib/lifecycleConstant.d.ts +2 -1
  32. package/runtime/lib/lifecycleConstant.js +1 -0
  33. package/runtime/lib/lifecycleConstant.js.map +1 -1
  34. package/runtime/lib/list.js +102 -12
  35. package/runtime/lib/list.js.map +1 -1
  36. package/runtime/lib/lynx/calledByNative.js +6 -9
  37. package/runtime/lib/lynx/calledByNative.js.map +1 -1
  38. package/runtime/lib/lynx/component.js +11 -14
  39. package/runtime/lib/lynx/component.js.map +1 -1
  40. package/runtime/lib/lynx/env.js +1 -2
  41. package/runtime/lib/lynx/env.js.map +1 -1
  42. package/runtime/lib/lynx/lazy-bundle.js +48 -21
  43. package/runtime/lib/lynx/lazy-bundle.js.map +1 -1
  44. package/runtime/lib/lynx/performance.d.ts +3 -19
  45. package/runtime/lib/lynx/performance.js +25 -26
  46. package/runtime/lib/lynx/performance.js.map +1 -1
  47. package/runtime/lib/lynx/tt.js +10 -5
  48. package/runtime/lib/lynx/tt.js.map +1 -1
  49. package/runtime/lib/lynx-api.d.ts +78 -1
  50. package/runtime/lib/lynx-api.js.map +1 -1
  51. package/runtime/lib/snapshot.d.ts +2 -0
  52. package/runtime/lib/snapshot.js +19 -5
  53. package/runtime/lib/snapshot.js.map +1 -1
  54. package/runtime/lib/utils.d.ts +1 -0
  55. package/runtime/lib/utils.js +6 -0
  56. package/runtime/lib/utils.js.map +1 -1
  57. package/runtime/src/backgroundSnapshot.ts +74 -55
  58. package/runtime/src/compat/initData.ts +10 -0
  59. package/runtime/src/index.ts +2 -0
  60. package/runtime/src/lifecycle/patch/commit.ts +5 -11
  61. package/runtime/src/lifecycle/patch/snapshotPatch.ts +9 -9
  62. package/runtime/src/lifecycle/patch/updateMainThread.ts +7 -8
  63. package/runtime/src/lifecycleConstant.ts +1 -0
  64. package/runtime/src/list.ts +118 -15
  65. package/runtime/src/lynx/calledByNative.ts +6 -8
  66. package/runtime/src/lynx/component.ts +17 -29
  67. package/runtime/src/lynx/env.ts +1 -2
  68. package/runtime/src/lynx/lazy-bundle.ts +55 -20
  69. package/runtime/src/lynx/performance.ts +26 -27
  70. package/runtime/src/lynx/tt.ts +10 -11
  71. package/runtime/src/lynx-api.ts +77 -1
  72. package/runtime/src/snapshot.ts +20 -5
  73. package/runtime/src/utils.ts +9 -0
  74. package/testing-library/dist/index.d.ts +4 -1
  75. package/testing-library/dist/pure.js +1 -1
  76. package/testing-library/dist/vitest.config.js +38 -1
  77. package/testing-library/types/entry.d.ts +3 -2
  78. package/transform/dist/wasm.cjs +1 -1
  79. package/types/react.d.ts +21 -1
  80. package/types/react.docs.d.ts +1 -1
@@ -35,8 +35,7 @@ import { onPostWorkletCtx } from './worklet/ctx.js';
35
35
  export class BackgroundSnapshotInstance {
36
36
  constructor(public type: string) {
37
37
  this.__snapshot_def = snapshotManager.values.get(type)!;
38
- let id;
39
- id = this.__id = backgroundSnapshotInstanceManager.nextId += 1;
38
+ const id = this.__id = backgroundSnapshotInstanceManager.nextId += 1;
40
39
  backgroundSnapshotInstanceManager.values.set(id, this);
41
40
 
42
41
  __globalSnapshotPatch?.push(SnapshotOperation.CreateElement, type, id);
@@ -45,6 +44,7 @@ export class BackgroundSnapshotInstance {
45
44
  __id: number;
46
45
  __values: any[] | undefined;
47
46
  __snapshot_def: Snapshot;
47
+ __extraProps?: Record<string, unknown> | undefined;
48
48
 
49
49
  private __parent: BackgroundSnapshotInstance | null = null;
50
50
  private __firstChild: BackgroundSnapshotInstance | null = null;
@@ -191,7 +191,7 @@ export class BackgroundSnapshotInstance {
191
191
  return nodes;
192
192
  }
193
193
 
194
- setAttribute(key: string | number, value: any): void {
194
+ setAttribute(key: string | number, value: unknown): void {
195
195
  if (__PROFILE__) {
196
196
  console.profile('setAttribute');
197
197
  }
@@ -199,8 +199,12 @@ export class BackgroundSnapshotInstance {
199
199
  if (__globalSnapshotPatch) {
200
200
  const oldValues = this.__values;
201
201
  if (oldValues) {
202
- for (let index = 0; index < value.length; index++) {
203
- const { needUpdate, valueToCommit } = this.setAttributeImpl(value[index], oldValues[index], index);
202
+ for (let index = 0; index < (value as unknown[]).length; index++) {
203
+ const { needUpdate, valueToCommit } = this.setAttributeImpl(
204
+ (value as unknown[])[index],
205
+ oldValues[index],
206
+ index,
207
+ );
204
208
  if (needUpdate) {
205
209
  __globalSnapshotPatch.push(
206
210
  SnapshotOperation.SetAttribute,
@@ -212,9 +216,9 @@ export class BackgroundSnapshotInstance {
212
216
  }
213
217
  } else {
214
218
  const patch = [];
215
- const length = value.length;
219
+ const length = (value as unknown[]).length;
216
220
  for (let index = 0; index < length; ++index) {
217
- const { valueToCommit } = this.setAttributeImpl(value[index], null, index);
221
+ const { valueToCommit } = this.setAttributeImpl((value as unknown[])[index], null, index);
218
222
  patch[index] = valueToCommit;
219
223
  }
220
224
  __globalSnapshotPatch.push(
@@ -235,22 +239,24 @@ export class BackgroundSnapshotInstance {
235
239
  }
236
240
  });
237
241
  }
238
- this.__values = value;
242
+ this.__values = value as unknown[];
239
243
  if (__PROFILE__) {
240
244
  console.profileEnd();
241
245
  }
242
246
  return;
243
247
  }
244
248
 
245
- // old path (`<__snapshot_xxxx_xxxx __0={} __1={} />` or `this.setAttribute(0, xxx)`)
246
- // is reserved as slow path
247
- const index = typeof key === 'string' ? Number(key.slice(2)) : key;
248
- (this.__values ??= [])[index] = value;
249
-
249
+ if (typeof key === 'string') {
250
+ (this.__extraProps ??= {})[key] = value;
251
+ } else {
252
+ // old path (`this.setAttribute(0, xxx)`)
253
+ // is reserved as slow path
254
+ (this.__values ??= [])[key] = value;
255
+ }
250
256
  __globalSnapshotPatch?.push(
251
257
  SnapshotOperation.SetAttribute,
252
258
  this.__id,
253
- index,
259
+ key,
254
260
  value,
255
261
  );
256
262
  if (__PROFILE__) {
@@ -341,7 +347,7 @@ export function hydrate(
341
347
  ): SnapshotPatch {
342
348
  initGlobalSnapshotPatch();
343
349
 
344
- const helper2 = (afters: BackgroundSnapshotInstance[], parentId: number) => {
350
+ const helper2 = (afters: BackgroundSnapshotInstance[], parentId: number, targetId?: number) => {
345
351
  for (const child of afters) {
346
352
  const id = child.__id;
347
353
  __globalSnapshotPatch!.push(SnapshotOperation.CreateElement, child.type, id);
@@ -350,8 +356,12 @@ export function hydrate(
350
356
  child.__values = undefined;
351
357
  child.setAttribute('values', values);
352
358
  }
359
+ const extraProps = child.__extraProps;
360
+ for (const key in extraProps) {
361
+ child.setAttribute(key, extraProps[key]);
362
+ }
353
363
  helper2(child.childNodes, id);
354
- __globalSnapshotPatch!.push(SnapshotOperation.InsertBefore, parentId, id, undefined);
364
+ __globalSnapshotPatch!.push(SnapshotOperation.InsertBefore, parentId, id, targetId);
355
365
  }
356
366
  };
357
367
 
@@ -361,35 +371,45 @@ export function hydrate(
361
371
  ) => {
362
372
  hydrationMap.set(after.__id, before.id);
363
373
  backgroundSnapshotInstanceManager.updateId(after.__id, before.id);
364
- after.__values?.forEach((value, index) => {
365
- const old = before.values![index];
374
+ after.__values?.forEach((value: unknown, index) => {
375
+ const old: unknown = before.values![index];
366
376
 
367
377
  if (value) {
368
- if (value.__spread) {
369
- // `value.__spread` my contain event ids using snapshot ids before hydration. Remove it.
370
- delete value.__spread;
371
- value = transformSpread(after, index, value);
372
- for (const key in value) {
373
- if (value[key] && value[key]._wkltId) {
374
- onPostWorkletCtx(value[key]);
375
- } else if (value[key] && value[key].__isGesture) {
376
- processGestureBackground(value[key] as GestureKind);
378
+ if (typeof value === 'object') {
379
+ if ('__spread' in value) {
380
+ // `value.__spread` my contain event ids using snapshot ids before hydration. Remove it.
381
+ delete value.__spread;
382
+ const __spread = transformSpread(after, index, value);
383
+ for (const key in __spread) {
384
+ const v = __spread[key];
385
+ if (v && typeof v === 'object') {
386
+ if ('_wkltId' in v) {
387
+ onPostWorkletCtx(v as Worklet);
388
+ } else if ('__isGesture' in v) {
389
+ processGestureBackground(v as GestureKind);
390
+ }
391
+ }
377
392
  }
393
+ (after.__values![index]! as Record<string, unknown>)['__spread'] = __spread;
394
+ value = __spread;
395
+ } else if ('__ref' in value) {
396
+ // skip patch
397
+ value = old;
398
+ } else if ('_wkltId' in value) {
399
+ onPostWorkletCtx(value as Worklet);
400
+ } else if ('__isGesture' in value) {
401
+ processGestureBackground(value as GestureKind);
378
402
  }
379
- after.__values![index]!.__spread = value;
380
- } else if (value.__ref) {
381
- // skip patch
382
- value = old;
383
403
  } else if (typeof value === 'function') {
384
- value = `${after.__id}:${index}:`;
404
+ if ('__ref' in value) {
405
+ // skip patch
406
+ value = old;
407
+ } else {
408
+ value = `${after.__id}:${index}:`;
409
+ }
385
410
  }
386
411
  }
387
412
 
388
- if (value && value._wkltId) {
389
- onPostWorkletCtx(value);
390
- } else if (value && value.__isGesture) {
391
- processGestureBackground(value);
392
- }
393
413
  if (!isDirectOrDeepEqual(value, old)) {
394
414
  __globalSnapshotPatch!.push(
395
415
  SnapshotOperation.SetAttribute,
@@ -400,9 +420,24 @@ export function hydrate(
400
420
  }
401
421
  });
402
422
 
423
+ if (after.__extraProps) {
424
+ for (const key in after.__extraProps) {
425
+ const value = after.__extraProps[key];
426
+ const old = before.extraProps?.[key];
427
+ if (!isDirectOrDeepEqual(value, old)) {
428
+ __globalSnapshotPatch!.push(
429
+ SnapshotOperation.SetAttribute,
430
+ after.__id,
431
+ key,
432
+ value,
433
+ );
434
+ }
435
+ }
436
+ }
437
+
403
438
  const { slot } = after.__snapshot_def;
404
439
 
405
- const beforeChildNodes = before.children || [];
440
+ const beforeChildNodes = before.children ?? [];
406
441
  const afterChildNodes = after.childNodes;
407
442
 
408
443
  if (!slot) {
@@ -433,23 +468,7 @@ export function hydrate(
433
468
  beforeChildNodes,
434
469
  diffResult,
435
470
  (node, target) => {
436
- __globalSnapshotPatch!.push(
437
- SnapshotOperation.CreateElement,
438
- node.type,
439
- node.__id,
440
- );
441
- helper2(node.childNodes, node.__id);
442
- const values = node.__values;
443
- if (values) {
444
- node.__values = undefined;
445
- node.setAttribute('values', values);
446
- }
447
- __globalSnapshotPatch!.push(
448
- SnapshotOperation.InsertBefore,
449
- before.id,
450
- node.__id,
451
- target?.id,
452
- );
471
+ helper2([node], before.id, target?.id);
453
472
  return undefined as unknown as SerializedSnapshotInstance;
454
473
  },
455
474
  node => {
@@ -5,6 +5,7 @@ import type { ComponentChildren, Consumer, Context, Provider } from 'preact';
5
5
  import type { ComponentClass } from 'react';
6
6
 
7
7
  import { useLynxGlobalEventListener } from '../hooks/useLynxGlobalEventListener.js';
8
+ import { globalFlushOptions } from '../lifecycle/patch/commit.js';
8
9
 
9
10
  type Getter<T> = {
10
11
  [key in keyof T]: () => T[key];
@@ -33,6 +34,9 @@ export function factory<Data>(
33
34
  const [__, set] = useState<Data>(lynx[prop] as Data);
34
35
 
35
36
  const handleChange = () => {
37
+ if (prop === '__initData') {
38
+ globalFlushOptions.triggerDataUpdated = true;
39
+ }
36
40
  set(lynx[prop] as Data);
37
41
  };
38
42
 
@@ -52,6 +56,9 @@ export function factory<Data>(
52
56
  const use = (): Data => {
53
57
  const [__, set] = useState(lynx[prop]);
54
58
  useChanged(() => {
59
+ if (prop === '__initData') {
60
+ globalFlushOptions.triggerDataUpdated = true;
61
+ }
55
62
  set(lynx[prop]);
56
63
  });
57
64
 
@@ -65,6 +72,7 @@ export function factory<Data>(
65
72
  };
66
73
 
67
74
  return {
75
+ /* v8 ignore next */
68
76
  Context: () => Context,
69
77
  Provider: () => Provider,
70
78
  Consumer: () => Consumer,
@@ -100,6 +108,7 @@ export function factory<Data>(
100
108
  */
101
109
  export function withInitDataInState<P, S>(App: ComponentClass<P, S>): ComponentClass<P, S> {
102
110
  const isClassComponent = 'prototype' in App && 'render' in App.prototype;
111
+ /* v8 ignore next 4 */
103
112
  if (!isClassComponent) {
104
113
  // return as-is when not class component
105
114
  return App;
@@ -119,6 +128,7 @@ export function withInitDataInState<P, S>(App: ComponentClass<P, S>): ComponentC
119
128
  lynx.getJSModule('GlobalEventEmitter').addListener(
120
129
  'onDataChanged',
121
130
  this.h = () => {
131
+ globalFlushOptions.triggerDataUpdated = true;
122
132
  this.setState(lynx.__initData as S);
123
133
  },
124
134
  );
@@ -11,6 +11,7 @@ import {
11
11
  PureComponent,
12
12
  Suspense,
13
13
  lazy as backgroundLazy,
14
+ cloneElement,
14
15
  createContext,
15
16
  createElement,
16
17
  createRef,
@@ -86,6 +87,7 @@ export {
86
87
  Suspense,
87
88
  lazy,
88
89
  createElement,
90
+ cloneElement,
89
91
  useSyncExternalStore,
90
92
  };
91
93
 
@@ -23,13 +23,7 @@ import type { VNode } from 'preact';
23
23
  import { options } from 'preact';
24
24
 
25
25
  import { LifecycleConstant } from '../../lifecycleConstant.js';
26
- import {
27
- PerformanceTimingKeys,
28
- globalPipelineOptions,
29
- markTiming,
30
- markTimingLegacy,
31
- setPipeline,
32
- } from '../../lynx/performance.js';
26
+ import { globalPipelineOptions, markTiming, markTimingLegacy, setPipeline } from '../../lynx/performance.js';
33
27
  import { COMMIT } from '../../renderToOpcodes/constants.js';
34
28
  import { applyQueuedRefs } from '../../snapshot/ref.js';
35
29
  import { backgroundSnapshotInstanceManager } from '../../snapshot.js';
@@ -88,8 +82,8 @@ function replaceCommitHook(): void {
88
82
  }
89
83
 
90
84
  // Mark the end of virtual DOM diffing phase for performance tracking
91
- markTimingLegacy(PerformanceTimingKeys.updateDiffVdomEnd);
92
- markTiming(PerformanceTimingKeys.diffVdomEnd);
85
+ markTimingLegacy('updateDiffVdomEnd');
86
+ markTiming('diffVdomEnd');
93
87
 
94
88
  const backgroundSnapshotInstancesToRemove = globalBackgroundSnapshotInstancesToRemove;
95
89
  globalBackgroundSnapshotInstancesToRemove = [];
@@ -167,7 +161,7 @@ function commitPatchUpdate(patchList: PatchList, patchOptions: Omit<PatchOptions
167
161
  if (__PROFILE__) {
168
162
  console.profile('commitChanges');
169
163
  }
170
- markTiming(PerformanceTimingKeys.packChangesStart);
164
+ markTiming('packChangesStart');
171
165
  const obj: {
172
166
  data: string;
173
167
  patchOptions: PatchOptions;
@@ -178,7 +172,7 @@ function commitPatchUpdate(patchList: PatchList, patchOptions: Omit<PatchOptions
178
172
  reloadVersion: getReloadVersion(),
179
173
  },
180
174
  };
181
- markTiming(PerformanceTimingKeys.packChangesEnd);
175
+ markTiming('packChangesEnd');
182
176
  if (globalPipelineOptions) {
183
177
  obj.patchOptions.pipelineOptions = globalPipelineOptions;
184
178
  setPipeline(undefined);
@@ -8,16 +8,16 @@
8
8
  * efficient transmission between threads and application to element tree.
9
9
  */
10
10
 
11
- export const enum SnapshotOperation {
12
- CreateElement,
13
- InsertBefore,
14
- RemoveChild,
15
- SetAttribute,
16
- SetAttributes,
11
+ export const SnapshotOperation = {
12
+ CreateElement: 0,
13
+ InsertBefore: 1,
14
+ RemoveChild: 2,
15
+ SetAttribute: 3,
16
+ SetAttributes: 4,
17
17
 
18
- DEV_ONLY_AddSnapshot = 100,
19
- DEV_ONLY_RegisterWorklet = 101,
20
- }
18
+ DEV_ONLY_AddSnapshot: 100,
19
+ DEV_ONLY_RegisterWorklet: 101,
20
+ } as const;
21
21
 
22
22
  // Operation format definitions:
23
23
  //
@@ -8,7 +8,7 @@ import type { PatchList, PatchOptions } from './commit.js';
8
8
  import { setMainThreadHydrationFinished } from './isMainThreadHydrationFinished.js';
9
9
  import { snapshotPatchApply } from './snapshotPatchApply.js';
10
10
  import { LifecycleConstant } from '../../lifecycleConstant.js';
11
- import { PerformanceTimingKeys, markTiming, setPipeline } from '../../lynx/performance.js';
11
+ import { markTiming, setPipeline } from '../../lynx/performance.js';
12
12
  import { __pendingListUpdates } from '../../pendingListUpdates.js';
13
13
  import { applyRefQueue } from '../../snapshot/workletRef.js';
14
14
  import { __page } from '../../snapshot.js';
@@ -25,12 +25,12 @@ function updateMainThread(
25
25
  }
26
26
 
27
27
  setPipeline(patchOptions.pipelineOptions);
28
- markTiming(PerformanceTimingKeys.mtsRenderStart);
29
- markTiming(PerformanceTimingKeys.parseChangesStart);
28
+ markTiming('mtsRenderStart');
29
+ markTiming('parseChangesStart');
30
30
  const { patchList, flushOptions = {} } = JSON.parse(data) as PatchList;
31
31
 
32
- markTiming(PerformanceTimingKeys.parseChangesEnd);
33
- markTiming(PerformanceTimingKeys.patchChangesStart);
32
+ markTiming('parseChangesEnd');
33
+ markTiming('patchChangesStart');
34
34
 
35
35
  for (const { snapshotPatch, workletRefInitValuePatch } of patchList) {
36
36
  updateWorkletRefInitValueChanges(workletRefInitValuePatch);
@@ -42,8 +42,8 @@ function updateMainThread(
42
42
  // console.debug('********** Lepus updatePatch:');
43
43
  // printSnapshotInstance(snapshotInstanceManager.values.get(-1)!);
44
44
  }
45
- markTiming(PerformanceTimingKeys.patchChangesEnd);
46
- markTiming(PerformanceTimingKeys.mtsRenderEnd);
45
+ markTiming('patchChangesEnd');
46
+ markTiming('mtsRenderEnd');
47
47
  if (patchOptions.isHydration) {
48
48
  setMainThreadHydrationFinished(true);
49
49
  }
@@ -51,7 +51,6 @@ function updateMainThread(
51
51
  if (patchOptions.pipelineOptions) {
52
52
  flushOptions.pipelineOptions = patchOptions.pipelineOptions;
53
53
  }
54
- // TODO: triggerDataUpdated?
55
54
  __FlushElementTree(__page, flushOptions);
56
55
  }
57
56
 
@@ -7,6 +7,7 @@ export const enum LifecycleConstant {
7
7
  globalEventFromLepus = 'globalEventFromLepus',
8
8
  jsReady = 'rLynxJSReady',
9
9
  patchUpdate = 'rLynxChange',
10
+ publishEvent = 'rLynxPublishEvent',
10
11
  }
11
12
 
12
13
  export interface FirstScreenData {
@@ -1,12 +1,15 @@
1
1
  // Copyright 2024 The Lynx Authors. All rights reserved.
2
2
  // Licensed under the Apache License Version 2.0 that can be found in the
3
3
  // LICENSE file in the root directory of this source tree.
4
+ import { LifecycleConstant } from './lifecycleConstant.js';
4
5
  import { applyRefQueue } from './snapshot/workletRef.js';
5
6
  import type { SnapshotInstance } from './snapshot.js';
7
+ import { maybePromise } from './utils.js';
6
8
 
7
9
  export const gSignMap: Record<number, Map<number, SnapshotInstance>> = {};
8
10
  export const gRecycleMap: Record<number, Map<string, Map<number, SnapshotInstance>>> = {};
9
11
  const gParentWeakMap: WeakMap<SnapshotInstance, unknown> = new WeakMap();
12
+ const resolvedPromise = /* @__PURE__ */ Promise.resolve();
10
13
 
11
14
  export function clearListGlobal(): void {
12
15
  for (const key in gSignMap) {
@@ -38,10 +41,10 @@ export function componentAtIndexFactory(
38
41
  }
39
42
  });
40
43
 
41
- const componentAtIndex = (
44
+ const componentAtChildCtx = (
42
45
  list: FiberElement,
43
46
  listID: number,
44
- cellIndex: number,
47
+ childCtx: SnapshotInstance,
45
48
  operationID: number,
46
49
  enableReuseNotification: boolean,
47
50
  enableBatchRender: boolean = false,
@@ -53,13 +56,55 @@ export function componentAtIndexFactory(
53
56
  throw new Error('componentAtIndex called on removed list');
54
57
  }
55
58
 
56
- const childCtx = ctx[cellIndex];
57
- if (!childCtx) {
58
- throw new Error('childCtx not found');
59
- }
60
-
61
59
  const platformInfo = childCtx.__listItemPlatformInfo ?? {};
62
60
 
61
+ // The lifecycle of this `__extraProps.isReady`:
62
+ // 0 -> Promise<number> -> 1
63
+ // 0: The initial state, the list-item is not ready yet, we will send a event to background
64
+ // when `componentAtIndex` is called on it
65
+ // Promise<number>: A promise that will be resolved when the list-item is ready
66
+ // 1: The list-item is ready, we can use it to render the list
67
+ if (childCtx.__extraProps?.['isReady'] === 0) {
68
+ if (
69
+ typeof __GetAttributeByName === 'function'
70
+ && __GetAttributeByName(list, 'custom-list-name') === 'list-container'
71
+ ) {
72
+ // we are in supported env
73
+ // do not throw
74
+ } else {
75
+ throw new Error(
76
+ 'Unsupported: `<list-item/>` with `defer={true}` must be used with `<list custom-list-name="list-container"/>`',
77
+ );
78
+ }
79
+
80
+ __OnLifecycleEvent([LifecycleConstant.publishEvent, {
81
+ handlerName: `${childCtx.__id}:__extraProps:onComponentAtIndex`,
82
+ data: {},
83
+ }]);
84
+
85
+ let p: Promise<number>;
86
+ return (p = new Promise<number>((resolve) => {
87
+ Object.defineProperty(childCtx.__extraProps, 'isReady', {
88
+ set(isReady) {
89
+ if (isReady === 1) {
90
+ delete childCtx.__extraProps!['isReady'];
91
+ childCtx.__extraProps!['isReady'] = 1;
92
+
93
+ void resolvedPromise.then(() => {
94
+ // the cellIndex may be changed already, but the `childCtx` is the same
95
+ resolve(componentAtChildCtx(list, listID, childCtx, operationID, enableReuseNotification));
96
+ });
97
+ }
98
+ },
99
+ get() {
100
+ return p;
101
+ },
102
+ });
103
+ }));
104
+ } else if (maybePromise<number>(childCtx.__extraProps?.['isReady'])) {
105
+ throw new Error('componentAtIndex was called on a pending deferred list item');
106
+ }
107
+
63
108
  const uniqID = childCtx.type + (platformInfo['reuse-identifier'] ?? '');
64
109
  const recycleSignMap = recycleMap.get(uniqID);
65
110
 
@@ -102,6 +147,11 @@ export function componentAtIndexFactory(
102
147
  oldCtx.unRenderElements();
103
148
  if (!oldCtx.__id) {
104
149
  oldCtx.tearDown();
150
+ } else if (oldCtx.__extraProps?.['isReady'] === 1) {
151
+ __OnLifecycleEvent([LifecycleConstant.publishEvent, {
152
+ handlerName: `${oldCtx.__id}:__extraProps:onRecycleComponent`,
153
+ data: {},
154
+ }]);
105
155
  }
106
156
  const root = childCtx.__element_root!;
107
157
  applyRefQueue();
@@ -156,25 +206,78 @@ export function componentAtIndexFactory(
156
206
  return sign;
157
207
  };
158
208
 
159
- const componentAtIndexes = (
209
+ function componentAtIndex(
210
+ list: FiberElement,
211
+ listID: number,
212
+ cellIndex: number,
213
+ operationID: number,
214
+ enableReuseNotification: boolean,
215
+ ) {
216
+ const childCtx = ctx[cellIndex];
217
+ if (!childCtx) {
218
+ throw new Error('childCtx not found');
219
+ }
220
+ const r = componentAtChildCtx(list, listID, childCtx, operationID, enableReuseNotification);
221
+
222
+ /* v8 ignore start */
223
+ if (process.env['NODE_ENV'] === 'test') {
224
+ return r;
225
+ } else {
226
+ return typeof r === 'number' ? r : undefined;
227
+ }
228
+ /* v8 ignore end */
229
+ }
230
+
231
+ function componentAtIndexes(
160
232
  list: FiberElement,
161
233
  listID: number,
162
234
  cellIndexes: number[],
163
235
  operationIDs: number[],
164
236
  enableReuseNotification: boolean,
165
237
  asyncFlush: boolean,
166
- ) => {
167
- const uiSigns = cellIndexes.map((cellIndex, index) => {
168
- const operationID = operationIDs[index] ?? 0;
169
- return componentAtIndex(list, listID, cellIndex, operationID, enableReuseNotification, true, asyncFlush);
238
+ ) {
239
+ let hasUnready = false;
240
+ const p: Array<Promise<number> | number> = [];
241
+
242
+ cellIndexes.forEach((cellIndex, index) => {
243
+ const operationID = operationIDs[index]!;
244
+ const childCtx = ctx[cellIndex];
245
+ if (!childCtx) {
246
+ throw new Error('childCtx not found');
247
+ }
248
+
249
+ const u = componentAtChildCtx(list, listID, childCtx, operationID, enableReuseNotification, true, asyncFlush);
250
+ if (typeof u === 'number') {
251
+ // ready
252
+ } else {
253
+ hasUnready = true;
254
+ }
255
+ p.push(u);
170
256
  });
257
+
258
+ // We need __FlushElementTree twice:
259
+ // 1. The first time is sync, we flush the items that are ready, with unready items' uiSign as -1.
260
+ // 2. The second time is async, with all the uiSigns.
261
+ // NOTE: The `operationIDs` passed to __FlushElementTree must be the one passed in,
262
+ // not the one generated by any code here, to workaround a bug of Lynx Engine.
263
+ // So we CANNOT split the `operationIDs` into two parts: one for ready items, one for unready items.
264
+ if (hasUnready) {
265
+ void Promise.all(p).then((uiSigns) => {
266
+ __FlushElementTree(list, {
267
+ triggerLayout: true,
268
+ operationIDs,
269
+ elementIDs: uiSigns,
270
+ listID,
271
+ });
272
+ });
273
+ }
171
274
  __FlushElementTree(list, {
172
275
  triggerLayout: true,
173
- operationIDs: operationIDs,
174
- elementIDs: uiSigns,
276
+ operationIDs,
277
+ elementIDs: cellIndexes.map((_, index) => typeof p[index] === 'number' ? p[index] : -1),
175
278
  listID,
176
279
  });
177
- };
280
+ }
178
281
  return [componentAtIndex, componentAtIndexes] as const;
179
282
  }
180
283
 
@@ -12,7 +12,7 @@ import { __root, setRoot } from '../root.js';
12
12
  import { applyRefQueue } from '../snapshot/workletRef.js';
13
13
  import { SnapshotInstance, __page, setupPage } from '../snapshot.js';
14
14
  import { isEmptyObject } from '../utils.js';
15
- import { PerformanceTimingKeys, markTiming, setPipeline } from './performance.js';
15
+ import { markTiming, setPipeline } from './performance.js';
16
16
 
17
17
  function ssrEncode() {
18
18
  const { __opcodes } = __root;
@@ -116,13 +116,14 @@ function updatePage(data: Record<string, unknown> | undefined, options?: UpdateP
116
116
  Object.assign(lynx.__initData, data);
117
117
  }
118
118
 
119
+ const flushOptions = options ?? {};
119
120
  if (!isJSReady) {
120
121
  const oldRoot = __root;
121
122
  setRoot(new SnapshotInstance('root'));
122
123
  __root.__jsx = oldRoot.__jsx;
123
124
 
124
125
  setPipeline(options?.pipelineOptions);
125
- markTiming(PerformanceTimingKeys.updateDiffVdomStart);
126
+ markTiming('updateDiffVdomStart');
126
127
  {
127
128
  __pendingListUpdates.clear();
128
129
  renderMainThread();
@@ -138,14 +139,11 @@ function updatePage(data: Record<string, unknown> | undefined, options?: UpdateP
138
139
  __pendingListUpdates.flush();
139
140
  applyRefQueue();
140
141
  }
141
- markTiming(PerformanceTimingKeys.updateDiffVdomEnd);
142
+ flushOptions.triggerDataUpdated = true;
143
+ markTiming('updateDiffVdomEnd');
142
144
  }
143
145
 
144
- if (options) {
145
- __FlushElementTree(__page, options);
146
- } else {
147
- __FlushElementTree();
148
- }
146
+ __FlushElementTree(__page, flushOptions);
149
147
  }
150
148
 
151
149
  function updateGlobalProps(_data: any, options?: UpdatePageOption): void {