-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathkv-durable-writes.test.ts
More file actions
90 lines (77 loc) · 2.98 KB
/
Copy pathkv-durable-writes.test.ts
File metadata and controls
90 lines (77 loc) · 2.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import "fake-indexeddb/auto";
import { afterEach, describe, expect, it } from "@jest/globals";
import { getDocument, putDocument, updateDocument } from "./kv";
// What aborts a transaction at commit time in the field is quota exhaustion,
// and there is no way to exhaust fake-indexeddb's quota. Aborting from a
// listener on the successful request reproduces the ordering that matters:
// the request succeeds, and the transaction goes away afterwards.
const realPut = IDBObjectStore.prototype.put;
// Aborting while the request is still pending is the other order: the abort
// settles every in-flight request with an AbortError before the transaction
// itself gives up, so the request never succeeds at all.
function abortDuringNextPut(): void {
IDBObjectStore.prototype.put = function put(
this: IDBObjectStore,
...args: Parameters<IDBObjectStore["put"]>
) {
IDBObjectStore.prototype.put = realPut;
const request = realPut.apply(this, args);
this.transaction.abort();
return request;
};
}
function abortAfterNextPut(): void {
IDBObjectStore.prototype.put = function put(
this: IDBObjectStore,
...args: Parameters<IDBObjectStore["put"]>
) {
IDBObjectStore.prototype.put = realPut;
const request = realPut.apply(this, args);
request.addEventListener("success", () => {
request.transaction?.abort();
});
return request;
};
}
afterEach(() => {
IDBObjectStore.prototype.put = realPut;
});
describe("a write whose transaction aborts after the request succeeds", () => {
it("rejects rather than reporting the write as durable", async () => {
abortAfterNextPut();
await expect(
putDocument("aborted-preset", { name: "one" })
).rejects.toThrow();
});
it("leaves nothing behind to read", async () => {
abortAfterNextPut();
await putDocument("rolled-back-preset", { name: "two" }).catch(
() => undefined
);
expect(await getDocument("rolled-back-preset")).toBeUndefined();
});
// `run`'s `onerror` path cannot be isolated from its `onabort` path through
// this API: an unprevented request error aborts its own transaction anyway,
// so both handlers fire and either one alone would reject. Removing
// `request.onerror` was checked, and this still passes. What it does pin is
// that a request which never succeeds settles the promise rather than
// leaving it pending forever -- the failure mode that resolving on
// `oncomplete` would introduce if the abort rejection went missing.
it("rejects rather than hanging when the abort lands mid-flight", async () => {
abortDuringNextPut();
await expect(
putDocument("abandoned-preset", { name: "three" })
).rejects.toThrow();
});
});
describe("updateDocument", () => {
it("rejects when the transaction aborts after its put succeeds", async () => {
abortAfterNextPut();
await expect(
updateDocument<number[]>("aborted-counter", (current) => [
...(current ?? []),
1,
])
).rejects.toThrow();
});
});