Skip to content

Commit 321f05c

Browse files
committed
feat: add setBlob and deleteBlob to batched atomic
Closes #7
1 parent cff4d3d commit 321f05c

6 files changed

+212
-80
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ where currently only 10 operations operations can be part of a commit.
1212
Similar to `Deno.Kv#atomic()`, but will batch individual transactions across as
1313
many atomic operations as necessary.
1414

15+
There are two additional methods supported on batched atomics not supported by
16+
Deno KV atomic transactions:
17+
18+
- `.setBlob(key, value, options?)` - Allows setting of arbitrarily size blob
19+
values as part of an atomic transaction. The values can be a byte
20+
`ReadableStream` or array buffer like. It will work around the constraints of
21+
Deno KV value sizes by splitting the value across multiple keys.
22+
23+
- `.deleteBlob(key)` - Allows deletion of all parts of a blob value as part of
24+
an atomic transaction.
25+
1526
The `commit()` method will return a promise which resolves with an array of
1627
results based on how many batches the operations was broken up into.
1728

batched_atomic.test.ts

+37
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
setup,
77
teardown,
88
} from "./_test_util.ts";
9+
import { keys } from "./keys.ts";
910

1011
import { batchedAtomic } from "./batched_atomic.ts";
12+
import { set } from "./blob.ts";
1113

1214
Deno.test({
1315
name: "batched atomic handles checks",
@@ -61,3 +63,38 @@ Deno.test({
6163
return teardown();
6264
},
6365
});
66+
67+
Deno.test({
68+
name: "batched atomic supports setting blobs",
69+
async fn() {
70+
const kv = await setup();
71+
const blob = new Uint8Array(65_536);
72+
window.crypto.getRandomValues(blob);
73+
const operation = batchedAtomic(kv);
74+
operation.setBlob(["hello"], blob);
75+
await operation.commit();
76+
const actual = await keys(kv, { prefix: ["hello"] });
77+
assertEquals(actual, [
78+
["hello", "__kv_toolbox_blob__", 1],
79+
["hello", "__kv_toolbox_blob__", 2],
80+
]);
81+
return teardown();
82+
},
83+
});
84+
85+
Deno.test({
86+
name: "batched atomic supports deleting blobs",
87+
async fn() {
88+
const kv = await setup();
89+
const blob = new Uint8Array(65_536);
90+
window.crypto.getRandomValues(blob);
91+
await set(kv, ["hello"], blob);
92+
assertEquals((await keys(kv, { prefix: ["hello"] })).length, 2);
93+
const operation = batchedAtomic(kv);
94+
await operation
95+
.deleteBlob(["hello"])
96+
.commit();
97+
assertEquals((await keys(kv, { prefix: ["hello"] })).length, 0);
98+
return teardown();
99+
},
100+
});

batched_atomic.ts

+66-17
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,23 @@
66
* @module
77
*/
88

9+
import { BLOB_KEY, setBlob } from "./blob_util.ts";
10+
import { keys } from "./keys.ts";
11+
912
/** The default batch size for atomic operations. */
1013
const BATCH_SIZE = 10;
1114

12-
type AtomicOperationKeys = keyof Deno.AtomicOperation;
15+
interface KVToolboxAtomicOperation extends Deno.AtomicOperation {
16+
deleteBlob(key: Deno.KvKey): this;
17+
18+
setBlob(
19+
key: Deno.KvKey,
20+
value: ArrayBufferLike | ReadableStream<Uint8Array>,
21+
options?: { expireIn?: number },
22+
): this;
23+
}
24+
25+
type AtomicOperationKeys = keyof KVToolboxAtomicOperation;
1326

1427
export class BatchedAtomicOperation {
1528
#batchSize: number;
@@ -19,7 +32,7 @@ export class BatchedAtomicOperation {
1932

2033
#enqueue<Op extends AtomicOperationKeys>(
2134
operation: Op,
22-
args: Parameters<Deno.AtomicOperation[Op]>,
35+
args: Parameters<KVToolboxAtomicOperation[Op]>,
2336
): this {
2437
this.#queue.push([operation, args]);
2538
return this;
@@ -98,6 +111,19 @@ export class BatchedAtomicOperation {
98111
return this.#enqueue("set", [key, value, options]);
99112
}
100113

114+
/**
115+
* Add to the operation a mutation that sets a blob value in the store if all
116+
* checks pass during the commit. The blob can be any array buffer like
117+
* structure or a byte {@linkcode ReadableStream}.
118+
*/
119+
setBlob(
120+
key: Deno.KvKey,
121+
value: ArrayBufferLike | ReadableStream<Uint8Array>,
122+
options?: { expireIn?: number },
123+
): this {
124+
return this.#enqueue("setBlob", [key, value, options]);
125+
}
126+
101127
/**
102128
* Add to the operation a mutation that deletes the specified key if all
103129
* checks pass during the commit.
@@ -106,6 +132,14 @@ export class BatchedAtomicOperation {
106132
return this.#enqueue("delete", [key]);
107133
}
108134

135+
/**
136+
* Add to the operation a set of mutations to delete the specified parts of
137+
* a blob value if all checks pass during the commit.
138+
*/
139+
deleteBlob(key: Deno.KvKey): this {
140+
return this.#enqueue("deleteBlob", [key]);
141+
}
142+
109143
/**
110144
* Add to the operation a mutation that enqueues a value into the queue if all
111145
* checks pass during the commit.
@@ -144,23 +178,38 @@ export class BatchedAtomicOperation {
144178
while (this.#queue.length) {
145179
const [method, args] = this.#queue.shift()!;
146180
count++;
147-
if (method === "check") {
148-
hasCheck = true;
149-
}
150-
// deno-lint-ignore no-explicit-any
151-
(operation[method] as any).apply(operation, args);
152-
if (count >= this.#batchSize || !this.#queue.length) {
153-
const rp = operation.commit();
154-
results.push(rp);
155-
if (this.#queue.length) {
156-
if (hasCheck) {
157-
const result = await rp;
158-
if (!result.ok) {
159-
break;
181+
if (method === "setBlob") {
182+
const queue = this.#queue;
183+
this.#queue = [];
184+
const [key, value, options] = args;
185+
const items = await keys(this.#kv, { prefix: [...key, BLOB_KEY] });
186+
await setBlob(this, key, value, items.length, options);
187+
this.#queue = [...this.#queue, ...queue];
188+
} else if (method === "deleteBlob") {
189+
const [key] = args;
190+
const items = await keys(this.#kv, { prefix: [...key, BLOB_KEY] });
191+
for (const item of items) {
192+
this.#queue.unshift(["delete", [item]]);
193+
}
194+
} else {
195+
if (method === "check") {
196+
hasCheck = true;
197+
}
198+
// deno-lint-ignore no-explicit-any
199+
(operation[method] as any).apply(operation, args);
200+
if (count >= this.#batchSize || !this.#queue.length) {
201+
const rp = operation.commit();
202+
results.push(rp);
203+
if (this.#queue.length) {
204+
if (hasCheck) {
205+
const result = await rp;
206+
if (!result.ok) {
207+
break;
208+
}
160209
}
210+
count = 0;
211+
operation = this.#kv.atomic();
161212
}
162-
count = 0;
163-
operation = this.#kv.atomic();
164213
}
165214
}
166215
}

blob.test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ Deno.test({
2525
},
2626
});
2727

28+
Deno.test({
29+
name: "set - sets a blob value as a stream",
30+
async fn() {
31+
const kv = await setup();
32+
const data = new Uint8Array(65_536);
33+
window.crypto.getRandomValues(data);
34+
const blob = new Blob([data]);
35+
await set(kv, ["hello"], blob.stream());
36+
const actual = await keys(kv, { prefix: ["hello"] });
37+
assertEquals(actual, [
38+
["hello", "__kv_toolbox_blob__", 1],
39+
["hello", "__kv_toolbox_blob__", 2],
40+
]);
41+
return teardown();
42+
},
43+
});
44+
2845
Deno.test({
2946
name: "set - replacing value sizes keys properly",
3047
async fn() {

blob.ts

+3-63
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,11 @@
88
* @module
99
*/
1010

11-
import {
12-
batchedAtomic,
13-
type BatchedAtomicOperation,
14-
} from "./batched_atomic.ts";
11+
import { batchedAtomic } from "./batched_atomic.ts";
12+
import { BLOB_KEY, CHUNK_SIZE, setBlob } from "./blob_util.ts";
1513
import { keys } from "./keys.ts";
1614

1715
const BATCH_SIZE = 10;
18-
const CHUNK_SIZE = 63_000;
19-
const BLOB_KEY = "__kv_toolbox_blob__";
2016

2117
function asStream(
2218
kv: Deno.Kv,
@@ -78,56 +74,6 @@ async function asUint8Array(
7874
return found ? value : null;
7975
}
8076

81-
function deleteKeys(
82-
operation: BatchedAtomicOperation,
83-
key: Deno.KvKey,
84-
count: number,
85-
length: number,
86-
): BatchedAtomicOperation {
87-
while (++count <= length) {
88-
operation.delete([...key, BLOB_KEY, count]);
89-
}
90-
return operation;
91-
}
92-
93-
function writeArrayBuffer(
94-
operation: BatchedAtomicOperation,
95-
key: Deno.KvKey,
96-
blob: ArrayBufferLike,
97-
start = 0,
98-
options?: { expireIn?: number },
99-
): [count: number, operation: BatchedAtomicOperation] {
100-
const buffer = new Uint8Array(blob);
101-
let offset = 0;
102-
let count = start;
103-
while (buffer.byteLength > offset) {
104-
count++;
105-
const chunk = buffer.subarray(offset, offset + CHUNK_SIZE);
106-
operation.set([...key, BLOB_KEY, count], chunk, options);
107-
offset += CHUNK_SIZE;
108-
}
109-
return [count, operation];
110-
}
111-
112-
async function writeStream(
113-
operation: BatchedAtomicOperation,
114-
key: Deno.KvKey,
115-
stream: ReadableStream<Uint8Array>,
116-
options?: { expireIn?: number },
117-
): Promise<[count: number, operation: BatchedAtomicOperation]> {
118-
let start = 0;
119-
for await (const chunk of stream) {
120-
[start, operation] = writeArrayBuffer(
121-
operation,
122-
key,
123-
chunk,
124-
start,
125-
options,
126-
);
127-
}
128-
return [start, operation];
129-
}
130-
13177
/** Remove/delete a binary object from the store with a given key that has been
13278
* {@linkcode set}.
13379
*
@@ -252,12 +198,6 @@ export async function set(
252198
): Promise<void> {
253199
const items = await keys(kv, { prefix: [...key, BLOB_KEY] });
254200
let operation = batchedAtomic(kv);
255-
let count;
256-
if (blob instanceof ReadableStream) {
257-
[count, operation] = await writeStream(operation, key, blob, options);
258-
} else {
259-
[count, operation] = writeArrayBuffer(operation, key, blob, 0, options);
260-
}
261-
operation = deleteKeys(operation, key, count, items.length);
201+
operation = await setBlob(operation, key, blob, items.length, options);
262202
await operation.commit();
263203
}

blob_util.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* This is an internal module which contains some of the blob writing
3+
* functionality and is not part of the public API of kv-toolbox.
4+
*
5+
* @module
6+
*/
7+
8+
import { type BatchedAtomicOperation } from "./batched_atomic.ts";
9+
10+
export const BLOB_KEY = "__kv_toolbox_blob__";
11+
export const CHUNK_SIZE = 63_000;
12+
13+
function deleteKeys(
14+
operation: BatchedAtomicOperation,
15+
key: Deno.KvKey,
16+
count: number,
17+
length: number,
18+
): BatchedAtomicOperation {
19+
while (++count <= length) {
20+
operation.delete([...key, BLOB_KEY, count]);
21+
}
22+
return operation;
23+
}
24+
25+
function writeArrayBuffer(
26+
operation: BatchedAtomicOperation,
27+
key: Deno.KvKey,
28+
blob: ArrayBufferLike,
29+
start = 0,
30+
options?: { expireIn?: number },
31+
): [count: number, operation: BatchedAtomicOperation] {
32+
const buffer = new Uint8Array(blob);
33+
let offset = 0;
34+
let count = start;
35+
while (buffer.byteLength > offset) {
36+
count++;
37+
const chunk = buffer.subarray(offset, offset + CHUNK_SIZE);
38+
operation.set([...key, BLOB_KEY, count], chunk, options);
39+
offset += CHUNK_SIZE;
40+
}
41+
return [count, operation];
42+
}
43+
44+
async function writeStream(
45+
operation: BatchedAtomicOperation,
46+
key: Deno.KvKey,
47+
stream: ReadableStream<Uint8Array>,
48+
options?: { expireIn?: number },
49+
): Promise<[count: number, operation: BatchedAtomicOperation]> {
50+
let start = 0;
51+
for await (const chunk of stream) {
52+
[start, operation] = writeArrayBuffer(
53+
operation,
54+
key,
55+
chunk,
56+
start,
57+
options,
58+
);
59+
}
60+
return [start, operation];
61+
}
62+
63+
export async function setBlob(
64+
operation: BatchedAtomicOperation,
65+
key: Deno.KvKey,
66+
blob: ArrayBufferLike | ReadableStream<Uint8Array>,
67+
itemCount: number,
68+
options?: { expireIn?: number },
69+
) {
70+
let count;
71+
if (blob instanceof ReadableStream) {
72+
[count, operation] = await writeStream(operation, key, blob, options);
73+
} else {
74+
[count, operation] = writeArrayBuffer(operation, key, blob, 0, options);
75+
}
76+
operation = deleteKeys(operation, key, count, itemCount);
77+
return operation;
78+
}

0 commit comments

Comments
 (0)