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
2 changes: 2 additions & 0 deletions packages/eslint-config-universe/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Disabled `import/no-named-as-default` in both core and TypeScript presets ([#45088](https://github.com/expo/expo/pull/45088) by [@hassankhan](https://github.com/hassankhan))

### 🐛 Bug fixes

### 💡 Others
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-config-universe/flat/shared/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ module.exports = defineConfig([
],

'import/default': 'off',
'import/no-named-as-default': 'off',
'import/export': 'error',
'import/first': 'warn',

Expand Down
3 changes: 3 additions & 0 deletions packages/eslint-config-universe/flat/shared/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ module.exports = defineConfig([

// TODO (Kadi): Enable this. Disabling for now because import/recommended adds it, but we didn't use to have it enabled
'import/no-unresolved': 'off',

// NOTE(@hassankhan): This is disabled in `core`, but `eslint-plugin-import`'s recommended rules re-enable it
'import/no-named-as-default': 'off',
},
},
]);
1 change: 1 addition & 0 deletions packages/eslint-config-universe/shared/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ module.exports = {
yoda: ['warn', 'never', { exceptRange: true }],

'import/default': 'off',
'import/no-named-as-default': 'off',
'import/export': 'error',
'import/first': 'warn',
'import/namespace': ['error', { allowComputed: true }],
Expand Down
3 changes: 3 additions & 0 deletions packages/eslint-config-universe/shared/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ module.exports = {
// The typescript-eslint FAQ recommends turning off "no-undef" in favor of letting tsc check for
// undefined variables, including types
'no-undef': 'off',

// NOTE(@hassankhan): This is disabled in `core`, but `eslint-plugin-import`'s recommended rules re-enable it
'import/no-named-as-default': 'off',
},
settings: {
'import/extensions': allExtensions,
Expand Down
1 change: 1 addition & 0 deletions packages/expo-modules-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

### 🐛 Bug fixes

- [iOS] Fix random crash when concurrent access of the same `Record` on `Field.options`. The set is now guarded by a per-field lock and accessed through a single `withOptions` API. ([#45072](https://github.com/expo/expo/pull/45072) by [@nishan](https://github.com/intergalacticspacehighway))
- [iOS] Fixed converting `Enumerable` function results to JS values. ([#45168](https://github.com/expo/expo/pull/45168) by [@tsapeta](https://github.com/tsapeta))
- [iOS] Fixed prebuild dependencies for `ExpoModulesJSI` ([#45124](https://github.com/expo/expo/pull/45124) by [@chrfalch](https://github.com/chrfalch))
- [iOS] Add async/await overload for StaticAsyncFunction ([#44471](https://github.com/expo/expo/pull/44471) by [@Wenszel](https://github.com/Wenszel))
Expand Down
4 changes: 3 additions & 1 deletion packages/expo-modules-core/ios/Core/Records/AnyField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public protocol AnyField {
*/
internal protocol AnyFieldInternal: AnyField {
var key: String? { get }
var options: Set<FieldOption> { get set }
var fieldType: AnyDynamicType { get }

/**
Expand All @@ -22,6 +21,9 @@ internal protocol AnyFieldInternal: AnyField {
*/
var isRequired: Bool { get }

// Read‑modify‑write inside `body` is atomic.
func withOptions<T>(_ body: (inout Set<FieldOption>) -> T) -> T

func set(_ newValue: Any?, appContext: AppContext) throws

@JavaScriptActor
Expand Down
20 changes: 14 additions & 6 deletions packages/expo-modules-core/ios/Core/Records/Field.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,31 @@ public final class Field<Type: AnyArgument>: AnyFieldInternal, @unchecked Sendab
Sadly, property wrappers don't receive properties' label, so we must wait until it's assigned by `Record`.
*/
internal var key: String? {
return options.first { $0.rawValue == FieldOption.keyed("").rawValue }?.key
return withOptions { options in
options.first { $0.rawValue == FieldOption.keyed("").rawValue }?.key
}
}

/**
Additional options of the field, such is if providing the value is required (`FieldOption.required`).
Locked because `fieldsOf` lazily writes the keyed option while other threads may read. Access only via `withOptions`.
*/
internal var options: Set<FieldOption> = Set()
private let _options: Mutex<Set<FieldOption>>

internal func withOptions<T>(_ body: (inout Set<FieldOption>) -> T) -> T {
return _options.withLock { body(&$0) }
}

internal var isRequired: Bool {
options.contains(.required)
withOptions { $0.contains(.required) }
}

/**
Initializes the field with given value and customized key.
*/
public init(wrappedValue: Type, _ options: FieldOption...) {
self.wrappedValue = wrappedValue
self.options = Set(options)
self._options = Mutex(Set(options))
}

/**
Expand All @@ -42,7 +49,7 @@ public final class Field<Type: AnyArgument>: AnyFieldInternal, @unchecked Sendab
*/
public init(wrappedValue: Type, _ options: [FieldOption]) {
self.wrappedValue = wrappedValue
self.options = Set(options)
self._options = Mutex(Set(options))
}

/**
Expand All @@ -51,11 +58,12 @@ public final class Field<Type: AnyArgument>: AnyFieldInternal, @unchecked Sendab
*/
public init(wrappedValue: Type = nil) where Type: ExpressibleByNilLiteral {
self.wrappedValue = wrappedValue
self._options = Mutex(Set())
}

public init(wrappedValue: Type = nil, _ options: FieldOption...) where Type: ExpressibleByNilLiteral {
self.wrappedValue = wrappedValue
self.options = Set(options)
self._options = Mutex(Set(options))
}

/**
Expand Down
9 changes: 7 additions & 2 deletions packages/expo-modules-core/ios/Core/Records/Record.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,15 @@ internal func allMirrorChildren(_ mirror: Mirror) -> [Mirror.Child] {
internal func fieldsOf(_ record: Record) -> [AnyFieldInternal] {
let mirror = Mirror(reflecting: record)
return allMirrorChildren(mirror).compactMap { (label: String?, value: Any) in
guard var field = value as? AnyFieldInternal, let key = field.key ?? convertLabelToKey(label) else {
guard let field = value as? AnyFieldInternal, let key = field.key ?? convertLabelToKey(label) else {
return nil
}
field.options = field.options.union([.keyed(key)])
field.withOptions { options in
let alreadyKeyed = options.contains { $0.rawValue == FieldOption.keyed("").rawValue }
if !alreadyKeyed {
options.insert(.keyed(key))
}
}
return field
}
}
Expand Down
33 changes: 33 additions & 0 deletions packages/expo-modules-core/ios/Tests/RecordSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,38 @@ class RecordSpec: ExpoSpec {
expect(error).to(beAKindOf(FieldInvalidTypeException.self))
})
}

it("serializes concurrently on a shared record without crashing") {
struct StressRecord: Record {
@Field var a: String? = nil
@Field var b: String? = nil
@Field var c: String? = nil
}

let record = StressRecord(a: "a", b: "b", c: "c")
let workers = 16
let iterations = 100
let group = DispatchGroup()
let startGate = DispatchSemaphore(value: 0)

for _ in 0..<workers {
group.enter()
DispatchQueue.global(qos: .userInitiated).async {
startGate.wait()
for _ in 0..<iterations {
_ = record.toDictionary()
}
group.leave()
}
}
// Release every worker so they collide on the first reflection.
for _ in 0..<workers { startGate.signal() }
group.wait()

let finalDict = record.toDictionary()
expect(finalDict.keys.count).to(equal(3))
expect(finalDict["a"] as? String).to(equal("a"))
expect(finalDict["c"] as? String).to(equal("c"))
}
}
}
Loading
Loading