재구축할 인프라를 대체하는 8kB 상태 컨테이너

hackernews | | 📦 오픈소스
#entity store #javascript #quop #review #상태관리 #인프라
원문 출처: hackernews · Genesis Park에서 요약 및 분석

요약

기사는 단 8kB의 초경량 상태 관리 컨테이너를 소개하며, 이를 통해 개발자들이 새로운 인프라를 직접 구축해야 하는 수고를 덜 수 있다고 강조합니다. 해당 툴은 복잡한 인프라를 대체하여 개발 생산성을 높이고 시스템의 효율성을 개선하는 데 중점을 두고 있습니다.

본문

Works anywhere with JavaScript and memory. QuOp is a typed entity store with spatial indexing, reactive events, and compound queries. ~10kB, zero dependencies. The code written with QuOp reads like the question you're asking, not like the data structure answering it. Core: ~10kB | ~190 lines Plus: +4.6kB | +~24 lines Total: ~14kB 7kB Minified 2.8kB Minified + Gzipped QuOp stores typed entities — plain objects with an id , a type , and any fields you choose. It maintains indexes automatically so you can retrieve them instantly by type, filter them with compound predicates, find them by spatial proximity, and react to changes through events — all in a single in-memory structure with zero configuration. import { createStore, where } from './QuOp.js' const store = createStore() // Entities are plain objects — any shape, any fields const a = store.create('sensor', { location: 'floor-3', value: 42, active: true }) const b = store.create('sensor', { location: 'floor-1', value: 18, active: false }) const c = store.create('threshold', { min: 20, max: 80, channel: 'floor-3' }) // Retrieve by type — O(1), no scan const sensors = store.find('sensor').all() // Compound filter — multiple conditions, cached after first call const active = store.find('sensor', where.and( where.eq('active', true), where.gt('value', 20) )).all() // React to changes store.on('update', ({ id, item, old }) => { console.log(`${id}: ${old.value} → ${item.value}`) }) // Mutations merge changes and maintain all indexes store.update(a.id, { value: 55 }) store.set(b.id, 'active', true) store.increment(a.id, 'value', 3) Type indexing. Every entity belongs to a type. The type index is maintained on every write. store.find('sensor') returns all sensors without scanning the full item set — the index hands back the Set directly. Compound queries with a cache. where.eq , where.gt , where.and , where.or and the rest build predicates with stable string keys. The first call scans; every subsequent call with the same predicate returns a frozen result object in under 0.01ms. Writes evict only the cache entries whose predicate fields overlap with the changed fields — a write to value leaves location and active queries warm. Spatial indexing. Entities with x and y coordinates are tracked in a grid cell index. store.near(type, x, y, radius) searches only the cells that intersect the radius, filtered to the given type, sorted by distance. Non-spatial writes skip the spatial index entirely. Live views. store.view(type, predicate) wraps a query in a cached result that recomputes automatically when relevant entities change. Between writes the result is returned directly — no scan, no predicate evaluation. Views support spatial recentering with a movement threshold. Events. on('create' | 'update' | 'delete' | 'change' | 'batch', callback) — subscribe to any write. Events include the item and its previous state. Unsubscribe by calling the returned function. Transactions. store.transaction(() => { ... }) — all-or-nothing. Any throw rolls back every write in the block. Functional updaters. store.update(id, old => ({ value: old.value * 2 })) — derive the next state from current state atomically. The write path in w() makes decisions based on what is actually needed: - The changed-field Set is only constructed if the query cache for that type has entries — if nothing is cached, field extraction is skipped entirely - The pre-mutation snapshot ( old in update events) is only spread if an update or change listener is registered - The spatial index update is only run if x ory is among the changed fields - Transaction logging only runs when inside a transaction The result is a write path that pays for what it uses. A store with no listeners, no cached queries, and no spatial coordinates is close to the cost of a bare Map mutation. To take advantage of QuOps cache, its a good idea to choose a abstraction level such that one entity is sparse in changes. All benchmarks: Node v22, Intel Xeon Platinum 8370C, median of 100 runs + 20 warmup(Warmed JIT) and 1 run 0 warmup(Cold Start) with a deterministic PRNG (mulberry32, fixed seed), reporting median. Compared against LokiJS, NodeCache, MemoryCache, QuickLRU, Lodash collections, Immutable.js, and raw Array/Object stores. Hardware variance. Absolute numbers scale with your CPU — the relative ordering is what stays stable. Run node bench.js to measure on your own hardware. | Library | Cold Start (ops/sec) | Warmed JIT (ops/sec) | |---|---|---| | QuOp (ref) | 1,457,095 | 8,549,449 | | QuOp (safe get) | 1,430,647 | 7,139,960 | | LokiJS | 67,574 | 85,987 | | MemoryCache | 22,450 | 27,806 | | Lodash | 19,376 | 27,841 | | NodeCache | 21,728 | 26,959 | | QuickLRU | 18,489 | 27,676 | | Immutable | 20,184 | 21,021 | | Array Store | 15,892 | 17,446 | | Object Store | 9,888 | 10,976 | The mixed workload is the one that reflects real usage. The gap between QuOp and LokiJS widens after JIT warmup, which reflects long-running application behavior. Th

Genesis Park 편집팀이 AI를 활용하여 작성한 분석입니다. 원문은 출처 링크를 통해 확인할 수 있습니다.

공유

관련 저널 읽기

전체 보기 →