feat(core): add @choojs/core — ESM port of choo with consolidated nano* internals
Same public API as choo 7.1.0 (use/route/start/mount/toString/emit; choo() callable with or without new, Choo exported). nanobus, nanorouter+wayfarer, nanolru+component cache, nanoraf, nanohref, nanotiming, document-ready and scroll-to-anchor are ported into lib/ with per-file attribution; nanoquery is replaced by URLSearchParams. The v7 node test suite is ported from tape to node:test and passes unchanged in behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
This commit is contained in:
co-authored by
Claude Fable 5
parent
90cca611c9
commit
4ad3d02207
@@ -0,0 +1,150 @@
|
||||
// Component instance cache for state.cache().
|
||||
// LRU ported from nanolru 1.0.0 (MIT) — https://github.com/s3ththompson/nanolru
|
||||
// Cache wrapper ported from choo 7.1.0 component/cache.js (MIT)
|
||||
|
||||
import { ok, equal } from './assert.js'
|
||||
|
||||
export class LRU {
|
||||
constructor (opts) {
|
||||
if (typeof opts === 'number') opts = { max: opts }
|
||||
if (!opts) opts = {}
|
||||
this.cache = {}
|
||||
this.head = this.tail = null
|
||||
this.length = 0
|
||||
this.max = opts.max || 1000
|
||||
}
|
||||
|
||||
get keys () {
|
||||
return Object.keys(this.cache)
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.cache = {}
|
||||
this.head = this.tail = null
|
||||
this.length = 0
|
||||
}
|
||||
|
||||
remove (key) {
|
||||
if (typeof key !== 'string') key = '' + key
|
||||
if (!Object.hasOwn(this.cache, key)) return
|
||||
|
||||
const element = this.cache[key]
|
||||
delete this.cache[key]
|
||||
this._unlink(key, element.prev, element.next)
|
||||
return element.value
|
||||
}
|
||||
|
||||
_unlink (key, prev, next) {
|
||||
this.length--
|
||||
|
||||
if (this.length === 0) {
|
||||
this.head = this.tail = null
|
||||
} else {
|
||||
if (this.head === key) {
|
||||
this.head = prev
|
||||
this.cache[this.head].next = null
|
||||
} else if (this.tail === key) {
|
||||
this.tail = next
|
||||
this.cache[this.tail].prev = null
|
||||
} else {
|
||||
this.cache[prev].next = next
|
||||
this.cache[next].prev = prev
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peek (key) {
|
||||
if (!Object.hasOwn(this.cache, key)) return
|
||||
return this.cache[key].value
|
||||
}
|
||||
|
||||
set (key, value) {
|
||||
if (typeof key !== 'string') key = '' + key
|
||||
|
||||
let element
|
||||
|
||||
if (Object.hasOwn(this.cache, key)) {
|
||||
element = this.cache[key]
|
||||
element.value = value
|
||||
|
||||
// If it's already the head, there's nothing more to do:
|
||||
if (key === this.head) return value
|
||||
this._unlink(key, element.prev, element.next)
|
||||
} else {
|
||||
element = { value, next: null, prev: null }
|
||||
this.cache[key] = element
|
||||
|
||||
// Eviction is only possible if the key didn't already exist:
|
||||
if (this.length === this.max) this.evict()
|
||||
}
|
||||
|
||||
this.length++
|
||||
element.next = null
|
||||
element.prev = this.head
|
||||
|
||||
if (this.head) this.cache[this.head].next = key
|
||||
this.head = key
|
||||
|
||||
if (!this.tail) this.tail = key
|
||||
return value
|
||||
}
|
||||
|
||||
get (key) {
|
||||
if (typeof key !== 'string') key = '' + key
|
||||
if (!Object.hasOwn(this.cache, key)) return
|
||||
|
||||
const element = this.cache[key]
|
||||
|
||||
if (this.head !== key) {
|
||||
if (key === this.tail) {
|
||||
this.tail = element.next
|
||||
this.cache[this.tail].prev = null
|
||||
} else {
|
||||
// Set prev.next -> element.next:
|
||||
this.cache[element.prev].next = element.next
|
||||
}
|
||||
|
||||
// Set element.next.prev -> element.prev:
|
||||
this.cache[element.next].prev = element.prev
|
||||
|
||||
// Element is the new head
|
||||
this.cache[this.head].next = key
|
||||
element.prev = this.head
|
||||
element.next = null
|
||||
this.head = key
|
||||
}
|
||||
|
||||
return element.value
|
||||
}
|
||||
|
||||
evict () {
|
||||
if (!this.tail) return
|
||||
this.remove(this.tail)
|
||||
}
|
||||
}
|
||||
|
||||
export default class ComponentCache {
|
||||
constructor (state, emit, lru) {
|
||||
equal(typeof state, 'object', 'ComponentCache: state should be type object')
|
||||
equal(typeof emit, 'function', 'ComponentCache: emit should be type function')
|
||||
|
||||
if (typeof lru === 'number') this.cache = new LRU(lru)
|
||||
else this.cache = lru || new LRU(100)
|
||||
this.state = state
|
||||
this.emit = emit
|
||||
}
|
||||
|
||||
// Get & create component instances.
|
||||
render (Component, id, ...args) {
|
||||
equal(typeof Component, 'function', 'ComponentCache.render: Component should be type function')
|
||||
ok(typeof id === 'string' || typeof id === 'number', 'ComponentCache.render: id should be type string or type number')
|
||||
|
||||
let el = this.cache.get(id)
|
||||
if (!el) {
|
||||
el = new Component(id, this.state, this.emit, ...args)
|
||||
this.cache.set(id, el)
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user