@quereus/store 4.9.0 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/dist/src/common/cached-kv-store.d.ts.map +1 -1
- package/dist/src/common/cached-kv-store.js +21 -6
- package/dist/src/common/cached-kv-store.js.map +1 -1
- package/dist/src/common/index.d.ts +1 -0
- package/dist/src/common/index.d.ts.map +1 -1
- package/dist/src/common/index.js +2 -0
- package/dist/src/common/index.js.map +1 -1
- package/dist/src/common/kv-store.d.ts +53 -1
- package/dist/src/common/kv-store.d.ts.map +1 -1
- package/dist/src/common/memory-store.d.ts.map +1 -1
- package/dist/src/common/memory-store.js +9 -1
- package/dist/src/common/memory-store.js.map +1 -1
- package/dist/src/common/paged-iterate.d.ts +42 -0
- package/dist/src/common/paged-iterate.d.ts.map +1 -0
- package/dist/src/common/paged-iterate.js +113 -0
- package/dist/src/common/paged-iterate.js.map +1 -0
- package/dist/src/common/serialization.d.ts +11 -1
- package/dist/src/common/serialization.d.ts.map +1 -1
- package/dist/src/common/serialization.js +114 -31
- package/dist/src/common/serialization.js.map +1 -1
- package/dist/src/testing/index.d.ts +5 -1
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +5 -1
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/kv-conformance.d.ts +52 -3
- package/dist/src/testing/kv-conformance.d.ts.map +1 -1
- package/dist/src/testing/kv-conformance.js +152 -7
- package/dist/src/testing/kv-conformance.js.map +1 -1
- package/package.json +5 -5
|
@@ -4,62 +4,134 @@
|
|
|
4
4
|
* Uses extended JSON format that preserves SQL value types:
|
|
5
5
|
* - bigint: { "$bigint": "12345678901234567890" }
|
|
6
6
|
* - Uint8Array: { "$blob": "base64..." }
|
|
7
|
-
* - JSON objects with marker-colliding keys: { "$json": { "$bigint": "not-a-bigint" } }
|
|
8
7
|
* - Other types: Native JSON representation
|
|
8
|
+
*
|
|
9
|
+
* JSON/object values whose own keys collide with a marker name are escaped on
|
|
10
|
+
* write by prefixing one extra `$` (`$bigint` -> `$$bigint`, `$$blob` ->
|
|
11
|
+
* `$$$blob`) and unescaped on read. Escaping happens per key at any depth, so
|
|
12
|
+
* a marker-shaped object can never be confused with a real marker no matter
|
|
13
|
+
* how deeply it is nested.
|
|
14
|
+
*
|
|
15
|
+
* Escaping (not wrapping) is required because `JSON.parse` invokes the reviver
|
|
16
|
+
* bottom-up: an enclosing `{"$json": ...}` wrapper is visited *after* the value
|
|
17
|
+
* it was meant to protect, so the inner object would already have been decoded
|
|
18
|
+
* (or thrown) before the wrapper could suppress it.
|
|
9
19
|
*/
|
|
10
20
|
const BIGINT_MARKER = '$bigint';
|
|
11
21
|
const BLOB_MARKER = '$blob';
|
|
22
|
+
/** Legacy write-side wrapper; still unwrapped on read for rows written before key escaping. */
|
|
12
23
|
const JSON_MARKER = '$json';
|
|
24
|
+
/** A user key that would collide with a marker, or with an escaped marker key. */
|
|
25
|
+
const COLLIDING_KEY = /^\$+(?:bigint|blob|json)$/;
|
|
26
|
+
/** An escaped user key produced by `escapeKey` — one `$` more than a colliding key. */
|
|
27
|
+
const ESCAPED_KEY = /^\$\$+(?:bigint|blob|json)$/;
|
|
28
|
+
const DOLLAR_CHAR_CODE = 36;
|
|
13
29
|
// TextEncoder is stateless and reusable — hoist one instance rather than
|
|
14
30
|
// allocating a fresh encoder on every serialize call.
|
|
15
31
|
const textEncoder = new TextEncoder();
|
|
32
|
+
// TextDecoder is likewise stateless per non-streaming decode() call, so one
|
|
33
|
+
// instance can be shared safely across concurrent scans.
|
|
34
|
+
const decoder = new TextDecoder();
|
|
35
|
+
/**
|
|
36
|
+
* True if the decoded JSON text could need reviving — either a marker object
|
|
37
|
+
* emitted by `replacer` or a key escaped by `escapeMarkerKeys`.
|
|
38
|
+
*
|
|
39
|
+
* Markers are single-key objects, so they always serialize with the literal
|
|
40
|
+
* `{"$` sigil. Escaped keys always start with `$$` but may sit at any position
|
|
41
|
+
* in their object, so they only guarantee the `"$$` sigil. Both are preceded by
|
|
42
|
+
* a quote, so a single `"$` scan rules out the overwhelmingly common case in
|
|
43
|
+
* one pass; the narrower checks then run only for text that already contains a
|
|
44
|
+
* quoted `$`, keeping ordinary values like `"$125.00 fee"` off the slow path.
|
|
45
|
+
*
|
|
46
|
+
* Centralized so a future fourth marker type can't add a new marker shape
|
|
47
|
+
* while forgetting this gate.
|
|
48
|
+
*
|
|
49
|
+
* NOTE: a row that does contain a marker (any bigint or blob column) is scanned
|
|
50
|
+
* up to three times before its reviver runs. Measured neutral against the
|
|
51
|
+
* unconditional-reviver baseline on such rows; if very wide rows or large blobs
|
|
52
|
+
* ever make these scans show up in a profile, fold the three passes into one
|
|
53
|
+
* hand-rolled character scan.
|
|
54
|
+
*/
|
|
55
|
+
function needsReviver(json) {
|
|
56
|
+
if (!json.includes('"$'))
|
|
57
|
+
return false;
|
|
58
|
+
return json.includes('{"$') || json.includes('"$$');
|
|
59
|
+
}
|
|
16
60
|
/**
|
|
17
61
|
* Serialize a row to a byte array for storage.
|
|
18
62
|
*/
|
|
19
63
|
export function serializeRow(row) {
|
|
20
|
-
|
|
21
|
-
const safeRow = row.map(wrapJsonIfNeeded);
|
|
22
|
-
const json = JSON.stringify(safeRow, replacer);
|
|
64
|
+
const json = JSON.stringify(row, replacer);
|
|
23
65
|
return textEncoder.encode(json);
|
|
24
66
|
}
|
|
25
67
|
/**
|
|
26
68
|
* Deserialize a byte array back to a row.
|
|
27
69
|
*/
|
|
28
70
|
export function deserializeRow(buffer) {
|
|
29
|
-
const json =
|
|
30
|
-
|
|
71
|
+
const json = decoder.decode(buffer);
|
|
72
|
+
// NOTE: accepted tradeoff — the reviver gate infers from the decoded text rather than a
|
|
73
|
+
// write-time flag byte. A flag would save ~0.11 µs/row over this scan (measured) but needs
|
|
74
|
+
// a stored-format version + migration; not worth it on its own. Revisit only if the row
|
|
75
|
+
// codec is being opened anyway — a binary/columnar format subsumes this entirely.
|
|
76
|
+
return JSON.parse(json, needsReviver(json) ? reviver : undefined);
|
|
31
77
|
}
|
|
32
78
|
/**
|
|
33
79
|
* Serialize a single SQL value to a byte array.
|
|
34
80
|
*/
|
|
35
81
|
export function serializeValue(value) {
|
|
36
|
-
const
|
|
37
|
-
const json = JSON.stringify(safe, replacer);
|
|
82
|
+
const json = JSON.stringify(value, replacer);
|
|
38
83
|
return textEncoder.encode(json);
|
|
39
84
|
}
|
|
40
85
|
/**
|
|
41
86
|
* Deserialize a byte array back to a SQL value.
|
|
42
87
|
*/
|
|
43
88
|
export function deserializeValue(buffer) {
|
|
44
|
-
const json =
|
|
45
|
-
return JSON.parse(json, reviver);
|
|
89
|
+
const json = decoder.decode(buffer);
|
|
90
|
+
return JSON.parse(json, needsReviver(json) ? reviver : undefined);
|
|
91
|
+
}
|
|
92
|
+
/** True if `key` needs escaping (or unescaping) — cheap `$` test before the regex. */
|
|
93
|
+
function isCollidingKey(key) {
|
|
94
|
+
return key.charCodeAt(0) === DOLLAR_CHAR_CODE && COLLIDING_KEY.test(key);
|
|
95
|
+
}
|
|
96
|
+
/** True if `key` was produced by escaping a colliding key. */
|
|
97
|
+
function isEscapedKey(key) {
|
|
98
|
+
return key.charCodeAt(0) === DOLLAR_CHAR_CODE && ESCAPED_KEY.test(key);
|
|
46
99
|
}
|
|
47
100
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
101
|
+
* Rebuilds `obj` with each own key passed through `rename`.
|
|
102
|
+
*
|
|
103
|
+
* Built via `Object.fromEntries` rather than assignment because a JSON value
|
|
104
|
+
* may legitimately carry an own `__proto__` key; `copy[key] = value` would
|
|
105
|
+
* invoke the prototype setter, silently dropping the key and re-pointing the
|
|
106
|
+
* object's prototype. `fromEntries` defines own properties instead.
|
|
50
107
|
*/
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
108
|
+
function rekey(obj, rename) {
|
|
109
|
+
return Object.fromEntries(Object.keys(obj).map(key => [rename(key), obj[key]]));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Rewrites marker-colliding keys of a plain object so no user data can ever
|
|
113
|
+
* present the shape of a marker. Returns the original object untouched when
|
|
114
|
+
* nothing collides, so the common case allocates nothing.
|
|
115
|
+
*
|
|
116
|
+
* Only the object's own keys are rewritten — nested objects are reached by
|
|
117
|
+
* `JSON.stringify`, which applies `replacer` top-down to every member.
|
|
118
|
+
*/
|
|
119
|
+
function escapeMarkerKeys(obj) {
|
|
120
|
+
if (!Object.keys(obj).some(isCollidingKey))
|
|
121
|
+
return obj;
|
|
122
|
+
return rekey(obj, key => (isCollidingKey(key) ? `$${key}` : key));
|
|
123
|
+
}
|
|
124
|
+
/** Inverse of `escapeMarkerKeys`; likewise returns the original object when nothing was escaped. */
|
|
125
|
+
function unescapeMarkerKeys(obj) {
|
|
126
|
+
if (!Object.keys(obj).some(isEscapedKey))
|
|
127
|
+
return obj;
|
|
128
|
+
return rekey(obj, key => (isEscapedKey(key) ? key.slice(1) : key));
|
|
60
129
|
}
|
|
61
130
|
/**
|
|
62
131
|
* JSON replacer function for SQL values.
|
|
132
|
+
*
|
|
133
|
+
* `JSON.stringify` calls this top-down for every member, which is what lets the
|
|
134
|
+
* key escaping reach arbitrary nesting depth without a separate pre-walk.
|
|
63
135
|
*/
|
|
64
136
|
function replacer(_key, value) {
|
|
65
137
|
if (typeof value === 'bigint') {
|
|
@@ -68,24 +140,35 @@ function replacer(_key, value) {
|
|
|
68
140
|
if (value instanceof Uint8Array) {
|
|
69
141
|
return { [BLOB_MARKER]: uint8ArrayToBase64(value) };
|
|
70
142
|
}
|
|
143
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
144
|
+
return escapeMarkerKeys(value);
|
|
145
|
+
}
|
|
71
146
|
return value;
|
|
72
147
|
}
|
|
73
148
|
/**
|
|
74
149
|
* JSON reviver function for SQL values.
|
|
150
|
+
*
|
|
151
|
+
* Markers are matched only in their exact single-key form; escaping guarantees
|
|
152
|
+
* that shape is unreachable from user data, so no ambiguity remains.
|
|
75
153
|
*/
|
|
76
154
|
function reviver(_key, value) {
|
|
77
155
|
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
78
156
|
const obj = value;
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
157
|
+
const keys = Object.keys(obj);
|
|
158
|
+
if (keys.length === 1) {
|
|
159
|
+
const [key] = keys;
|
|
160
|
+
if (key === BIGINT_MARKER && typeof obj[BIGINT_MARKER] === 'string') {
|
|
161
|
+
return BigInt(obj[BIGINT_MARKER]);
|
|
162
|
+
}
|
|
163
|
+
if (key === BLOB_MARKER && typeof obj[BLOB_MARKER] === 'string') {
|
|
164
|
+
return base64ToUint8Array(obj[BLOB_MARKER]);
|
|
165
|
+
}
|
|
166
|
+
// Legacy wrapper from before key escaping; user data now escapes `$json`.
|
|
167
|
+
if (key === JSON_MARKER) {
|
|
168
|
+
return obj[JSON_MARKER];
|
|
169
|
+
}
|
|
88
170
|
}
|
|
171
|
+
return unescapeMarkerKeys(obj);
|
|
89
172
|
}
|
|
90
173
|
return value;
|
|
91
174
|
}
|
|
@@ -132,6 +215,6 @@ export function serializeStats(stats) {
|
|
|
132
215
|
* Deserialize table statistics.
|
|
133
216
|
*/
|
|
134
217
|
export function deserializeStats(buffer) {
|
|
135
|
-
return JSON.parse(
|
|
218
|
+
return JSON.parse(decoder.decode(buffer));
|
|
136
219
|
}
|
|
137
220
|
//# sourceMappingURL=serialization.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialization.js","sourceRoot":"","sources":["../../../src/common/serialization.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"serialization.js","sourceRoot":"","sources":["../../../src/common/serialization.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* - {@link runKVStoreConformance} — the {@link KVStore} contract (all backends);
|
|
7
7
|
* - {@link runKVProviderConformance} — the provider-level atomic multi-store commit
|
|
8
8
|
* ({@link KVStoreProvider.beginAtomicBatch}), for the backends that have one.
|
|
9
|
+
*
|
|
10
|
+
* {@link assertBoundedIterate} is the bounded-iteration tier's core assertion, exported
|
|
11
|
+
* standalone so a spec can drive it directly (including against a store double built to
|
|
12
|
+
* fail it — a guard nobody has watched fail is not a guard).
|
|
9
13
|
*/
|
|
10
|
-
export { runKVStoreConformance, type KVBackend } from './kv-conformance.js';
|
|
14
|
+
export { runKVStoreConformance, assertBoundedIterate, type KVBackend, type ReadMeter } from './kv-conformance.js';
|
|
11
15
|
export { runKVProviderConformance, type KVProviderBackend } from './kv-provider-conformance.js';
|
|
12
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAClH,OAAO,EAAE,wBAAwB,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* - {@link runKVStoreConformance} — the {@link KVStore} contract (all backends);
|
|
7
7
|
* - {@link runKVProviderConformance} — the provider-level atomic multi-store commit
|
|
8
8
|
* ({@link KVStoreProvider.beginAtomicBatch}), for the backends that have one.
|
|
9
|
+
*
|
|
10
|
+
* {@link assertBoundedIterate} is the bounded-iteration tier's core assertion, exported
|
|
11
|
+
* standalone so a spec can drive it directly (including against a store double built to
|
|
12
|
+
* fail it — a guard nobody has watched fail is not a guard).
|
|
9
13
|
*/
|
|
10
|
-
export { runKVStoreConformance } from './kv-conformance.js';
|
|
14
|
+
export { runKVStoreConformance, assertBoundedIterate } from './kv-conformance.js';
|
|
11
15
|
export { runKVProviderConformance } from './kv-provider-conformance.js';
|
|
12
16
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAkC,MAAM,qBAAqB,CAAC;AAClH,OAAO,EAAE,wBAAwB,EAA0B,MAAM,8BAA8B,CAAC"}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* contract (not against any single backend), invoked once per backend. Each backend
|
|
6
6
|
* supplies only a small {@link KVBackend} adapter (how to open / reopen / tear down);
|
|
7
7
|
* this file supplies every assertion. A behavior that drifts on one backend then
|
|
8
|
-
* fails the suite instead of silently diverging across the
|
|
9
|
-
*
|
|
8
|
+
* fails the suite instead of silently diverging across the interchangeable stores
|
|
9
|
+
* (in-memory, LevelDB, IndexedDB, React Native LevelDB, NativeScript SQLite).
|
|
10
10
|
*
|
|
11
11
|
* Deps are deliberately minimal so this file can be compiled into `@quereus/store`'s
|
|
12
12
|
* normal `src` build without dragging a test framework into the shipped package:
|
|
@@ -21,7 +21,29 @@
|
|
|
21
21
|
* store, so the memory backend is tested against the contract as honestly as the
|
|
22
22
|
* others.
|
|
23
23
|
*/
|
|
24
|
-
import type { KVStore } from '../common/kv-store.js';
|
|
24
|
+
import type { KVStore, IterateOptions } from '../common/kv-store.js';
|
|
25
|
+
/**
|
|
26
|
+
* Read instrumentation for the bounded-iteration tier.
|
|
27
|
+
*
|
|
28
|
+
* The battery only ever holds a {@link KVStore}; it cannot see what a backend reads
|
|
29
|
+
* underneath. A backend supplies this by wrapping whatever its store reads FROM — the
|
|
30
|
+
* level handle, the SQL driver, the IDB cursor — in its own adapter's closure.
|
|
31
|
+
*/
|
|
32
|
+
export interface ReadMeter {
|
|
33
|
+
/**
|
|
34
|
+
* Entries read from backing storage so far — monotonically increasing over the
|
|
35
|
+
* adapter's lifetime. Callers take a baseline and measure the delta, so this never
|
|
36
|
+
* needs an explicit reset.
|
|
37
|
+
*/
|
|
38
|
+
entriesRead(): number;
|
|
39
|
+
/**
|
|
40
|
+
* How many entries this backend may read ahead of one yield — its batch size.
|
|
41
|
+
* `abstract-level` awaits `next()` once per entry, so 1; IndexedDB pages 256.
|
|
42
|
+
* Count what the meter actually observes, including any extra probe read the
|
|
43
|
+
* implementation takes to discover a batch is full.
|
|
44
|
+
*/
|
|
45
|
+
maxReadAhead: number;
|
|
46
|
+
}
|
|
25
47
|
/**
|
|
26
48
|
* Per-backend lifecycle adapter. The suite drives its own per-test lifecycle and
|
|
27
49
|
* calls {@link makeBackend} fresh for every test, so state never leaks between tests.
|
|
@@ -37,7 +59,34 @@ export interface KVBackend {
|
|
|
37
59
|
reopen?(): Promise<KVStore>;
|
|
38
60
|
/** Release everything open()/reopen() created (close handles, rm temp dir / delete db). */
|
|
39
61
|
teardown(): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Optional read instrumentation for the bounded-iteration tier. Omit it and that
|
|
64
|
+
* tier's METERED cases are not registered for this backend (its unmetered cases —
|
|
65
|
+
* break and throw mid-iteration — still run). See {@link ReadMeter}.
|
|
66
|
+
*/
|
|
67
|
+
readMeter?: ReadMeter;
|
|
40
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Assert that `store.iterate(options)` reads a BOUNDED amount from backing storage:
|
|
71
|
+
* consume `take` entries, then stop, and check the meter.
|
|
72
|
+
*
|
|
73
|
+
* The allowance is `take + maxReadAhead - 1`: a backend that reads one entry per yield
|
|
74
|
+
* reads exactly `take`, and one that pages reads at most `take` rounded up past a batch
|
|
75
|
+
* boundary. Crucially the allowance does NOT grow with the size of the range, so a
|
|
76
|
+
* backend that materializes the whole range fails however large the range is — which is
|
|
77
|
+
* the property the bounded-iteration tier exists to pin. Passing `take` = the whole range
|
|
78
|
+
* pins the other half of the contract, that total work is linear: a backend paging by
|
|
79
|
+
* `limit`/`offset` reads a bounded FIRST batch but O(n²) overall.
|
|
80
|
+
*
|
|
81
|
+
* Exported standalone so a spec can drive it against a deliberately-buffering store
|
|
82
|
+
* double and prove it rejects (see `bounded-iterate.spec.ts`).
|
|
83
|
+
*
|
|
84
|
+
* @param store - The store under test.
|
|
85
|
+
* @param meter - Read instrumentation for whatever `store` reads from.
|
|
86
|
+
* @param options - Iterate options. The range must hold at least `take` entries.
|
|
87
|
+
* @param take - Entries to consume before stopping. Default 1 (prefix-and-stop).
|
|
88
|
+
*/
|
|
89
|
+
export declare function assertBoundedIterate(store: KVStore, meter: ReadMeter, options?: IterateOptions, take?: number): Promise<void>;
|
|
41
90
|
/**
|
|
42
91
|
* Register the full KVStore conformance battery under `describe(name)`. Call once per
|
|
43
92
|
* backend. `makeBackend` is invoked fresh for every test (so no state leaks); it is
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kv-conformance.d.ts","sourceRoot":"","sources":["../../../src/testing/kv-conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"kv-conformance.d.ts","sourceRoot":"","sources":["../../../src/testing/kv-conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAarE;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACzB;;;;OAIG;IACH,WAAW,IAAI,MAAM,CAAC;IACtB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACzB,+EAA+E;IAC/E,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE,MAAU,GACd,OAAO,CAAC,IAAI,CAAC,CAqBf;AAuED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,GAAG,IAAI,CAgftF"}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* contract (not against any single backend), invoked once per backend. Each backend
|
|
6
6
|
* supplies only a small {@link KVBackend} adapter (how to open / reopen / tear down);
|
|
7
7
|
* this file supplies every assertion. A behavior that drifts on one backend then
|
|
8
|
-
* fails the suite instead of silently diverging across the
|
|
9
|
-
*
|
|
8
|
+
* fails the suite instead of silently diverging across the interchangeable stores
|
|
9
|
+
* (in-memory, LevelDB, IndexedDB, React Native LevelDB, NativeScript SQLite).
|
|
10
10
|
*
|
|
11
11
|
* Deps are deliberately minimal so this file can be compiled into `@quereus/store`'s
|
|
12
12
|
* normal `src` build without dragging a test framework into the shipped package:
|
|
@@ -25,6 +25,43 @@ import assert from 'node:assert/strict';
|
|
|
25
25
|
import { compareBytes } from '../common/bytes.js';
|
|
26
26
|
import { encodeCompositeKey } from '../common/encoding.js';
|
|
27
27
|
import { assertBytes, b, u8 } from './kv-assert.js';
|
|
28
|
+
/**
|
|
29
|
+
* Assert that `store.iterate(options)` reads a BOUNDED amount from backing storage:
|
|
30
|
+
* consume `take` entries, then stop, and check the meter.
|
|
31
|
+
*
|
|
32
|
+
* The allowance is `take + maxReadAhead - 1`: a backend that reads one entry per yield
|
|
33
|
+
* reads exactly `take`, and one that pages reads at most `take` rounded up past a batch
|
|
34
|
+
* boundary. Crucially the allowance does NOT grow with the size of the range, so a
|
|
35
|
+
* backend that materializes the whole range fails however large the range is — which is
|
|
36
|
+
* the property the bounded-iteration tier exists to pin. Passing `take` = the whole range
|
|
37
|
+
* pins the other half of the contract, that total work is linear: a backend paging by
|
|
38
|
+
* `limit`/`offset` reads a bounded FIRST batch but O(n²) overall.
|
|
39
|
+
*
|
|
40
|
+
* Exported standalone so a spec can drive it against a deliberately-buffering store
|
|
41
|
+
* double and prove it rejects (see `bounded-iterate.spec.ts`).
|
|
42
|
+
*
|
|
43
|
+
* @param store - The store under test.
|
|
44
|
+
* @param meter - Read instrumentation for whatever `store` reads from.
|
|
45
|
+
* @param options - Iterate options. The range must hold at least `take` entries.
|
|
46
|
+
* @param take - Entries to consume before stopping. Default 1 (prefix-and-stop).
|
|
47
|
+
*/
|
|
48
|
+
export async function assertBoundedIterate(store, meter, options, take = 1) {
|
|
49
|
+
assert.ok(take >= 1, `assertBoundedIterate: take must be >= 1, got ${take}`);
|
|
50
|
+
const baseline = meter.entriesRead();
|
|
51
|
+
let consumed = 0;
|
|
52
|
+
for await (const _entry of store.iterate(options)) {
|
|
53
|
+
if (++consumed >= take)
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const read = meter.entriesRead() - baseline;
|
|
57
|
+
// Guards against a vacuous pass: a store that yields nothing reads nothing, and a
|
|
58
|
+
// meter wired to the wrong thing reports zero however the store behaves.
|
|
59
|
+
assert.strictEqual(consumed, take, `expected to consume ${take} entries before stopping, got ${consumed} — the range is too small to measure`);
|
|
60
|
+
assert.ok(read >= 1, 'the read meter counted nothing — it is not wired to what this store reads from');
|
|
61
|
+
const allowed = take + meter.maxReadAhead - 1;
|
|
62
|
+
assert.ok(read <= allowed, `bounded iteration: consuming ${take} entr${take === 1 ? 'y' : 'ies'} read ${read} from backing storage, ` +
|
|
63
|
+
`allowance is ${allowed} (take + maxReadAhead - 1, maxReadAhead=${meter.maxReadAhead})`);
|
|
64
|
+
}
|
|
28
65
|
// ============================================================================
|
|
29
66
|
// Assertion + iteration helpers
|
|
30
67
|
// ============================================================================
|
|
@@ -44,6 +81,10 @@ async function seed1to5(store) {
|
|
|
44
81
|
for (let i = 1; i <= 5; i++)
|
|
45
82
|
await store.put(b(i), b(i * 10));
|
|
46
83
|
}
|
|
84
|
+
/** 2-byte big-endian key for the large-range tiers (0..65535) — byte order matches key order. */
|
|
85
|
+
const enc = (n) => b((n >> 8) & 0xff, n & 0xff);
|
|
86
|
+
/** Inverse of {@link enc}. */
|
|
87
|
+
const dec = (k) => (k[0] << 8) | k[1];
|
|
47
88
|
// ============================================================================
|
|
48
89
|
// Tier 6 golden vector — cross-backend byte-ordering agreement
|
|
49
90
|
// ============================================================================
|
|
@@ -88,8 +129,11 @@ const key1 = (v) => encodeCompositeKey([v]);
|
|
|
88
129
|
*/
|
|
89
130
|
export function runKVStoreConformance(name, makeBackend) {
|
|
90
131
|
// Probe (no open()/teardown() — the factory only builds the adapter object) to
|
|
91
|
-
// learn whether this backend persists across a reopen
|
|
92
|
-
|
|
132
|
+
// learn whether this backend persists across a reopen, and whether it can report
|
|
133
|
+
// what it reads from backing storage.
|
|
134
|
+
const probe = makeBackend();
|
|
135
|
+
const supportsReopen = typeof probe.reopen === 'function';
|
|
136
|
+
const supportsReadMeter = probe.readMeter !== undefined;
|
|
93
137
|
describe(name, () => {
|
|
94
138
|
let backend;
|
|
95
139
|
let store;
|
|
@@ -249,12 +293,13 @@ export function runKVStoreConformance(name, makeBackend) {
|
|
|
249
293
|
// ------------------------------------------------------------------
|
|
250
294
|
describe('tier 3: streaming iteration across a batch boundary', () => {
|
|
251
295
|
const COUNT = 306; // > 256 so iteration crosses at least one IndexedDB page boundary
|
|
252
|
-
const enc = (n) => b((n >> 8) & 0xff, n & 0xff); // 2-byte big-endian
|
|
253
|
-
const dec = (k) => (k[0] << 8) | k[1];
|
|
254
296
|
beforeEach(async () => {
|
|
297
|
+
// Value = key. Values must be UNIQUE across the whole fixture (not `i & 0xff`,
|
|
298
|
+
// which repeats every 256 entries) so the pairing case below can catch a
|
|
299
|
+
// backend that misaligns keys against values by a whole page.
|
|
255
300
|
const batch = store.batch();
|
|
256
301
|
for (let i = 0; i < COUNT; i++)
|
|
257
|
-
batch.put(enc(i),
|
|
302
|
+
batch.put(enc(i), enc(i));
|
|
258
303
|
await batch.write();
|
|
259
304
|
});
|
|
260
305
|
it('streams every entry once, ascending, while awaiting an unrelated get each step', async () => {
|
|
@@ -269,6 +314,36 @@ export function runKVStoreConformance(name, makeBackend) {
|
|
|
269
314
|
for (let i = 0; i < COUNT; i++)
|
|
270
315
|
assert.strictEqual(seen[i], i, `entry ${i}`);
|
|
271
316
|
});
|
|
317
|
+
it('pairs every value with its own key across the batch boundary', async () => {
|
|
318
|
+
// A backend that reads keys and values as two separate bulk requests (IndexedDB's
|
|
319
|
+
// getAllKeys + getAll) could zip them out of step and hand back real-looking rows
|
|
320
|
+
// filed under the wrong keys. Values are unique here, so any misalignment shows.
|
|
321
|
+
let seen = 0;
|
|
322
|
+
for await (const entry of store.iterate()) {
|
|
323
|
+
assert.strictEqual(dec(entry.value), dec(entry.key), `entry ${dec(entry.key)} came back carrying value ${dec(entry.value)}`);
|
|
324
|
+
seen++;
|
|
325
|
+
}
|
|
326
|
+
assert.strictEqual(seen, COUNT);
|
|
327
|
+
});
|
|
328
|
+
it('mutating yielded keys/values mid-iteration does not perturb resumption', async () => {
|
|
329
|
+
// A paged backend derives the next page's resume edge from the last key of this
|
|
330
|
+
// page. If it captures that edge AFTER handing the entry to the consumer, a
|
|
331
|
+
// consumer scribbling on the key moves where the next page starts — skipping or
|
|
332
|
+
// repeating entries. Tier 2 pins the single-page version of the read-buffer
|
|
333
|
+
// contract; this is the version that crosses a page boundary.
|
|
334
|
+
const seen = [];
|
|
335
|
+
for await (const entry of store.iterate()) {
|
|
336
|
+
seen.push(dec(entry.key));
|
|
337
|
+
entry.key.fill(0xff);
|
|
338
|
+
entry.value.fill(0xff);
|
|
339
|
+
}
|
|
340
|
+
assert.strictEqual(seen.length, COUNT, 'every entry must be seen exactly once');
|
|
341
|
+
for (let i = 0; i < COUNT; i++)
|
|
342
|
+
assert.strictEqual(seen[i], i, `entry ${i}`);
|
|
343
|
+
// ...and the scribbles must not have reached the store's own buffers.
|
|
344
|
+
assertBytes(await store.get(enc(0)), enc(0));
|
|
345
|
+
assertBytes(await store.get(enc(COUNT - 1)), enc(COUNT - 1));
|
|
346
|
+
});
|
|
272
347
|
it('honors reverse across the batch boundary', async () => {
|
|
273
348
|
const seen = [];
|
|
274
349
|
for await (const entry of store.iterate({ reverse: true }))
|
|
@@ -431,6 +506,76 @@ export function runKVStoreConformance(name, makeBackend) {
|
|
|
431
506
|
assertBytes(await store.get(key1(5n)), b(2));
|
|
432
507
|
});
|
|
433
508
|
});
|
|
509
|
+
// ------------------------------------------------------------------
|
|
510
|
+
// Tier 7 — bounded iteration (see KVStore.iterate's contract)
|
|
511
|
+
//
|
|
512
|
+
// The unmetered cases run everywhere: they pin that abandoning an iteration
|
|
513
|
+
// releases whatever the backend held. The metered cases need the adapter to
|
|
514
|
+
// supply a ReadMeter, because a KVStore handle cannot see its own reads.
|
|
515
|
+
// ------------------------------------------------------------------
|
|
516
|
+
describe('tier 7: bounded iteration', () => {
|
|
517
|
+
it('breaking out mid-iteration leaves the store usable and closeable', async () => {
|
|
518
|
+
await seed1to5(store);
|
|
519
|
+
let seen = 0;
|
|
520
|
+
for await (const _entry of store.iterate()) {
|
|
521
|
+
seen++;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
assert.strictEqual(seen, 1);
|
|
525
|
+
// An iterator/transaction left open by the abandoned scan would wedge these.
|
|
526
|
+
assertBytes(await store.get(b(3)), b(30));
|
|
527
|
+
assert.deepStrictEqual(await keysOf(store), [b(1), b(2), b(3), b(4), b(5)]);
|
|
528
|
+
await store.close(); // must resolve, not hang on an unreleased handle
|
|
529
|
+
});
|
|
530
|
+
it('an exception thrown from the loop body propagates and leaves the store usable', async () => {
|
|
531
|
+
await seed1to5(store);
|
|
532
|
+
await assert.rejects(async () => {
|
|
533
|
+
for await (const _entry of store.iterate()) {
|
|
534
|
+
throw new Error('consumer boom');
|
|
535
|
+
}
|
|
536
|
+
}, /consumer boom/);
|
|
537
|
+
assertBytes(await store.get(b(3)), b(30));
|
|
538
|
+
assert.deepStrictEqual(await keysOf(store), [b(1), b(2), b(3), b(4), b(5)]);
|
|
539
|
+
});
|
|
540
|
+
if (supportsReadMeter) {
|
|
541
|
+
describe('metered: reads stay bounded regardless of range size', () => {
|
|
542
|
+
let meter;
|
|
543
|
+
let count;
|
|
544
|
+
beforeEach(async () => {
|
|
545
|
+
meter = backend.readMeter;
|
|
546
|
+
// Big enough that a backend materializing the range blows the allowance
|
|
547
|
+
// by orders of magnitude, whatever its batch size is. Several batches
|
|
548
|
+
// deep, so the full-drain case below actually exercises resumption.
|
|
549
|
+
count = Math.max(3 * meter.maxReadAhead, 512);
|
|
550
|
+
const batch = store.batch();
|
|
551
|
+
for (let i = 0; i < count; i++)
|
|
552
|
+
batch.put(enc(i), b(i & 0xff));
|
|
553
|
+
await batch.write();
|
|
554
|
+
});
|
|
555
|
+
it('an unbounded iterate stopped after one entry reads at most one batch', async () => {
|
|
556
|
+
await assertBoundedIterate(store, meter);
|
|
557
|
+
});
|
|
558
|
+
it('the same holds in reverse', async () => {
|
|
559
|
+
await assertBoundedIterate(store, meter, { reverse: true });
|
|
560
|
+
});
|
|
561
|
+
it('a small limit costs the limit, not the range', async () => {
|
|
562
|
+
// NOTE: on a backend whose batch is much larger than 5 this case adds
|
|
563
|
+
// little over the unbounded one — the allowance is dominated by
|
|
564
|
+
// maxReadAhead, so it cannot tell "pushed the limit down" from "read a
|
|
565
|
+
// whole batch". It is exact for a one-entry-per-yield backend. If a
|
|
566
|
+
// backend ever needs its limit pushdown pinned specifically, assert on
|
|
567
|
+
// a limit LARGER than its batch instead of adding slack here.
|
|
568
|
+
await assertBoundedIterate(store, meter, { limit: 5 }, 5);
|
|
569
|
+
});
|
|
570
|
+
it('draining the whole range costs about one read per entry', async () => {
|
|
571
|
+
// Prefix cases above cannot see a backend that pages by re-reading from
|
|
572
|
+
// the start and discarding a growing prefix (`limit`/`offset` paging):
|
|
573
|
+
// its FIRST batch is bounded, and only the total goes quadratic.
|
|
574
|
+
await assertBoundedIterate(store, meter, undefined, count);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
});
|
|
434
579
|
});
|
|
435
580
|
}
|
|
436
581
|
//# sourceMappingURL=kv-conformance.js.map
|