Skip to content

Commit 1723f06

Browse files
committed
fix: enumerate JS indexed collection subclasses
1 parent 7d7e9d3 commit 1723f06

4 files changed

Lines changed: 140 additions & 17 deletions

File tree

NativeScript/ffi/shared/bridge/Install.mm

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -242,14 +242,26 @@ function nativeExtensionMethodsWithIndexedCollectionAliases(methods) {
242242
return methods;
243243
}
244244
245+
var hasObjectAtIndex =
246+
Object.prototype.hasOwnProperty.call(methods, 'objectAtIndex');
247+
var hasCount =
248+
Object.prototype.hasOwnProperty.call(methods, 'count');
249+
var hasSymbolIterator =
250+
typeof Symbol === 'function' && Symbol.iterator &&
251+
Object.prototype.hasOwnProperty.call(methods, Symbol.iterator);
245252
var needsObjectAtIndexedSubscript =
246-
Object.prototype.hasOwnProperty.call(methods, 'objectAtIndex') &&
253+
hasObjectAtIndex &&
247254
!Object.prototype.hasOwnProperty.call(methods, 'objectAtIndexedSubscript');
248255
var needsSetObjectAtIndexedSubscript =
249256
Object.prototype.hasOwnProperty.call(methods, 'replaceObjectAtIndexWithObject') &&
250257
!Object.prototype.hasOwnProperty.call(methods, 'setObjectAtIndexedSubscript');
258+
var needsIndexedCollectionIterator =
259+
typeof Symbol === 'function' && Symbol.iterator &&
260+
hasObjectAtIndex && hasCount && !hasSymbolIterator;
251261
252-
if (!needsObjectAtIndexedSubscript && !needsSetObjectAtIndexedSubscript) {
262+
if (!needsObjectAtIndexedSubscript &&
263+
!needsSetObjectAtIndexedSubscript &&
264+
!needsIndexedCollectionIterator) {
253265
return methods;
254266
}
255267
@@ -278,9 +290,57 @@ function nativeExtensionMethodsWithIndexedCollectionAliases(methods) {
278290
});
279291
}
280292
293+
if (needsIndexedCollectionIterator) {
294+
Object.defineProperty(prepared, Symbol.iterator, {
295+
configurable: true,
296+
enumerable: false,
297+
writable: true,
298+
value: function() {
299+
var receiver = this;
300+
var index = 0;
301+
return {
302+
next: function() {
303+
var countValue = receiver.count;
304+
var count = typeof countValue === 'function'
305+
? countValue.call(receiver)
306+
: countValue;
307+
if (!(index < count)) {
308+
return { done: true };
309+
}
310+
return {
311+
value: receiver.objectAtIndex(index++),
312+
done: false
313+
};
314+
}
315+
};
316+
}
317+
});
318+
}
319+
281320
return prepared;
282321
}
283322
323+
function nativeExtensionMethodsHaveIterator(methods) {
324+
return typeof Symbol === 'function' && Symbol.iterator &&
325+
methods != null && typeof methods === 'object' &&
326+
Object.prototype.hasOwnProperty.call(methods, Symbol.iterator);
327+
}
328+
329+
function nativeExtensionOptionsWithIterator(options, methods) {
330+
var extendOptions = options || {};
331+
if (!nativeExtensionMethodsHaveIterator(methods)) {
332+
return extendOptions;
333+
}
334+
try {
335+
return Object.assign({}, extendOptions, {
336+
__hasIterator: true
337+
});
338+
} catch (_) {
339+
extendOptions.__hasIterator = true;
340+
return extendOptions;
341+
}
342+
}
343+
284344
Object.defineProperty(globalThis, '__nativeScriptCreateNativeApiIterator', {
285345
configurable: false,
286346
enumerable: false,
@@ -753,17 +813,8 @@ function rememberInstanceClass(instance) {
753813
throw new Error('extend() first parameter must be an object');
754814
}
755815
var extensionMethods = nativeExtensionMethodsWithIndexedCollectionAliases(methods);
756-
var extendOptions = options || {};
757-
if (typeof Symbol === 'function' &&
758-
Object.prototype.hasOwnProperty.call(methods, Symbol.iterator)) {
759-
try {
760-
extendOptions = Object.assign({}, extendOptions, {
761-
__hasIterator: true
762-
});
763-
} catch (_) {
764-
extendOptions.__hasIterator = true;
765-
}
766-
}
816+
var extendOptions =
817+
nativeExtensionOptionsWithIterator(options, extensionMethods);
767818
var extendedNativeClass = api.__extendClass(nativeClass, extensionMethods, extendOptions);
768819
var extended = wrapNativeClass(extendedNativeClass);
769820
try {
@@ -1405,10 +1456,11 @@ function materializeTypeScriptNativeClass(constructor) {
14051456
options.exposedMethods = constructor.ObjCExposedMethods;
14061457
}
14071458
1408-
var nativeBase = nativeClassLikeHandle(baseWrapper);
1409-
var extensionMethods =
1410-
nativeExtensionMethodsWithIndexedCollectionAliases(constructor.prototype || {});
1411-
var nativeClass = api.__extendClass(nativeBase, extensionMethods, options);
1459+
var nativeBase = nativeClassLikeHandle(baseWrapper);
1460+
var extensionMethods =
1461+
nativeExtensionMethodsWithIndexedCollectionAliases(constructor.prototype || {});
1462+
options = nativeExtensionOptionsWithIterator(options, extensionMethods);
1463+
var nativeClass = api.__extendClass(nativeBase, extensionMethods, options);
14121464
var wrapper = wrapNativeClass(nativeClass);
14131465
state.wrapper = wrapper;
14141466

PROGRESS.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,48 @@ TypeScript/UI worklets.
1717

1818
## Latest Update - 2026-06-29
1919

20+
### 2026-06-29 22:20 EDT - indexed collection fast enumeration for JS subclasses
21+
22+
- CI finding:
23+
- GitHub Actions run `28414381388` on `7d7e9d3f` kept setup,
24+
dependency install, FFI boundary check, V8 download, libffi build,
25+
metadata generation, NativeScript build, CLI build, and macOS tests green.
26+
- The previous macOS `MethodCallsTests` and `ApiTests.js
27+
NSMutableArrayMethods` failures were fixed.
28+
- iOS simulator still timed out in `ApiTests.js NSMutableArrayMethods`.
29+
Verbose-spec logs showed `MethodCallsTests`, `ApiTests.js Appearance`,
30+
and the immediately preceding `ApiTests` specs passed, then the run hung
31+
after `SPEC START ... ApiTests.js NSMutableArrayMethods` with no completed
32+
JUnit file.
33+
- Root cause refinement:
34+
- The remaining iOS-only hang is consistent with native fast enumeration of
35+
a JS-backed `NSMutableArray` subclass relying on Foundation's inherited
36+
`NSArray` enumeration behavior instead of a bridge-owned JS primitive path.
37+
- The subclass implements the indexed collection contract (`count` and
38+
`objectAtIndex`) but does not explicitly provide `Symbol.iterator`, so the
39+
existing NativeScript fast-enumeration bridge was not installed.
40+
- Changes:
41+
- JS native-class extension preparation now synthesizes a non-enumerable
42+
`Symbol.iterator` for indexed collection subclasses that provide
43+
`count` and `objectAtIndex`.
44+
- Both `.extend(...)` and TypeScript `NativeClass` materialization now enable
45+
the existing native fast-enumeration bridge from the prepared method set,
46+
including synthesized iterators.
47+
- The existing indexed subscript aliases remain in place.
48+
- Verification:
49+
- `node packages/react-native/test/runtime-indexed-collection-alias.test.js`
50+
passed.
51+
- Runtime RN JS tests passed:
52+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
53+
- `git diff --check` passed.
54+
- `npm run check:ffi-boundaries` passed.
55+
- `npm run build:macos-cli` passed.
56+
- Still next:
57+
- Commit/push this indexed collection fast-enumeration fix and watch fresh PR
58+
CI for the iOS simulator `NSMutableArrayMethods` result.
59+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep
60+
against original RNS.
61+
2062
### 2026-06-29 21:15 EDT - initializer self-return receiver preservation
2163

2264
- Goal:

RN_API.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ UIKit-backed React Native libraries in TypeScript/UI worklets.
6868
- Prototype setters remain authoritative and must not be shadowed by the
6969
expando fallback.
7070

71+
## 2026-06-29 JS Indexed Collection Fast Enumeration
72+
73+
- No new public NativeScript React Native API was added.
74+
- Generic native bridge behavior was clarified for JS/TypeScript native
75+
subclasses:
76+
- JS-backed indexed collection subclasses that provide `count` and
77+
`objectAtIndex` should expose a bridge-owned iterator when they do not
78+
define `Symbol.iterator` themselves.
79+
- The existing native fast-enumeration bridge should be installed from the
80+
prepared method set for both `.extend(...)` and TypeScript `NativeClass`
81+
materialization, including synthesized indexed-collection iterators.
82+
- Native `for...in`/`NSFastEnumeration` over JS-backed NSArray/NSMutableArray
83+
subclasses should enumerate through the JS indexed primitive contract
84+
rather than relying on platform-specific inherited Foundation behavior.
85+
7186
## 2026-06-29 Simulator Latency Comparison
7287

7388
- No new public NativeScript React Native API was added.

packages/react-native/test/runtime-indexed-collection-alias.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,29 @@ for (const relativePath of [
2424
source.includes("return this.replaceObjectAtIndexWithObject(index, anObject);"),
2525
`${relativePath}: synthesized subscript aliases should delegate to the JS primitive methods with native argument order corrected`,
2626
);
27+
assert(
28+
source.includes("needsIndexedCollectionIterator") &&
29+
source.includes("Object.defineProperty(prepared, Symbol.iterator") &&
30+
source.includes("value: receiver.objectAtIndex(index++)"),
31+
`${relativePath}: JS-backed indexed collection subclasses should synthesize Symbol.iterator from count/objectAtIndex`,
32+
);
33+
assert(
34+
source.includes("function nativeExtensionOptionsWithIterator(options, methods)") &&
35+
source.includes("nativeExtensionMethodsHaveIterator(methods)") &&
36+
source.includes("__hasIterator: true"),
37+
`${relativePath}: prepared indexed collection iterators should enable the native fast-enumeration bridge`,
38+
);
2739
assert(
2840
source.includes("var extensionMethods = nativeExtensionMethodsWithIndexedCollectionAliases(methods);") &&
41+
source.includes("nativeExtensionOptionsWithIterator(options, extensionMethods)") &&
2942
source.includes("api.__extendClass(nativeClass, extensionMethods, extendOptions)") &&
3043
source.includes("Object.getOwnPropertyDescriptors(extensionMethods)") &&
3144
source.includes("Object.keys(extensionMethods)"),
3245
`${relativePath}: NativeClass.extend should register and expose the prepared indexed collection method set`,
3346
);
3447
assert(
3548
source.includes("nativeExtensionMethodsWithIndexedCollectionAliases(constructor.prototype || {})") &&
49+
source.includes("options = nativeExtensionOptionsWithIterator(options, extensionMethods)") &&
3650
source.includes("api.__extendClass(nativeBase, extensionMethods, options)") &&
3751
source.includes("api.__rememberClassWrapper(nativeClass, constructor, extensionMethods)"),
3852
`${relativePath}: TypeScript native class materialization should use the same prepared method set`,

0 commit comments

Comments
 (0)