Skip to content

Commit

Permalink
Add a single-value tracker, optimize tracked handle.
Browse files Browse the repository at this point in the history
  • Loading branch information
dead-claudia committed Oct 31, 2024
1 parent 8cafcfe commit 953665e
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 100 deletions.
3 changes: 2 additions & 1 deletion src/entry/mithril.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import m from "../core.js"
import {debouncer, throttler} from "../std/rate-limit.js"
import {link, route} from "../std/router.js"
import {match, p, query} from "../std/path-query.js"
import {tracked, trackedList} from "../std/tracked.js"
import init from "../std/init.js"
import lazy from "../std/lazy.js"
import tracked from "../std/tracked.js"
import withProgress from "../std/with-progress.js"

m.route = route
Expand All @@ -17,6 +17,7 @@ m.withProgress = withProgress
m.lazy = lazy
m.init = init
m.tracked = tracked
m.trackedList = trackedList
m.throttler = throttler
m.debouncer = debouncer

Expand Down
116 changes: 80 additions & 36 deletions src/std/tracked.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ why that was removed in favor of this:
want to clear some state and not other state. You might want to preserve some elements of a
sibling's state. Embedding it in the renderer would force an opinion on you, and in order to
work around it, you'd have to do something like this anyways.
As for the difference between `m.trackedList()` and `m.tracked()`, the first is for tracking lists
(and is explained above), and `m.tracked()` is for single values (but uses `m.trackedList()`
internally to avoid a ton of code duplication).
*/

import {checkCallback} from "../util.js"
import m from "../core.js"

import {checkCallback, noop} from "../util.js"

/**
* @template K, V
Expand All @@ -55,6 +61,7 @@ import {checkCallback} from "../util.js"
* @property {V} value
* @property {AbortSignal} signal
* @property {() => void} release
* @property {() => void} remove
*/

/**
Expand All @@ -70,47 +77,64 @@ import {checkCallback} from "../util.js"
* @property {(key: K) => boolean} delete
*/

/**
* @template K, V
* @param {Iterable<[K, V]>} [initial]
* @param {() => void} redraw
* @returns {Tracked<K, V>}
*/
var tracked = (redraw, initial) => {
var trackedState = (redraw) => {
checkCallback(redraw, false, "redraw")

/** @type {Map<K, TrackedHandle<K, V> & {_: AbortController}>} */ var state = new Map()
/** @type {Map<K, AbortController & TrackedHandle<K, V>>} */
var state = new Map()
var removed = new WeakSet()
/** @type {Set<TrackedHandle<K, V>>} */ var live = new Set()

/** @param {null | AbortController & TrackedHandle<K, V>} prev */
var abort = (prev) => {
try {
if (prev) {
if (prev._) prev._.abort()
else live.delete(prev)
if (removed.has(prev)) {
live.delete(prev)
} else {
prev.abort()
}
}
} catch (e) {
console.error(e)
}
}

// Bit 1 forcibly releases the old handle, and bit 2 causes an update notification to be sent
// (something that's unwanted during initialization).
/** @param {K} k */
var remove = (k, r) => {
var prev = state.get(k)
var result = state.delete(k)
abort(prev)
if (r) redraw()
return result
}

/**
* @param {K} k
* @param {V} v
* @param {number} bits
* Bit 1 forcibly releases the old handle, and bit 2 causes an update notification to be sent
* (something that's unwanted during initialization).
*/
var setHandle = (k, v, bits) => {
var prev = state.get(k)
var ctrl = new AbortController()
/** @type {TrackedHandle<K, V>} */
var handle = {
_: ctrl,
key: k,
value: v,
signal: ctrl.signal,
release() {
if (state.get(handle.key) === handle) {
handle._ = null
} else if (live.delete(handle)) {
redraw()
}
},
// Note: it extending `AbortController` is an implementation detail. It exposing a `signal`
// property is *not*.
var handle = /** @type {AbortController & TrackedHandle<K, V>} */ (new AbortController())
handle.key = k
handle.value = v
handle.release = (ev) => {
if (ev) m.capture(ev)
if (!handle) return
if (state.get(handle.key) === handle) {
removed.add(handle)
handle = null
} else if (live.delete(handle)) {
redraw()
}
}
handle.remove = (ev) => {
if (ev) m.capture(ev)
remove(handle.key, 0)
}
state.set(k, handle)
live.add(handle)
Expand All @@ -121,6 +145,18 @@ var tracked = (redraw, initial) => {
if (bits & 2) redraw()
}

return {s: state, l: live, h: setHandle, r: remove}
}

/**
* @template K, V
* @param {Iterable<[K, V]>} [initial]
* @param {() => void} redraw
* @returns {TrackedList<K, V>}
*/
var trackedList = (redraw, initial) => {
var {s: state, l: live, h: setHandle, r: remove} = trackedState(redraw)

for (var [k, v] of initial || []) setHandle(k, v, 1)

return {
Expand All @@ -130,14 +166,22 @@ var tracked = (redraw, initial) => {
get: (k) => (k = state.get(k)) && k.value,
set: (k, v) => setHandle(k, v, 3),
replace: (k, v) => setHandle(k, v, 2),
delete(k) {
var prev = state.get(k)
var result = state.delete(k)
abort(prev)
redraw()
return result
},
delete: (k) => remove(k, 1),
forget: (k) => (k = state.get(k)) && k.release(),
}
}

var tracked = (redraw) => {
var {l: live, h: setHandle, r: remove} = trackedState(redraw)
var initial = noop
var id = -1
return (state) => {
if (!Object.is(initial, initial = state)) {
remove(id++, 0)
setHandle(id, state, 1)
}
return [...live]
}
}

export {tracked as default}
export {tracked, trackedList}
4 changes: 2 additions & 2 deletions test-utils/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function setupGlobals(env = {}) {

o.beforeEach(() => {
initialize({...env})
return env.initialize && env.initialize()
return env.initialize && env.initialize(G)
})

o.afterEach(() => {
Expand All @@ -87,7 +87,7 @@ export function setupGlobals(env = {}) {
o(errors).deepEquals([])
errors.length = 0
o(mock.queueLength()).equals(0)
return env.cleanup && env.cleanup()
return env.cleanup && env.cleanup(G)
})

return {
Expand Down
Loading

0 comments on commit 953665e

Please sign in to comment.