{"id":"alternativelua/catalog","name":"catalog","scope":"alternativelua","platform":"roblox","description":"Catalog library","version":"0.4.0","latest":"0.4.0","versions":["0.1.0","0.2.0","0.4.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":false,"dependencies":{},"integrity":"7b28d0167cb590fd756c55419037f38f963510e8d8d96cdc926bb43a50cb0347","likes":0,"downloads":0,"install":"forest install alternativelua/catalog","url":"https://forest.dev/p/roblox/alternativelua/catalog","files":"https://api.forest.dev/ai/package/roblox/alternativelua/catalog/files","readme":"# Catalog\nA fast library for searching a large group of items\n\n## Installation\n\n- **Wally (Luau):** `catalog = \"alternativelua/catalog@0.4.0\"` or through releases\n- **npm (roblox-ts):** `npm install @rbxts/catalog`\n\n## Usage\n\n```luau\nlocal Catalog = require(ReplicatedStorage.Catalog)\n\n-- Create a catalog. Pass the parameters you want indexed for fast equality\n-- search; omit the argument to index every parameter.\nlocal cat = Catalog.new({ \"category\", \"rarity\", \"tier\" })\n\n-- Add items. Each item needs a unique `id` and a `parameters` table.\n-- The optional `data` field holds any payload you want to carry along.\nCatalog.AddToCatalog(cat, {\n\tid = \"sword_01\",\n\tparameters = { category = \"weapon\", rarity = \"legendary\", tier = 42 },\n\tdata = { damage = 120 },\n})\n\n-- Or add many at once.\nCatalog.BulkAddToCatalog(cat, {\n\t{ id = \"shield_01\", parameters = { category = \"armor\", rarity = \"rare\", tier = 12 } },\n\t{ id = \"potion_01\", parameters = { category = \"consumable\", rarity = \"common\", tier = 3 } },\n})\n\n-- Equality search. Multiple parameters are AND-ed together. Indexed\n-- parameters go through the inverted index (fast); non-indexed ones are\n-- filtered linearly against those candidates, so results are always correct.\nlocal legendaryWeapons = Catalog.Search(cat, { category = \"weapon\", rarity = \"legendary\" })\n\n-- Update parameters on an existing item; the index stays in sync.\n-- (Never mutate item.parameters directly — that corrupts the index.)\nCatalog.SetParameter(cat, \"sword_01\", \"tier\", 43)\nCatalog.SetParameter(cat, \"sword_01\", \"event\", nil) -- nil removes the parameter\nCatalog.UpdateParameters(cat, \"sword_01\", { rarity = \"mythic\", tier = 44 })\n\n-- Substring search over a string parameter (Boyer-Moore, full scan).\nlocal swords = Catalog.SearchText(cat, \"name\", \"sword\")\n\n-- Case-insensitive substring search; combine with sort via the options table.\nlocal anySwords = Catalog.SearchText(cat, \"name\", \"SWORD\", { caseInsensitive = true })\nlocal sortedSwords = Catalog.SearchText(cat, \"name\", \"sword\", {\n\tcaseInsensitive = true,\n\tsort = { parameter = \"tier\", direction = \"descending\" },\n})\n\n-- Sort results by passing sort options as the last argument to either\n-- search function. Direction defaults to \"ascending\".\nlocal byTier = Catalog.Search(cat, { category = \"weapon\" }, { parameter = \"tier\", direction = \"descending\" })\n\n-- Multi-key sort: pass an array of keys; earlier keys take priority.\nlocal ranked = Catalog.Search(cat, { category = \"weapon\" }, {\n\t{ parameter = \"rarity\" },\n\t{ parameter = \"tier\", direction = \"descending\" },\n})\n\n-- Or sort any item array in place directly.\nCatalog.Sort(swords, { parameter = \"name\" })\n\n-- Custom search: supply your own predicate for conditions the index can't\n-- express (ranges, checks against `data`, etc.). O(n) scan, sort optional.\nlocal hardHitters = Catalog.SearchCustom(cat, function(item)\n\treturn item.parameters.category == \"weapon\" and item.data ~= nil and item.data.damage > 100\nend, { parameter = \"tier\", direction = \"descending\" })\n\n-- Direct lookup by id.\nlocal item = Catalog.Get(cat, \"sword_01\")\nlocal exists = Catalog.Has(cat, \"sword_01\")\n\n-- Removal.\nCatalog.RemoveFromCatalog(cat, \"potion_01\")\nCatalog.BulkRemoveFromCatalog(cat, { \"shield_01\" })\n\n-- Clear everything.\nCatalog.Destroy(cat)\n```\n\n## roblox-ts\n\nThe npm package ships the same Luau source with TypeScript typings. Type the\nparameters (and optionally the `data` payload) when creating a catalog and\nevery search, sort key, and result is checked against them:\n\n```ts\nimport Catalog from \"@rbxts/catalog\";\n\ntype ItemParams = { category: string; rarity: string; tier: number; name: string };\ntype ItemData = { damage: number };\n\n// `new Catalog(...)` compiles to `Catalog.new(...)`.\nconst cat = new Catalog<ItemParams, ItemData>([\"category\", \"rarity\", \"tier\"]);\n\nCatalog.AddToCatalog(cat, {\n\tid: \"sword_01\",\n\tparameters: { category: \"weapon\", rarity: \"legendary\", tier: 42, name: \"Sword\" },\n\tdata: { damage: 120 },\n});\n\n// Typed: `{ categry: \"weapon\" }` or `{ tier: \"42\" }` are compile errors.\nconst legendaryWeapons = Catalog.Search(cat, { category: \"weapon\", rarity: \"legendary\" });\n\n// Sort keys are constrained to keyof ItemParams.\nconst ranked = Catalog.Search(cat, { category: \"weapon\" }, [\n\t{ parameter: \"rarity\" },\n\t{ parameter: \"tier\", direction: \"descending\" },\n]);\n\n// `item.data` is typed as ItemData | undefined.\nconst hardHitters = Catalog.SearchCustom(cat, (item) => item.data !== undefined && item.data.damage > 100);\n```\n\n## Performance\n\nBenchmarked on **100,000 items** (Catalog v0.4.0). Each item has `category`,\n`rarity`, `tier`, and `name` parameters; `category`, `rarity`, and `tier` are\nindexed. See [`test/init.server.luau`](test/init.server.luau) for the harness.\n\n| Operation | Method | Time | Results |\n| --- | --- | --- | --- |\n| Build the catalog | `BulkAddToCatalog` (100k items) | **31.46 ms** total | — |\n| Equality search, high-selectivity | `Search { tier = 42 }` | **0.0298 ms/op** | 1,000 |\n| Equality search, low-selectivity | `Search { category = \"weapon\" }` | **0.6725 ms/op** | 20,000 |\n| Equality search, intersection | `Search { category = \"weapon\", rarity = \"legendary\" }` | **0.7151 ms/op** | 4,000 |\n| Substring search, matching | `SearchText(name, \"sword\")` | **22.9133 ms/op** | 20,000 |\n| Substring search, no match | `SearchText(name, \"<no match>\")` | **15.2455 ms/op** | 0 |\n| Custom search, predicate | `SearchCustom(tier >= 90)` | **11.2289 ms/op** | 10,000 |\n| Sorted search, single key | `Search { category = \"weapon\" }` + sort `tier` desc | **10.0535 ms/op** | 20,000 |\n| Sorted search, multi-key | `Search { category = \"weapon\" }` + sort `rarity`, `tier` desc | **12.3270 ms/op** | 20,000 |\n| Direct lookup by id | `Get(\"item_50000\")` | **~0.0000 ms/op** | — |\n\n### Notes\n\n- **Equality search** uses an inverted index, so cost scales with the number of\n  *matching* items, not the catalog size. Multi-parameter queries intersect on\n  the smallest result set first, which is why `weapon + legendary` (4,000 hits)\n  runs about as fast as `weapon` alone despite the extra filter.\n- **Non-indexed parameters** in a `Search` query are filtered linearly: against\n  the candidates produced by the indexed parameters when there are any, or as a\n  full scan otherwise. Results are always correct; indexing a parameter only\n  makes querying it faster.\n- **Substring search** (`SearchText`) is a Boyer-Moore, O(n) scan over every\n  item, since arbitrary substrings can't be indexed. The no-match case is\n  slightly faster because it never allocates result entries; both cases visit\n  all 100k items.\n- **Custom search** (`SearchCustom`) calls the predicate once per item, so it\n  is also an O(n) scan; cost is the scan plus whatever the predicate does.\n- **Sorted search** adds the sort cost on top of the search: sorting the\n  20,000 `weapon` hits by `tier` costs about 9.4 ms over the 0.67 ms unsorted\n  search, and the two-key sort about 11.7 ms. Cost scales with the *result*\n  count, not the catalog size.\n- **Direct lookup** (`Get`) is a single hash-map read and effectively free\n  (below timer resolution across 100k iterations).\n\n## Sorting\n\nBoth `Search` and `SearchText` accept optional sort options as their last\nargument, and `Catalog.Sort(items, options)` sorts any `{ Item }` array in\nplace (returning the same array). Options are a single key or an array of\nkeys, each `{ parameter: string, direction: \"ascending\" | \"descending\"? }`.\n\nOrdering rules:\n\n- Values of the same type compare naturally (`<` for numbers and strings,\n  `false` before `true` for booleans).\n- Mixed types order `boolean < number < string`.\n- Items missing the parameter always sort last, regardless of direction.\n- Ties fall back to comparing `id`, so output order is deterministic.\n\nSorting extracts each item's key values once up front, so the comparator runs\non flat arrays instead of hashing into `item.parameters` on every comparison.\nWhen a key's values are uniformly numbers or strings with none missing, a raw\n`<` fast path is used. Compared to a naive `table.sort` comparator this is\nroughly 2–3.5x faster for single-key sorts and 1.4–2x for multi-key.\n\n## Updating items\n\n`Catalog.SetParameter(cat, id, parameter, value)` changes one parameter on an\nexisting item and reindexes only that parameter; passing `nil` removes the\nparameter. `Catalog.UpdateParameters(cat, id, { ... })` merge-updates several\nat once. Both return `false` when no item with `id` exists. To replace an item\nwholesale, `AddToCatalog` with the same id still works.\n\nNever mutate `item.parameters` on an item that is inside a catalog — the\ninverted index would keep pointing at the old values. `data` is not indexed,\nso mutating the payload directly is fine.\n\n## Custom search\n\n`Catalog.SearchCustom(cat, predicate, sort?)` runs your own\n`(item) -> boolean` function against every item and returns the matches. Use\nit for anything the inverted index can't answer: range checks, conditions on\nthe `data` payload, or combinations with OR logic. Like `SearchText` it is an\nO(n) scan over the whole catalog, so prefer `Search` when plain equality on\nindexed parameters is enough.\n","readmeTruncated":false}