Skip to content

Commit d4cae97

Browse files
committed
fix: bridge indexed collection subclass aliases
1 parent 72f1af0 commit d4cae97

4 files changed

Lines changed: 177 additions & 6 deletions

File tree

HANDOFF.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,49 @@ During this thread `agent-device` was version `0.18.0`; the npm package is
110110

111111
## Current Verified State
112112

113+
Update from 2026-06-29 18:19 EDT:
114+
115+
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.
116+
- Draft PR #46 branch is still
117+
`codex/rn-module-fabric-turbomodule-worklets` and remains the RN module /
118+
Fabric / TurboModule / worklets branch split from `refactor`.
119+
- GitHub Actions run `28404253218` on `72f1af05` kept setup, dependency
120+
install, FFI boundary check, V8 download, libffi build, metadata generation,
121+
NativeScript build, CLI build, and macOS tests green.
122+
- The improved iOS diagnostics identified the current simulator hang:
123+
`ApiTests.js NSMutableArrayMethods`.
124+
- `ApiTests.js Appearance` passed on iOS in that run. No `44abcd` output was
125+
printed for `NSMutableArrayMethods`, so the stall is before the native
126+
callback's first `TNSLog`, likely during JS-backed `NSMutableArray`
127+
mutation/subscript dispatch.
128+
- Current runtime fix:
129+
- Native class extension install now synthesizes indexed collection aliases
130+
for JS-backed Objective-C subclasses: `objectAtIndex` also supplies
131+
`objectAtIndexedSubscript`, and `replaceObjectAtIndexWithObject` also
132+
supplies `setObjectAtIndexedSubscript`.
133+
- The write alias corrects Objective-C selector argument order by calling
134+
`replaceObjectAtIndexWithObject(index, anObject)` from
135+
`setObjectAtIndexedSubscript(anObject, index)`.
136+
- The alias preparation is applied to both `.extend(...)` and TypeScript
137+
`NativeClass` materialization.
138+
- Source coverage was added in
139+
`packages/react-native/test/runtime-indexed-collection-alias.test.js`.
140+
- Local verification:
141+
- `node packages/react-native/test/runtime-indexed-collection-alias.test.js`
142+
passed.
143+
- Runtime RN JS tests passed:
144+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
145+
- `git diff --check` passed.
146+
- `npm run check:ffi-boundaries` passed.
147+
- `npm run build:macos-cli` passed.
148+
- A focused local macOS `NSMutableArrayMethods` run was blocked before launch
149+
by the known local metadata-generator x86_64/libclang mismatch, so CI is
150+
still the authoritative iOS simulator signal.
151+
- Not done:
152+
- Commit/push the indexed collection alias fix and watch fresh PR CI for the
153+
iOS simulator `NSMutableArrayMethods` result.
154+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
155+
113156
Update from 2026-06-29 17:34 EDT:
114157

115158
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.

NativeScript/ffi/shared/bridge/Install.mm

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,50 @@ function findPrototypeDescriptor(className, property) {
237237
return undefined;
238238
}
239239
240+
function nativeExtensionMethodsWithIndexedCollectionAliases(methods) {
241+
if (methods == null || typeof methods !== 'object') {
242+
return methods;
243+
}
244+
245+
var needsObjectAtIndexedSubscript =
246+
Object.prototype.hasOwnProperty.call(methods, 'objectAtIndex') &&
247+
!Object.prototype.hasOwnProperty.call(methods, 'objectAtIndexedSubscript');
248+
var needsSetObjectAtIndexedSubscript =
249+
Object.prototype.hasOwnProperty.call(methods, 'replaceObjectAtIndexWithObject') &&
250+
!Object.prototype.hasOwnProperty.call(methods, 'setObjectAtIndexedSubscript');
251+
252+
if (!needsObjectAtIndexedSubscript && !needsSetObjectAtIndexedSubscript) {
253+
return methods;
254+
}
255+
256+
var prepared = Object.create(Object.getPrototypeOf(methods));
257+
Object.defineProperties(prepared, Object.getOwnPropertyDescriptors(methods));
258+
259+
if (needsObjectAtIndexedSubscript) {
260+
Object.defineProperty(prepared, 'objectAtIndexedSubscript', {
261+
configurable: true,
262+
enumerable: false,
263+
writable: true,
264+
value: function(index) {
265+
return this.objectAtIndex(index);
266+
}
267+
});
268+
}
269+
270+
if (needsSetObjectAtIndexedSubscript) {
271+
Object.defineProperty(prepared, 'setObjectAtIndexedSubscript', {
272+
configurable: true,
273+
enumerable: false,
274+
writable: true,
275+
value: function(anObject, index) {
276+
return this.replaceObjectAtIndexWithObject(index, anObject);
277+
}
278+
});
279+
}
280+
281+
return prepared;
282+
}
283+
240284
Object.defineProperty(globalThis, '__nativeScriptCreateNativeApiIterator', {
241285
configurable: false,
242286
enumerable: false,
@@ -708,6 +752,7 @@ function rememberInstanceClass(instance) {
708752
if (methods == null || typeof methods !== 'object') {
709753
throw new Error('extend() first parameter must be an object');
710754
}
755+
var extensionMethods = nativeExtensionMethodsWithIndexedCollectionAliases(methods);
711756
var extendOptions = options || {};
712757
if (typeof Symbol === 'function' &&
713758
Object.prototype.hasOwnProperty.call(methods, Symbol.iterator)) {
@@ -719,18 +764,18 @@ function rememberInstanceClass(instance) {
719764
extendOptions.__hasIterator = true;
720765
}
721766
}
722-
var extendedNativeClass = api.__extendClass(nativeClass, methods, extendOptions);
767+
var extendedNativeClass = api.__extendClass(nativeClass, extensionMethods, extendOptions);
723768
var extended = wrapNativeClass(extendedNativeClass);
724769
try {
725770
Object.setPrototypeOf(extended, wrapper || constructable);
726771
} catch (_) {
727772
}
728773
var extendedPrototype = Object.create(constructable.prototype || null);
729774
try {
730-
Object.defineProperties(extendedPrototype, Object.getOwnPropertyDescriptors(methods));
775+
Object.defineProperties(extendedPrototype, Object.getOwnPropertyDescriptors(extensionMethods));
731776
} catch (_) {
732-
Object.keys(methods).forEach(function(key) {
733-
extendedPrototype[key] = methods[key];
777+
Object.keys(extensionMethods).forEach(function(key) {
778+
extendedPrototype[key] = extensionMethods[key];
734779
});
735780
}
736781
try {
@@ -1361,7 +1406,9 @@ function materializeTypeScriptNativeClass(constructor) {
13611406
}
13621407
13631408
var nativeBase = nativeClassLikeHandle(baseWrapper);
1364-
var nativeClass = api.__extendClass(nativeBase, constructor.prototype || {}, options);
1409+
var extensionMethods =
1410+
nativeExtensionMethodsWithIndexedCollectionAliases(constructor.prototype || {});
1411+
var nativeClass = api.__extendClass(nativeBase, extensionMethods, options);
13651412
var wrapper = wrapNativeClass(nativeClass);
13661413
state.wrapper = wrapper;
13671414
@@ -1370,7 +1417,7 @@ function materializeTypeScriptNativeClass(constructor) {
13701417
} catch (_) {
13711418
}
13721419
try {
1373-
api.__rememberClassWrapper(nativeClass, constructor, constructor.prototype || {});
1420+
api.__rememberClassWrapper(nativeClass, constructor, extensionMethods);
13741421
} catch (_) {
13751422
}
13761423
return wrapper;

PROGRESS.md

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

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

20+
### 2026-06-29 18:19 EDT - indexed collection subclass aliases
21+
22+
- Goal:
23+
- Keep PR #46 on the RN module / generic runtime primitive branch and stay
24+
simulator-only. No physical devices were used.
25+
- CI finding:
26+
- GitHub Actions run `28404253218` on `72f1af05` kept setup, dependency
27+
install, FFI boundary check, V8 download, libffi build, metadata
28+
generation, NativeScript build, CLI build, and macOS tests green.
29+
- The improved iOS diagnostics showed `ApiTests.js Appearance` passed, then
30+
the iOS simulator hung in `ApiTests.js NSMutableArrayMethods`.
31+
- No `44abcd` output was printed, so the stall happens before the native
32+
callback's first `TNSLog`, likely during JS-backed `NSMutableArray`
33+
mutation/subscript dispatch.
34+
- Changes:
35+
- Native class extension install now synthesizes `objectAtIndexedSubscript`
36+
from `objectAtIndex` and `setObjectAtIndexedSubscript` from
37+
`replaceObjectAtIndexWithObject`.
38+
- The write alias corrects Objective-C selector argument order by calling
39+
`replaceObjectAtIndexWithObject(index, anObject)` from
40+
`setObjectAtIndexedSubscript(anObject, index)`.
41+
- The alias preparation now applies to both `.extend(...)` and TypeScript
42+
`NativeClass` materialization.
43+
- Added a source guard for the indexed collection alias behavior.
44+
- Verification:
45+
- `node packages/react-native/test/runtime-indexed-collection-alias.test.js`
46+
passed.
47+
- Runtime RN JS tests passed:
48+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
49+
- `git diff --check` passed.
50+
- `npm run check:ffi-boundaries` passed.
51+
- `npm run build:macos-cli` passed.
52+
- Focused local macOS `NSMutableArrayMethods` testing was blocked before
53+
launch by the known metadata-generator x86_64/libclang mismatch.
54+
- Still next:
55+
- Commit/push this indexed collection alias fix and watch fresh PR CI for
56+
the iOS simulator `NSMutableArrayMethods` result.
57+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
58+
2059
### 2026-06-29 17:34 EDT - iOS simulator hang diagnostics
2160

2261
- Goal:
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const assert = require("assert");
2+
const fs = require("fs");
3+
const path = require("path");
4+
5+
const repoRoot = path.resolve(__dirname, "../../..");
6+
7+
for (const relativePath of [
8+
"NativeScript/ffi/shared/bridge/Install.mm",
9+
"packages/react-native/native-api/ffi/shared/bridge/Install.mm",
10+
]) {
11+
const source = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
12+
13+
assert(
14+
source.includes("function nativeExtensionMethodsWithIndexedCollectionAliases(methods)"),
15+
`${relativePath}: native class extension should prepare indexed collection method aliases`,
16+
);
17+
assert(
18+
source.includes("needsObjectAtIndexedSubscript") &&
19+
source.includes("needsSetObjectAtIndexedSubscript"),
20+
`${relativePath}: indexed collection aliases should cover read and write native subscript selectors`,
21+
);
22+
assert(
23+
source.includes("return this.objectAtIndex(index);") &&
24+
source.includes("return this.replaceObjectAtIndexWithObject(index, anObject);"),
25+
`${relativePath}: synthesized subscript aliases should delegate to the JS primitive methods with native argument order corrected`,
26+
);
27+
assert(
28+
source.includes("var extensionMethods = nativeExtensionMethodsWithIndexedCollectionAliases(methods);") &&
29+
source.includes("api.__extendClass(nativeClass, extensionMethods, extendOptions)") &&
30+
source.includes("Object.getOwnPropertyDescriptors(extensionMethods)") &&
31+
source.includes("Object.keys(extensionMethods)"),
32+
`${relativePath}: NativeClass.extend should register and expose the prepared indexed collection method set`,
33+
);
34+
assert(
35+
source.includes("nativeExtensionMethodsWithIndexedCollectionAliases(constructor.prototype || {})") &&
36+
source.includes("api.__extendClass(nativeBase, extensionMethods, options)") &&
37+
source.includes("api.__rememberClassWrapper(nativeClass, constructor, extensionMethods)"),
38+
`${relativePath}: TypeScript native class materialization should use the same prepared method set`,
39+
);
40+
}
41+
42+
console.log("runtime indexed collection alias tests passed");

0 commit comments

Comments
 (0)