Last active
February 15, 2025 03:57
-
-
Save romgrk/bf39bb3e6ad34b7ea86c10a578a1ef00 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* BorrowMap: (possibly) zero-copy mutable data structure in a react context | |
* ========================================================================= | |
* | |
* The idea behind this concept is that we can have a mutable container in a | |
* React-linked store but ONLY IF we never share a reference to the mutable | |
* container itself. All the reads must be contained inside selectors. As long | |
* as no one has a second reference to the mutable container, then we are sure | |
* that no one is able to read/write it without us knowing. | |
* | |
* This follows the same rules that guide Rust's borrow checker, although the | |
* language here doesn't provide us with compile-time guarantees of safety, | |
* therefore the "borrow" operation must be marked as "unsafe". | |
*/ | |
/* STATE */ | |
type User = { | |
id: number, | |
name: string, | |
age: number, | |
} | |
const initialState = () => ({ | |
usersById: new BorrowMap<number, any>(), | |
}) | |
type State = ReturnType<typeof initialState> | |
/* SELECTORS */ | |
// reads the mutable structure without a copy | |
const userSelector = (state: State, id: number) => state.usersById.get(id) | |
// writes the mutable structure without a copy UNLESS `slow_usersByIdSelector` has been used | |
const userWriter = (state: State, id: number, user: User) => (state.usersById.set(id, user), state) | |
// as long as we keep reads localized to a scope and that the underlying mutable container | |
// doesn't escape that scope, we can assert that we don't need to make copies | |
const maxUserAgeSelector = (state: State) => { | |
return state.usersById.unsafe_borrow(usersById => { | |
let maxAge = 0 | |
for (const [, user] of usersById) { | |
if (user.age > maxAge) { | |
maxAge = user.age | |
} | |
} | |
return maxAge | |
}) | |
} | |
// This triggers a copy on the next write, but my intuition is that we can avoid it | |
// with specialized selectors. | |
const slow_usersByIdSelector = (state: State) => state.usersById.slow_read() | |
/* BORROW MAP */ | |
class BorrowMap<K, V> { | |
/** The tick field can provide a way to know if the container has changed. It is | |
* incremented on every read. */ | |
tick: number | |
private data: Map<K, V> | |
private wasRead: boolean | |
constructor(initial?: [K, V][]) { | |
this.tick = 0 | |
this.data = new Map(initial) | |
this.wasRead = false | |
} | |
get size() { return this.data.size } | |
get(key: K): V | undefined { return this.data.get(key) } | |
set(key: K, value: V) { | |
if (this.wasRead) { | |
this.wasRead = false | |
this.data = new Map(this.data) | |
} | |
this.tick += 1 | |
return this.data.set(key, value) | |
} | |
slow_read() { | |
this.wasRead = true | |
return this.data | |
} | |
/** | |
* SAFETY: Is it forbidden to keep a reference to `value` past the scope of `fn` | |
*/ | |
unsafe_borrow<T>(fn: (value: Map<K, V>) => T) { | |
return fn(this.data) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment