Skip to content

Instantly share code, notes, and snippets.

@romgrk
Last active February 15, 2025 03:57
Show Gist options
  • Save romgrk/bf39bb3e6ad34b7ea86c10a578a1ef00 to your computer and use it in GitHub Desktop.
Save romgrk/bf39bb3e6ad34b7ea86c10a578a1ef00 to your computer and use it in GitHub Desktop.
/*
* 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