@yorozu/resource-cache
v1.0.151
Published
blob TTL / bytes / count engine (Collection in)
Readme
@yorozu/resource-cache
Blob TTL / bytes / count engine. Persistence is an injected @yorozu/db Collection. This package does not open IndexedDB or SQLite; the host supplies a driver.
Setup
import { openMemoryDb } from "@yorozu/db" // tests; prod: host driver.open
import type { Logger } from "@yorozu/log"
import { BytesLruMap, createResourceCache, dropDelete, dropStripBlob, resourceSchema } from "@yorozu/resource-cache"
let db = await openMemoryDb(resourceSchema("media", ["blobs", "thumbs"]))
let log: Logger | undefined
let blob: Blob
let caps = {
maxBytes: 1024 ** 3,
maxAgeMs: 7 * 24 * 60 * 60 * 1000,
maxEntries: 2000,
}
let strip = dropStripBlob()
let blobs = createResourceCache({
collection: db.collection("blobs"),
drop: {
apply(col, plan) {
if (plan.reason === "bytes") return strip.apply(col, plan)
return dropDelete.apply(col, plan)
},
},
caps,
evictMetaEveryNPuts: 50,
log, // optional; silent default
l1: new BytesLruMap({
maxBytes: 64 * 1024 * 1024,
maxEntries: 512,
sizeOf: (r) => r.bytes,
onEvict: (key) => {
/* revoke object URLs */
},
}),
onDropped: (keys, reason) => {
if (reason === "bytes" || reason === "ttl") {
/* revoke object URLs */
}
},
})
await blobs.put({ key: "a", storedAt: Date.now(), blob, meta: { name: "a.png" } })
await blobs.setCaps({ ...caps, maxBytes: 512 * 1024 ** 2 })
await blobs.evict("full")
let thumbs = createResourceCache({
collection: db.collection("thumbs"),
drop: dropDelete,
caps: { maxBytes: 512 * 1024 * 1024, maxAgeMs: 7 * 24 * 60 * 60 * 1000 },
})Logger is optional. Internally: opts.log ?? init("yorozu-resource-cache", "evict"). Bytes trim logs skip / start / done as info messages.
Omitted caps turn that policy off. dropDelete removes rows. dropStripBlob() keeps the row, bytes === 0, no blob. A host that wants strip on bytes and delete on TTL/count composes plan.reason in its own DropHandler.
Classed bytes trim
Optional class on put ("thumb" | "original" | any string). Pass preferDrop: ["thumb"] so bytes trim drops matching classes first (in array order, oldest within each class), then the rest by age. Two collections (blobs + thumbs) remain valid and are not required. Classed trim stays keysOnly: it scans covering by-evict then joins class from side index by-evict-class (["storedAt","bytes","class"]) — never values. Rows without a string class are absent from by-evict-class but still appear on by-evict. Omit / empty preferDrop stays a single by-evict keysOnly walk.
Default resourceSchema version is 2 so hosts that call it pick up by-evict-class on upgrade. Callers who pass an explicit version keep that number. Hosts must bump their DbSchema.version to create the new index (IDB skips an index name that already exists, so by-evict keyPath stays ["storedAt","bytes"]).
Musts
- Port is
Collection. Host opens memory / IDB / SQLite via aDbDriverand passesdb.collection(...). - Covering index
by-evictis["storedAt", "bytes"]. Side indexby-evict-classis["storedAt", "bytes", "class"]. TTL/count eviction walksscan("by-evict", { keysOnly: true, limit?: n }). Count trim passeslimit: extra; seed / ledger rebuild omitlimit. Bytes trim is always keysOnly; withpreferDropit also keysOnly-scansby-evict-classand joins class by primary key. NevergetAll()on blob collections. - Bytes:
getBytesTotalthen ordered pick. Under cap: do not scan for the pick. putbytes come fromblob.size. IfmaxBytesis set andbytes > maxBytes, put is a no-op.- Tests in this package use
openMemoryDbonly. Do not import@yorozu/db-idbor@yorozu/db-sqlitehere.
