@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.
@@ -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
- // Pre-process row elements to handle JSON objects with marker-colliding keys
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 = new TextDecoder().decode(buffer);
30
- return JSON.parse(json, reviver);
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 safe = wrapJsonIfNeeded(value);
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 = new TextDecoder().decode(buffer);
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
- * Wraps a JSON object in a $json marker if it contains keys that
49
- * would collide with our bigint/blob markers during deserialization.
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 wrapJsonIfNeeded(value) {
52
- if (typeof value !== 'object' || value === null || value instanceof Uint8Array) {
53
- return value;
54
- }
55
- // It's a JSON object or array stored as a SqlValue
56
- if (!Array.isArray(value) && (BIGINT_MARKER in value || BLOB_MARKER in value)) {
57
- return { [JSON_MARKER]: value };
58
- }
59
- return value;
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
- // Unwrap $json marker first (before checking for $bigint/$blob)
80
- if (JSON_MARKER in obj && Object.keys(obj).length === 1) {
81
- return obj[JSON_MARKER];
82
- }
83
- if (BIGINT_MARKER in obj && typeof obj[BIGINT_MARKER] === 'string') {
84
- return BigInt(obj[BIGINT_MARKER]);
85
- }
86
- if (BLOB_MARKER in obj && typeof obj[BLOB_MARKER] === 'string') {
87
- return base64ToUint8Array(obj[BLOB_MARKER]);
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(new TextDecoder().decode(buffer));
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":"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"}
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;;;;;;;;GAQG;AAEH,OAAO,EAAE,qBAAqB,EAAE,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAE,wBAAwB,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC"}
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;;;;;;;;GAQG;AAEH,OAAO,EAAE,qBAAqB,EAAkB,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAE,wBAAwB,EAA0B,MAAM,8BAA8B,CAAC"}
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 three interchangeable
9
- * stores (in-memory, LevelDB, IndexedDB).
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,EAAkB,MAAM,uBAAuB,CAAC;AAarE;;;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;CAC1B;AAkED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,GAAG,IAAI,CA+XtF"}
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 three interchangeable
9
- * stores (in-memory, LevelDB, IndexedDB).
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
- const supportsReopen = typeof makeBackend().reopen === 'function';
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), b(i & 0xff));
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