Skip to content

Commit 9c03346

Browse files
committed
feat: move store plan to ArchitecturePlan
1 parent 4293793 commit 9c03346

1 file changed

Lines changed: 79 additions & 105 deletions

File tree

Lines changed: 79 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Brownfield Store
1+
# Brownie Architecture
22

3-
Shared state management between React Native and Native apps (iOS/Android).
3+
Shared state management between React Native and Native apps (iOS).
44

5-
## Architecture
5+
## High-Level Architecture
66

77
```
88
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
@@ -23,18 +23,14 @@ Shared state management between React Native and Native apps (iOS/Android).
2323
│ ▼
2424
┌───────────────────┐ ┌───────────────────────┐
2525
│ JS/React Hook │ │ Native Store │
26-
│ useBrownfield() │ │ (Swift/Kotlin)
26+
│ useBrownfield() │ │ (Swift)
2727
└───────────────────┘ └───────────────────────┘
2828
```
2929

3030
## CLI
3131

3232
The `brownie` CLI generates native store types from TypeScript schema.
3333

34-
### Installation
35-
36-
CLI is included in `@callstack/brownie` package.
37-
3834
### Usage
3935

4036
```bash
@@ -217,7 +213,7 @@ Output goes to `lib/scripts/` and is exposed via `bin` field in `package.json`.
217213
│ │ BrownieHostObject (jsi::HostObject) │ │
218214
│ │ - get(prop) -> converts folly::dynamic to jsi::Value │ │
219215
│ │ - set(prop, val) -> converts jsi::Value to folly::dynamic│ │
220-
│ │ - unbox() -> full snapshot as jsi::Object │ │
216+
│ │ - getPropertyNames() -> list of property names │ │
221217
│ └───────────────────────────────────────────────────────────┘ │
222218
└─────────────────────────────────────────────────────────────────┘
223219
@@ -269,8 +265,9 @@ packages/brownie/
269265

270266
- `get(prop)` - Read property, converts `folly::dynamic``jsi::Value`
271267
- `set(prop, value)` - Write property, converts `jsi::Value``folly::dynamic`
272-
- `unbox()` - Returns full state snapshot
273-
- Custom `dynamicToJSI`/`jsiToDynamic` converters
268+
- `unbox()` - Returns full state snapshot as JSI object
269+
- `getPropertyNames()` - Returns list of all property names
270+
- Uses built-in `jsi::valueFromDynamic`/`jsi::dynamicFromValue` converters
274271

275272
### iOS Bridge
276273

@@ -291,6 +288,7 @@ packages/brownie/
291288
- `init(_ initialState, key:)` - Create store, register with C++, push initial state
292289
- `set(_:)` - Update state with closure
293290
- `set(_:to:)` - Update via keypath
291+
- `get(_:)` - Get property via keypath
294292
- Observes `BrownieStoreUpdated` notification, rebuilds typed state from C++ snapshot
295293
- `deinit` removes notification observer
296294

@@ -302,9 +300,8 @@ packages/brownie/
302300

303301
**@UseStore** - SwiftUI property wrapper:
304302

305-
- `init(_ keyPath, key:)` - Access specific property
306-
- `wrappedValue` - Current value
307-
- `projectedValue` - Access to full store for mutations
303+
- Uses `BrownieStoreProtocol` to automatically derive store key via `storeName`
304+
- `wrappedValue` - Access to full `Store<State>`
308305

309306
## JS API
310307

@@ -346,127 +343,104 @@ Internal `Map<string, StoreCache>` holds:
346343
- Current snapshot
347344
- Registered listeners
348345
349-
Snapshots refresh on `BrownieStoreUpdated` native event.
346+
Snapshots refresh on native store change callback.
350347
351-
## Auto-generation Hooks
348+
## Data Transfer & Memory Model
352349
353-
### iOS (Podfile)
350+
### Overview
354351
355-
```ruby
356-
pre_install do |installer|
357-
system("npx", "brownie", "codegen", "-p", "swift")
358-
end
359-
```
360-
361-
### Android (build.gradle.kts)
352+
Data flows through multiple layers with **full copies** at each boundary crossing. There is no shared memory or zero-copy optimization - each layer maintains its own representation.
362353
363-
```kotlin
364-
tasks.register("generateBrownfieldStore") {
365-
exec { commandLine("npx", "brownie", "codegen", "-p", "kotlin") }
366-
}
367-
preBuild.dependsOn("generateBrownfieldStore")
354+
```
355+
┌─────────────┐ copy ┌─────────────┐ copy ┌─────────────┐
356+
JS Object │ ─────────▶ │ folly:: │ ─────────▶ │ NSDictionary
357+
│ (jsi) │ ◀───────── │ dynamic │ ◀───────── │ / Swift
358+
└─────────────┘ copy └─────────────┘ copy └─────────────┘
368359
```
369360
370-
## TODO
371-
372-
### Codegen
373-
374-
- [x] Support multiple stores in package.json config (array of store configs)
375-
- [x] Auto-discover stores from `*.brownie.ts` files (no manual config needed)
376-
- [x] Add CLI tests
377-
- [x] Generate store keys enum/constants for type safety between JS and Native
378-
379-
### Native Runtime
380-
381-
- [x] Improve iOS runtime code (C++ core with folly::dynamic)
382-
- [x] Extract C++ JSI layer to be shared across Android and iOS
383-
- [ ] Android native runtime implementation (Store, StoreManager, JSI bridge)
384-
- [ ] Keypath→string codegen for optimized single-key updates (future)
361+
Note: In the future we may explore optimizations like shared memory or zero-copy techniques, but for now simplicity and correctness are prioritized.
385362
386-
### JS Runtime
363+
### C++ Layer (Source of Truth)
387364
388-
- [x] useState-like hook API (`useBrownieStore` returns `[state, setState]`)
389-
- [x] setState supports callback pattern `(prev) => partial`
390-
- [ ] Optimize re-renders on the JS side (selector support)
391-
- [ ] Zustand integration
365+
**Writes use move semantics where possible:**
392366
393-
## Re-render Optimization Plan
367+
- `BrownieStore::set(key, value)` - value is moved into state: `state_[key] = std::move(value)`
368+
- `BrownieStore::setState(state)` - entire state is moved: `state_ = std::move(state)`
369+
- `BrownieStoreManager::registerStore(key, store)` - shared_ptr is moved into map
394370
395-
### Problem
371+
**Reads always copy:**
396372
397-
Both JS and Swift currently re-render all subscribers when any property changes.
373+
- `BrownieStore::get(key)` - returns copy of `folly::dynamic` value
374+
- `BrownieStore::getSnapshot()` - returns copy of entire state
398375
399-
**JS**: `useBrownieStore` returns full state → any change triggers all consumers
400-
**Swift**: `@Published state` replaced wholesale → all `@UseStore` views re-render
376+
### JSI Boundary (JS ↔ C++)
401377
402-
### JavaScript Solution: Selector Hook
378+
**JS → C++ (writes):**
403379
404-
Add `useBrownieStoreSelector` that only re-renders when selected value changes:
380+
1. `jsi::dynamicFromValue(rt, value)` creates new `folly::dynamic` from JS value
381+
2. Value is then moved into C++ store
405382
406-
```ts
407-
function useBrownieStoreSelector<K extends keyof BrownieStores, T>(
408-
key: K,
409-
selector: (state: BrownieStores[K]) => T,
410-
isEqual?: (a: T, b: T) => boolean
411-
): T;
412-
```
383+
**C++ → JS (reads):**
413384
414-
**Usage:**
385+
1. `folly::dynamic` is copied from store
386+
2. `jsi::valueFromDynamic(rt, dynamic)` creates new JS value
415387
416-
```ts
417-
const counter = useBrownieStoreSelector('BrownfieldStore', (s) => s.counter);
418-
const user = useBrownieStoreSelector('BrownfieldStore', (s) => s.user);
419-
```
388+
### ObjC++ Bridge (C++ ↔ Swift)
420389
421-
**Implementation:**
390+
**Swift → C++ (writes):**
422391
423-
1. Store previous selected value in `useRef`
424-
2. On store change, run selector on new snapshot
425-
3. Compare with previous using `isEqual` (default: `Object.is`)
426-
4. Only trigger re-render if different
392+
1. Swift encodes `Codable` state to JSON via `JSONEncoder`
393+
2. JSON deserialized to `NSDictionary`
394+
3. `convertIdToFollyDynamic(dict)` creates `folly::dynamic` copy
395+
4. Value moved into C++ store
427396
428-
### Swift Solution: Selector Property Wrapper
397+
**C++ → Swift (reads):**
429398
430-
Add `@UseStoreSelector` that subscribes to specific keypath:
399+
1. `folly::dynamic` copied from store via `getSnapshot()`
400+
2. `convertFollyDynamicToId(dynamic)` creates `NSDictionary` copy
401+
3. `JSONSerialization.data(withJSONObject:)` serializes to JSON
402+
4. `JSONDecoder` deserializes to typed Swift struct (another copy)
431403
432-
```swift
433-
@propertyWrapper
434-
struct UseStoreSelector<State: BrownieStoreProtocol, Value: Equatable>: DynamicProperty {
435-
let keyPath: KeyPath<State, Value>
436-
@State private var value: Value
404+
### Performance Implications
437405
438-
init(_ keyPath: KeyPath<State, Value>)
439-
var wrappedValue: Value { get }
440-
}
441-
```
406+
| Operation | Copies | Notes |
407+
| ------------------------ | ------ | --------------------------------- |
408+
| JS read single property | 2 | dynamic copy + JSI conversion |
409+
| JS read snapshot (unbox) | 2 | dynamic copy + JSI conversion |
410+
| JS write single property | 2 | JSI→dynamic + move into store |
411+
| Swift read snapshot | 4 | dynamic→NSDictionary→JSON→Codable |
412+
| Swift write full state | 4 | Codable→JSON→NSDictionary→dynamic |
442413
443-
**Usage:**
414+
### Thread Safety
444415
445-
```swift
446-
@UseStoreSelector(\.counter) var counter: Int
447-
@UseStoreSelector(\.user) var user: String
448-
```
416+
- C++ store protected by `std::mutex` on all operations
417+
- Swift `StoreManager` uses `NSLock` for registry access
418+
- Change notifications dispatched to main queue via `dispatch_async`
449419
450-
**Implementation:**
420+
### Future Optimization Opportunities
451421
452-
1. Use `@State` instead of `@StateObject` (no `ObservableObject` dependency)
453-
2. Listen to `BrownieStoreUpdated` notification
454-
3. Extract value via keyPath from C++ snapshot
455-
4. Only update `@State` if value differs (Equatable check)
422+
The ObjC++ bridge layer has the most room for optimization:
456423
457-
### Tasks
424+
- **Skip JSON serialization** - Instead of `folly::dynamicNSDictionaryJSONCodable`, expose individual property accessors from C++ directly to Swift. Eliminates 2 intermediate copies.
425+
- **Single-property sync** - Currently `pushStateToCxx()` serializes entire state. Could track dirty properties and only sync changed values.
426+
- **Lazy Swift state rebuild** - Defer `Codable` struct reconstruction until property actually accessed.
427+
- **Direct C++ ↔ Swift interop** - Swift 5.9+ has experimental C++ interop that could bypass ObjC++ bridge entirely.
458428
459-
- [ ] JS: Implement `useBrownieStoreSelector` hook
460-
- [ ] JS: Add `isEqual` option for custom comparison (objects/arrays)
461-
- [ ] Swift: Implement `@UseStoreSelector` property wrapper
462-
- [ ] Swift: Consider `@Observable` macro for iOS 17+ (automatic property tracking)
429+
## Auto-generation Hooks
463430
464-
### Distribution
431+
### iOS (Podfile)
465432
466-
- [ ] Support xcframework packaging (iOS)
467-
- [ ] Support AAR packaging (Android)
468-
- [ ] Figure out autolinking of generated code
433+
```ruby
434+
pre_install do |installer|
435+
system("npx", "brownie", "codegen", "-p", "swift")
436+
end
437+
```
469438
470-
### Documentation
439+
### Android (build.gradle.kts)
471440
472-
- [ ] Documentation
441+
```kotlin
442+
tasks.register("generateBrownfieldStore") {
443+
exec { commandLine("npx", "brownie", "codegen", "-p", "kotlin") }
444+
}
445+
preBuild.dependsOn("generateBrownfieldStore")
446+
```

0 commit comments

Comments
 (0)