Repository navigation
feat: add many() for batch ID generation - #45
Conversation
|
Thanks a lot for this, Miguel! It's a careful PR, and walking through each trap in the description made it easy to review. I checked it locally. Here is what I found. What's good:
Three small changes to the tests before merge. The code itself is fine. These are spots where a future bug would slip through with every test still green. I tried each bug on purpose to check. 1. Test the rejection sampling. If I replace it("skips out-of-range bytes instead of wrapping them", () => {
setBytesProvider((length) =>
Uint8Array.from({ length }, (_, i) => (i % 2 === 0 ? 3 : 1)),
);
expect(createId({ alphabet: "abc", size: 4 }).many("k", 2)).toEqual([
"k_bbbb",
"k_bbbb",
]);
});2. Test that each ID gets its own slice. If it("gives each id its own slice of the random bytes", () => {
let next = 0;
setBytesProvider((length) =>
Uint8Array.from({ length }, () => next++ & 0xff),
);
expect(createId({ size: 4, alphabet: "abcdefgh" }).many("u", 2)).toEqual([
"u_abcd",
"u_efgh",
]);
});3. Make the count tests check our own error message. it("rejects an invalid count", () => {
for (const bad of [-1, 1.5, Number.NaN, Infinity, MAX_BATCH + 1]) {
const call = () => id.many("user", bad);
expect(call).toThrow(RangeError);
expect(call).toThrow(/`count`/);
}
}); it("validates count and prefix", () => {
for (const bad of [-1, 1.5, MAX_BATCH + 1]) {
expect(() => sortableId.many("evt", bad)).toThrow(/`count`/);
}
expect(() => sortableId.many("", 0)).toThrow(TypeError);
});The The new tests don't need a Small things (optional):
import { assertValidCount } from "../internal/count.js";
import { assertValidPrefix } from "../internal/prefix.js";
Heads-up about #41, since you wrote that one too. Whichever of the two lands second, the I tried all of this locally and |
|
Thanks for the thorough review. Pushed: the rejection-sampling test, the per-id slice test, and count tests that match our own On #41: I'll pass |
74bf54a to
5b18d02
Compare
|
Hi! #41 (strictPrefix) is merged, so this branch needs a rebase AND one small code change. Squash your two commits first: rebasing them one at a time conflicts in 5 files in commit 1 (including src/internal/prefix.ts, because assertValidCount lived there before you moved it to count.ts) and again in commit 2. After squashing there are only 4 conflicts: (If you'd rather not rewrite history, Resolve them like this:
Now run These are the Then add this guard test at the end of test/many.test.ts: describe("many() with strictPrefix", () => {
it("rejects an invalid prefix like the single generator", () => {
for (const gen of [
createId({ strictPrefix: true }),
createSortableId({ strictPrefix: true }),
]) {
expect(() => gen.many("bad id", 3)).toThrow(
/invalid character " " at index 3/,
);
expect(() => gen.many("bad id", 0)).toThrow(TypeError);
expect(() => gen.many("a".repeat(64), 1)).toThrow(TypeError);
expect(gen.many("ok", 3)).toHaveLength(3);
}
});
});Optionally, add to your many() CHANGELOG bullet that it "checks the prefix exactly like a single call, including |
id, sortableId and the generators from createId/createSortableId gain many(prefix, count). Random bytes are drawn in chunks of at most 65,536 bytes (the getRandomValues quota), count is capped at 10,000, and the existing rejection sampling is reused. BatchIdGenerator is a new type, so IdGenerator stays unchanged.
|
Thanks for the exact steps. Squashed, rebased onto The typecheck error you predicted was real; both
|
5b18d02 to
be3f7bf
Compare
|
Merged, thank you! Verified locally: identical to the version I'd fully tested, 158 tests passing, and the strictPrefix guard holds. Great work across all five PRs. |
Closes #36
id.many(prefix, count),sortableId.many(prefix, count), and.manyon the generators returned bycreateId()/createSortableId().Handling the traps from the issue:
IdGeneratoris unchanged. NewBatchIdGenerator<S> = IdGenerator<S> & { many }is the return type ofcreateId/createSortableId. A test assigns a plain function toIdGeneratorandnpm run typecheckcovers it.countmust be an integer from 0 to 10,000 (MAX_BATCH), otherwiseRangeError. 0 returns[].randomIndices, no modulo.manycalls the single generator in a loop, so monotonic overflow into the next millisecond is handled by the existing code. A test uses a 2-char alphabet and a frozen clock to force the overflow.Benchmark
10,000 IDs, median of 15 runs after warm-up, Apple Silicon, same machine:
Array.frommanyid(size 24)id(size 8)sortableIdid(size 24)sortableIdSo the speed-up holds for plain IDs, and is much larger on Node, where each
node:cryptocall is expensive. It does not hold for sortable IDs: the random part is only drawn when the millisecond changes, so there is nothing to batch, andmanythere is a convenience over a loop (documented in the README). Not measured: browsers or Deno.CHANGELOG and README updated.
npm run verifypasses.