Skip to content

Commit 2a0c155

Browse files
committed
feat: add blob support to uniqueCount
1 parent cffeaf5 commit 2a0c155

File tree

2 files changed

+158
-15
lines changed

2 files changed

+158
-15
lines changed

keys.test.ts

+101
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { assert, assertEquals, setup, teardown } from "./_test_util.ts";
2+
import { batchedAtomic } from "./batched_atomic.ts";
23

34
import {
45
equals,
@@ -120,6 +121,106 @@ Deno.test({
120121
},
121122
});
122123

124+
Deno.test({
125+
name: "uniqueCount - detects blobs",
126+
async fn() {
127+
const kv = await setup();
128+
const res = await batchedAtomic(kv)
129+
.set(["a"], "a")
130+
.set(["a", "b"], "b")
131+
.set(["a", "b", "c"], "c")
132+
.set(["a", "d", "f", "g"], "g")
133+
.set(["a", "h"], "h")
134+
.setBlob(["a", "i"], new Uint8Array([1, 2, 3]))
135+
.set(["a", "i", "j"], "j")
136+
.set(["e"], "e")
137+
.commit();
138+
assert(res.every(({ ok }) => ok));
139+
140+
const actual = await uniqueCount(kv, ["a"]);
141+
142+
assertEquals(actual, [
143+
{ key: ["a", "b"], count: 1 },
144+
{ key: ["a", "d"], count: 1 },
145+
{ key: ["a", "h"], count: 0 },
146+
{ key: ["a", "i"], count: 1, isBlob: true },
147+
]);
148+
149+
return teardown();
150+
},
151+
});
152+
153+
Deno.test({
154+
name: "uniqueCount - ignores blob keys",
155+
async fn() {
156+
const kv = await setup();
157+
const res = await batchedAtomic(kv)
158+
.set(["a"], "a")
159+
.set(["a", "b"], "b")
160+
.set(["a", "b", "c"], "c")
161+
.set(["a", "d", "f", "g"], "g")
162+
.set(["a", "h"], "h")
163+
.setBlob(["a", "i"], new Uint8Array([1, 2, 3]))
164+
.set(["a", "i", "j"], "j")
165+
.set(["e"], "e")
166+
.commit();
167+
assert(res.every(({ ok }) => ok));
168+
169+
const actual = await uniqueCount(kv, ["a", "i"]);
170+
171+
assertEquals(actual, [{ key: ["a", "i", "j"], count: 0 }]);
172+
173+
return teardown();
174+
},
175+
});
176+
177+
Deno.test({
178+
name: "uniqueCount - handles Uint8Array equality with blobs",
179+
async fn() {
180+
const kv = await setup();
181+
const res = await batchedAtomic(kv)
182+
.set(["a"], "a")
183+
.setBlob(["a", new Uint8Array([2, 3, 4])], new Uint8Array([1, 2, 3]))
184+
.set(["a", new Uint8Array([2, 3, 4]), "c"], "c")
185+
.set(["a", new Uint8Array([4, 5, 6]), "c"], "c")
186+
.set(["e"], "e")
187+
.commit();
188+
assert(res.every(({ ok }) => ok));
189+
190+
const actual = await uniqueCount(kv, ["a"]);
191+
192+
assertEquals(actual, [
193+
{ key: ["a", new Uint8Array([2, 3, 4])], count: 1, isBlob: true },
194+
{ key: ["a", new Uint8Array([4, 5, 6])], count: 1 },
195+
]);
196+
return teardown();
197+
},
198+
});
199+
200+
Deno.test({
201+
name: "uniqueCount - ignores blob keys with Uint8Array key parts",
202+
async fn() {
203+
const kv = await setup();
204+
const res = await batchedAtomic(kv)
205+
.set(["a"], "a")
206+
.setBlob(["a", new Uint8Array([2, 3, 4])], new Uint8Array([1, 2, 3]))
207+
.set(["a", new Uint8Array([2, 3, 4]), "c"], "c")
208+
.set(["a", new Uint8Array([4, 5, 6]), "c"], "c")
209+
.set(["e"], "e")
210+
.commit();
211+
assert(res.every(({ ok }) => ok));
212+
213+
const actual = await uniqueCount(kv, ["a", new Uint8Array([2, 3, 4])]);
214+
215+
assertEquals(actual, [{
216+
key: ["a", new Uint8Array([2, 3, 4]), "c"],
217+
count: 0,
218+
}]);
219+
220+
return teardown();
221+
},
222+
});
223+
123224
Deno.test({
124225
name: "equals",
125226
fn() {

keys.ts

+57-15
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@
146146

147147
import { timingSafeEqual } from "jsr:@std/[email protected]/timing_safe_equal";
148148

149+
import { BLOB_KEY, BLOB_META_KEY } from "./blob_util.ts";
150+
149151
function addIfUnique(set: Set<Deno.KvKeyPart>, item: Uint8Array) {
150152
for (const i of set) {
151153
if (ArrayBuffer.isView(i) && timingSafeEqual(i, item)) {
@@ -156,17 +158,29 @@ function addIfUnique(set: Set<Deno.KvKeyPart>, item: Uint8Array) {
156158
}
157159

158160
function addOrIncrement(
159-
map: Map<Deno.KvKeyPart, number>,
161+
map: Map<Deno.KvKeyPart, { count: number; isBlob?: boolean }>,
160162
item: Uint8Array,
161-
increment: boolean,
163+
next: Deno.KvKeyPart | undefined,
162164
) {
165+
let count = 0;
166+
let isBlob = false;
167+
if (next) {
168+
if (next === BLOB_KEY) {
169+
isBlob = true;
170+
} else if (next !== BLOB_META_KEY) {
171+
count = 1;
172+
}
173+
}
163174
for (const [k, v] of map) {
164175
if (ArrayBuffer.isView(k) && timingSafeEqual(k, item)) {
165-
map.set(k, increment ? v + 1 : v);
176+
if (isBlob) {
177+
v.isBlob = true;
178+
}
179+
v.count = count;
166180
return;
167181
}
168182
}
169-
map.set(item, increment ? 1 : 0);
183+
map.set(item, isBlob ? { count, isBlob } : { count });
170184
}
171185

172186
/** Determines if one {@linkcode Deno.KvKeyPart} equals another. This is more
@@ -318,6 +332,9 @@ export async function unique(
318332
throw new TypeError(`Unexpected key length of ${key.length}.`);
319333
}
320334
const part = key[prefixLength];
335+
if (part === BLOB_KEY || part === BLOB_META_KEY) {
336+
continue;
337+
}
321338
if (ArrayBuffer.isView(part)) {
322339
addIfUnique(prefixes, part);
323340
} else {
@@ -327,6 +344,17 @@ export async function unique(
327344
return [...prefixes].map((part) => [...prefix, part]);
328345
}
329346

347+
/** Elements of an array that gets resolved when calling
348+
* {@linkcode uniqueCount}. */
349+
export interface UniqueCountElement {
350+
/** The key of the element. */
351+
key: Deno.KvKey;
352+
/** The number of sub-keys the key has. */
353+
count: number;
354+
/** Indicates if the value of the key is a kv-toolbox blob value. */
355+
isBlob?: boolean;
356+
}
357+
330358
/** Resolves with an array of unique sub keys/prefixes for the provided prefix
331359
* along with the number of sub keys that match that prefix. The `count`
332360
* represents the number of sub keys, a value of `0` indicates that only the
@@ -336,7 +364,11 @@ export async function unique(
336364
* where you are retrieving a list including counts and you want to know all the
337365
* unique _descendants_ of a key in order to be able to enumerate them.
338366
*
339-
* For example if you had the following keys stored in a datastore:
367+
* If you omit a `prefix`, all unique root keys are resolved.
368+
*
369+
* @example
370+
*
371+
* If you had the following keys stored in a datastore:
340372
*
341373
* ```ts
342374
* ["a", "b"]
@@ -345,7 +377,7 @@ export async function unique(
345377
* ["a", "d", "f"]
346378
* ```
347379
*
348-
* And you would get the following results when using `unique()`:
380+
* And you would get the following results when using `uniqueCount()`:
349381
*
350382
* ```ts
351383
* import { uniqueCount } from "jsr:@kitsonk/kv-toolbox/keys";
@@ -356,36 +388,46 @@ export async function unique(
356388
* // { key: ["a", "d"], count: 2 }
357389
* await kv.close();
358390
* ```
359-
*
360-
* If you omit a `prefix`, all unique root keys are resolved.
361391
*/
362392
export async function uniqueCount(
363393
kv: Deno.Kv,
364394
prefix: Deno.KvKey = [],
365395
options?: Deno.KvListOptions,
366-
): Promise<{ key: Deno.KvKey; count: number }[]> {
396+
): Promise<UniqueCountElement[]> {
367397
const list = kv.list({ prefix }, options);
368398
const prefixLength = prefix.length;
369-
const prefixCounts = new Map<Deno.KvKeyPart, number>();
399+
const prefixCounts = new Map<
400+
Deno.KvKeyPart,
401+
{ count: number; isBlob?: boolean }
402+
>();
370403
for await (const { key } of list) {
371404
if (key.length <= prefixLength) {
372405
throw new TypeError(`Unexpected key length of ${key.length}.`);
373406
}
374407
const part = key[prefixLength];
408+
if (part === BLOB_KEY || part === BLOB_META_KEY) {
409+
continue;
410+
}
411+
const next = key[prefixLength + 1];
375412
if (ArrayBuffer.isView(part)) {
376-
addOrIncrement(prefixCounts, part, key.length > (prefixLength + 1));
413+
addOrIncrement(prefixCounts, part, next);
377414
} else {
378415
if (!prefixCounts.has(part)) {
379-
prefixCounts.set(part, 0);
416+
prefixCounts.set(part, { count: 0 });
380417
}
381-
if (key.length > (prefixLength + 1)) {
382-
prefixCounts.set(part, prefixCounts.get(part)! + 1);
418+
if (next) {
419+
const count = prefixCounts.get(part)!;
420+
if (next === BLOB_KEY) {
421+
count.isBlob = true;
422+
} else if (next !== BLOB_META_KEY) {
423+
count.count++;
424+
}
383425
}
384426
}
385427
}
386428
return [...prefixCounts].map(([part, count]) => ({
387429
key: [...prefix, part],
388-
count,
430+
...count,
389431
}));
390432
}
391433

0 commit comments

Comments
 (0)