@minnowdb/core 0.7.0 → 0.7.1

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.
Files changed (95) hide show
  1. package/dist/block-format/block.js +254 -325
  2. package/dist/block-format/checksum.js +30 -52
  3. package/dist/block-format/codecs.js +112 -125
  4. package/dist/block-format/column.js +187 -205
  5. package/dist/block-format/index.js +5 -1
  6. package/dist/block-format/physical.js +371 -387
  7. package/dist/block-format/types.js +4 -1
  8. package/dist/block-format/unicode.js +30 -39
  9. package/dist/date-value.js +67 -78
  10. package/dist/engine/artifact-cache.js +106 -116
  11. package/dist/engine/batch.js +36 -44
  12. package/dist/engine/buffered-writer.js +156 -158
  13. package/dist/engine/byte-estimates.js +20 -22
  14. package/dist/engine/cache-limits.js +24 -25
  15. package/dist/engine/cancellation.js +5 -3
  16. package/dist/engine/catalog.js +64 -73
  17. package/dist/engine/client.js +715 -780
  18. package/dist/engine/database.js +15941 -19313
  19. package/dist/engine/defaults.js +104 -111
  20. package/dist/engine/errors.js +113 -128
  21. package/dist/engine/fts.js +233 -280
  22. package/dist/engine/group-index.js +287 -321
  23. package/dist/engine/index.js +23 -12
  24. package/dist/engine/join-index.js +177 -186
  25. package/dist/engine/keyed-live.js +168 -173
  26. package/dist/engine/live-api.js +0 -1
  27. package/dist/engine/live-equal.js +32 -37
  28. package/dist/engine/live.js +603 -647
  29. package/dist/engine/memory.js +109 -111
  30. package/dist/engine/optimizer.d.ts +19 -1
  31. package/dist/engine/optimizer.js +2633 -2934
  32. package/dist/engine/point-read.js +122 -182
  33. package/dist/engine/query-api.js +12 -3
  34. package/dist/engine/query-cache.js +61 -69
  35. package/dist/engine/query.d.ts +6 -0
  36. package/dist/engine/query.js +8503 -9980
  37. package/dist/engine/result-wire.js +221 -252
  38. package/dist/engine/schema-wire.js +97 -112
  39. package/dist/engine/schema.js +1099 -1254
  40. package/dist/engine/sort-keys.js +315 -389
  41. package/dist/engine/sql-domains.js +646 -769
  42. package/dist/engine/sql-driver.js +0 -1
  43. package/dist/engine/sql-functions.js +1031 -1125
  44. package/dist/engine/sql-json.js +199 -218
  45. package/dist/engine/sql-semantics.js +572 -639
  46. package/dist/engine/typed-live.js +271 -293
  47. package/dist/engine/vector.js +4484 -5145
  48. package/dist/engine/worker-host.d.ts +8 -63
  49. package/dist/engine/worker-host.js +28 -1130
  50. package/dist/engine/worker-indexeddb.d.ts +1 -0
  51. package/dist/engine/worker-indexeddb.js +3 -0
  52. package/dist/engine/worker-memory.d.ts +1 -0
  53. package/dist/engine/worker-memory.js +3 -0
  54. package/dist/engine/worker-opfs.d.ts +1 -0
  55. package/dist/engine/worker-opfs.js +3 -0
  56. package/dist/engine/worker-server.d.ts +93 -0
  57. package/dist/engine/worker-server.js +999 -0
  58. package/dist/engine/worker-store-indexeddb.d.ts +2 -0
  59. package/dist/engine/worker-store-indexeddb.js +11 -0
  60. package/dist/engine/worker-store-memory.d.ts +2 -0
  61. package/dist/engine/worker-store-memory.js +6 -0
  62. package/dist/engine/worker-store-opfs.d.ts +2 -0
  63. package/dist/engine/worker-store-opfs.js +9 -0
  64. package/dist/engine/worker.js +0 -12
  65. package/dist/engine/write-block-planner.js +93 -100
  66. package/dist/plan/index.js +15 -16
  67. package/dist/plan/model.d.ts +6 -0
  68. package/dist/plan/model.js +20 -27
  69. package/dist/storage/index.js +0 -12
  70. package/dist/storage/indexeddb.js +11776 -13171
  71. package/dist/storage/memory.js +1198 -1241
  72. package/dist/storage/opfs/files.js +238 -295
  73. package/dist/storage/opfs/index.js +9 -3
  74. package/dist/storage/opfs/leader.js +4386 -4856
  75. package/dist/storage/opfs/rpc.js +223 -259
  76. package/dist/storage/opfs/snapshot-ledger.js +219 -260
  77. package/dist/storage/opfs/store.js +884 -1003
  78. package/dist/storage/persistence.js +59 -74
  79. package/dist/storage/snapshot-stream.js +788 -826
  80. package/dist/storage/snapshot.js +36 -37
  81. package/dist/storage/toolkit/extents.js +444 -520
  82. package/dist/storage/toolkit/index.js +28 -29
  83. package/dist/storage/toolkit/record-core.js +5792 -6377
  84. package/dist/storage/toolkit/sync-file.js +34 -42
  85. package/dist/storage/toolkit/wal.js +92 -127
  86. package/dist/storage/toolkit/wire.js +232 -256
  87. package/dist/storage/types.js +2948 -3250
  88. package/dist/testing/block-store-conformance.js +1556 -1629
  89. package/dist/testing/index.js +310 -291
  90. package/dist/testing/opfs-shim.js +280 -312
  91. package/dist/testing/simulator.js +496 -534
  92. package/dist/testing/sqllogictest.js +425 -365
  93. package/dist/transactions/index.js +1464 -1712
  94. package/dist/worker-protocol/index.js +70 -69
  95. package/package.json +20 -2
@@ -3,592 +3,554 @@ import { MemoryBlockStore } from "../storage/index.js";
3
3
  import { FaultInjectingBlockStore } from "./index.js";
4
4
  import { md5Hex } from "./sqllogictest.js";
5
5
  async function collectStoragePages(read) {
6
- const records = [];
7
- let cursor = null;
8
- for (;;) {
9
- const page = await read(cursor);
10
- records.push(...page.records);
11
- if (page.nextCursor === null)
12
- return records;
13
- cursor = page.nextCursor;
14
- }
6
+ const records = [];
7
+ let cursor = null;
8
+ for (; ; ) {
9
+ const page = await read(cursor);
10
+ records.push(...page.records);
11
+ if (page.nextCursor === null)
12
+ return records;
13
+ cursor = page.nextCursor;
14
+ }
15
15
  }
16
- /** Chooses every storage completion from a seeded queue and keeps only a bounded trace tail. */
17
- export class DeterministicScheduler {
18
- #random;
19
- #traceLimit;
20
- #pending = [];
21
- #trace = [];
22
- #sequence = 0;
23
- #highWater = 0;
24
- constructor(seed, traceLimit = 256) {
25
- if (!Number.isSafeInteger(traceLimit) || traceLimit < 0) {
26
- throw new RangeError("Simulator trace limit must be a non-negative whole number");
27
- }
28
- this.#random = mulberry32(seed);
29
- this.#traceLimit = traceLimit;
30
- }
31
- get highWater() {
32
- return this.#highWater;
33
- }
34
- get trace() {
35
- return this.#trace;
16
+ class DeterministicScheduler {
17
+ #random;
18
+ #traceLimit;
19
+ #pending = [];
20
+ #trace = [];
21
+ #sequence = 0;
22
+ #highWater = 0;
23
+ constructor(seed, traceLimit = 256) {
24
+ if (!Number.isSafeInteger(traceLimit) || traceLimit < 0) {
25
+ throw new RangeError("Simulator trace limit must be a non-negative whole number");
36
26
  }
37
- schedule(label, run) {
38
- return new Promise((resolve, reject) => {
39
- this.#pending.push({ label, run, resolve, reject });
40
- this.#highWater = Math.max(this.#highWater, this.#pending.length);
41
- });
42
- }
43
- async settle(promises) {
44
- const state = { complete: false };
45
- const settled = Promise.allSettled(promises).then((results) => {
46
- state.complete = true;
47
- return results;
48
- });
49
- let emptyTurns = 0;
50
- while (!state.complete) {
51
- await Promise.resolve();
52
- const pending = this.#pending.length;
53
- if (pending === 0) {
54
- // Production maintenance deliberately yields through a timer between bounded pages.
55
- // A microtask-only poll can therefore declare deadlock while the next scheduled store
56
- // call is merely waiting for its event-loop turn. Let timers advance before counting an
57
- // empty turn; the chosen storage completion order remains seeded and deterministic.
58
- await nextEventLoopTurn();
59
- if (this.#pending.length > 0) {
60
- emptyTurns = 0;
61
- continue;
62
- }
63
- emptyTurns++;
64
- if (emptyTurns > 100) {
65
- throw new Error("Deterministic simulator deadlocked with no scheduled storage operation");
66
- }
67
- continue;
68
- }
69
- emptyTurns = 0;
70
- const index = Math.floor(this.#random() * pending);
71
- const operation = this.#pending.splice(index, 1)[0];
72
- if (operation === undefined)
73
- continue;
74
- try {
75
- const value = await operation.run();
76
- operation.resolve(value);
77
- this.#record(operation.label, "resolved");
78
- }
79
- catch (error) {
80
- operation.reject(error);
81
- this.#record(operation.label, "rejected");
82
- }
27
+ this.#random = mulberry32(seed);
28
+ this.#traceLimit = traceLimit;
29
+ }
30
+ get highWater() {
31
+ return this.#highWater;
32
+ }
33
+ get trace() {
34
+ return this.#trace;
35
+ }
36
+ schedule(label, run) {
37
+ return new Promise((resolve, reject) => {
38
+ this.#pending.push({ label, run, resolve, reject });
39
+ this.#highWater = Math.max(this.#highWater, this.#pending.length);
40
+ });
41
+ }
42
+ async settle(promises) {
43
+ const state = { complete: false };
44
+ const settled = Promise.allSettled(promises).then((results) => {
45
+ state.complete = true;
46
+ return results;
47
+ });
48
+ let emptyTurns = 0;
49
+ while (!state.complete) {
50
+ await Promise.resolve();
51
+ const pending = this.#pending.length;
52
+ if (pending === 0) {
53
+ await nextEventLoopTurn();
54
+ if (this.#pending.length > 0) {
55
+ emptyTurns = 0;
56
+ continue;
83
57
  }
84
- return settled;
85
- }
86
- #record(operation, outcome) {
87
- this.#sequence++;
88
- if (this.#traceLimit === 0)
89
- return;
90
- this.#trace.push({ sequence: this.#sequence, operation, outcome });
91
- if (this.#trace.length > this.#traceLimit)
92
- this.#trace.shift();
58
+ emptyTurns++;
59
+ if (emptyTurns > 100) {
60
+ throw new Error("Deterministic simulator deadlocked with no scheduled storage operation");
61
+ }
62
+ continue;
63
+ }
64
+ emptyTurns = 0;
65
+ const index = Math.floor(this.#random() * pending);
66
+ const operation = this.#pending.splice(index, 1)[0];
67
+ if (operation === void 0)
68
+ continue;
69
+ try {
70
+ const value = await operation.run();
71
+ operation.resolve(value);
72
+ this.#record(operation.label, "resolved");
73
+ } catch (error) {
74
+ operation.reject(error);
75
+ this.#record(operation.label, "rejected");
76
+ }
93
77
  }
78
+ return settled;
79
+ }
80
+ #record(operation, outcome) {
81
+ this.#sequence++;
82
+ if (this.#traceLimit === 0)
83
+ return;
84
+ this.#trace.push({ sequence: this.#sequence, operation, outcome });
85
+ if (this.#trace.length > this.#traceLimit)
86
+ this.#trace.shift();
87
+ }
94
88
  }
95
89
  function nextEventLoopTurn() {
96
- if (typeof MessageChannel === "undefined") {
97
- return new Promise((resolve) => setTimeout(resolve, 0));
98
- }
99
- return new Promise((resolve) => {
100
- const channel = new MessageChannel();
101
- channel.port1.onmessage = () => {
102
- channel.port1.close();
103
- channel.port2.close();
104
- resolve();
105
- };
106
- channel.port2.postMessage(undefined);
107
- });
108
- }
109
- /** Wraps every asynchronous BlockStore entry point in the deterministic completion scheduler. */
110
- export function scheduledBlockStore(store, scheduler) {
111
- return new Proxy(store, {
112
- get(target, property) {
113
- const value = Reflect.get(target, property, target);
114
- if (typeof value !== "function")
115
- return value;
116
- const method = value;
117
- if (property === "close")
118
- return (...arguments_) => method.apply(target, arguments_);
119
- return (...arguments_) => scheduler.schedule(String(property), async () => method.apply(target, arguments_));
120
- },
121
- });
90
+ if (typeof MessageChannel === "undefined") {
91
+ return new Promise((resolve) => setTimeout(resolve, 0));
92
+ }
93
+ return new Promise((resolve) => {
94
+ const channel = new MessageChannel();
95
+ channel.port1.onmessage = () => {
96
+ channel.port1.close();
97
+ channel.port2.close();
98
+ resolve();
99
+ };
100
+ channel.port2.postMessage(void 0);
101
+ });
122
102
  }
123
- export function generateSimulationPlan(seed, options = {}) {
124
- if (!Number.isSafeInteger(seed)) {
125
- throw new RangeError("Simulator seed must be a whole number");
126
- }
127
- const rounds = options.rounds ?? 48;
128
- const clients = options.clients ?? 4;
129
- const keySpace = options.keySpace ?? 32;
130
- if (!Number.isSafeInteger(rounds) || rounds < 1 || rounds > 100_000) {
131
- throw new RangeError("Simulator rounds must be between 1 and 100,000");
132
- }
133
- if (!Number.isSafeInteger(clients) || clients < 1 || clients > 32) {
134
- throw new RangeError("Simulator clients must be between 1 and 32");
103
+ function scheduledBlockStore(store, scheduler) {
104
+ return new Proxy(store, {
105
+ get(target, property) {
106
+ const value = Reflect.get(target, property, target);
107
+ if (typeof value !== "function")
108
+ return value;
109
+ const method = value;
110
+ if (property === "close")
111
+ return (...arguments_) => method.apply(target, arguments_);
112
+ return (...arguments_) => scheduler.schedule(String(property), async () => method.apply(target, arguments_));
135
113
  }
136
- if (!Number.isSafeInteger(keySpace) || keySpace < 1 || keySpace > 10_000) {
137
- throw new RangeError("Simulator key space must be between 1 and 10,000");
138
- }
139
- if (keySpace < clients) {
140
- throw new RangeError("Simulator key space must be at least the client count");
114
+ });
115
+ }
116
+ function generateSimulationPlan(seed, options = {}) {
117
+ if (!Number.isSafeInteger(seed)) {
118
+ throw new RangeError("Simulator seed must be a whole number");
119
+ }
120
+ const rounds = options.rounds ?? 48;
121
+ const clients = options.clients ?? 4;
122
+ const keySpace = options.keySpace ?? 32;
123
+ if (!Number.isSafeInteger(rounds) || rounds < 1 || rounds > 1e5) {
124
+ throw new RangeError("Simulator rounds must be between 1 and 100,000");
125
+ }
126
+ if (!Number.isSafeInteger(clients) || clients < 1 || clients > 32) {
127
+ throw new RangeError("Simulator clients must be between 1 and 32");
128
+ }
129
+ if (!Number.isSafeInteger(keySpace) || keySpace < 1 || keySpace > 1e4) {
130
+ throw new RangeError("Simulator key space must be between 1 and 10,000");
131
+ }
132
+ if (keySpace < clients) {
133
+ throw new RangeError("Simulator key space must be at least the client count");
134
+ }
135
+ const random = mulberry32(seed);
136
+ const steps = [];
137
+ const faultPoints = [
138
+ "beforeBlockWrite",
139
+ "afterBlockWrite",
140
+ "beforeTransactionCommit",
141
+ "afterTransactionCommit"
142
+ ];
143
+ for (let round = 0; round < rounds; round++) {
144
+ const operations = [];
145
+ const start = Math.floor(random() * keySpace);
146
+ let stride = Math.floor(random() * keySpace) + 1;
147
+ while (greatestCommonDivisor(stride, keySpace) !== 1)
148
+ stride++;
149
+ for (let client = 0; client < clients; client++) {
150
+ const key = (start + client * stride) % keySpace + 1;
151
+ if (random() < 0.18)
152
+ operations.push({ kind: "delete", client, key });
153
+ else
154
+ operations.push({ kind: "put", client, key, amount: Math.floor(random() * 1e6) });
141
155
  }
142
- const random = mulberry32(seed);
143
- const steps = [];
144
- const faultPoints = [
145
- "beforeBlockWrite",
146
- "afterBlockWrite",
147
- "beforeTransactionCommit",
148
- "afterTransactionCommit",
149
- ];
150
- for (let round = 0; round < rounds; round++) {
151
- const operations = [];
152
- const start = Math.floor(random() * keySpace);
153
- let stride = Math.floor(random() * keySpace) + 1;
154
- while (greatestCommonDivisor(stride, keySpace) !== 1)
155
- stride++;
156
- for (let client = 0; client < clients; client++) {
157
- // A round is a genuinely concurrent commit race, but its row mutations commute. That gives
158
- // the reference model one exact answer instead of guessing which same-key promise linearized
159
- // last. Contention still happens at the shared manifest/transaction boundary.
160
- const key = ((start + client * stride) % keySpace) + 1;
161
- if (random() < 0.18)
162
- operations.push({ kind: "delete", client, key });
163
- else
164
- operations.push({ kind: "put", client, key, amount: Math.floor(random() * 1_000_000) });
165
- }
166
- steps.push({ kind: "concurrent", operations });
167
- if (round % 4 === 3)
168
- steps.push({ kind: "checkpoint" });
169
- if (round % 12 === 11)
170
- steps.push({ kind: "maintenance" });
171
- if (round % 20 === 19) {
172
- const point = faultPoints[Math.floor(random() * faultPoints.length)] ?? "beforeBlockWrite";
173
- steps.push({
174
- kind: "fault",
175
- point,
176
- occurrence: 1,
177
- operation: {
178
- kind: "put",
179
- client: Math.floor(random() * clients),
180
- key: Math.floor(random() * keySpace) + 1,
181
- amount: Math.floor(random() * 1_000_000),
182
- },
183
- });
156
+ steps.push({ kind: "concurrent", operations });
157
+ if (round % 4 === 3)
158
+ steps.push({ kind: "checkpoint" });
159
+ if (round % 12 === 11)
160
+ steps.push({ kind: "maintenance" });
161
+ if (round % 20 === 19) {
162
+ const point = faultPoints[Math.floor(random() * faultPoints.length)] ?? "beforeBlockWrite";
163
+ steps.push({
164
+ kind: "fault",
165
+ point,
166
+ occurrence: 1,
167
+ operation: {
168
+ kind: "put",
169
+ client: Math.floor(random() * clients),
170
+ key: Math.floor(random() * keySpace) + 1,
171
+ amount: Math.floor(random() * 1e6)
184
172
  }
173
+ });
185
174
  }
186
- steps.push({ kind: "checkpoint" }, { kind: "maintenance" }, { kind: "checkpoint" });
187
- return { version: 1, seed, clients, keySpace, steps };
175
+ }
176
+ steps.push({ kind: "checkpoint" }, { kind: "maintenance" }, { kind: "checkpoint" });
177
+ return { version: 1, seed, clients, keySpace, steps };
188
178
  }
189
- export function parseSimulationPlan(source) {
190
- const value = JSON.parse(source);
191
- if (typeof value !== "object" || value === null)
192
- throw new TypeError("Simulation plan must be an object");
193
- const plan = value;
194
- if (plan.version !== 1)
195
- throw new TypeError("Simulation plan version must be 1");
196
- if (!Number.isSafeInteger(plan.seed))
197
- throw new TypeError("Simulation seed must be a whole number");
198
- if (!Number.isSafeInteger(plan.clients) || (plan.clients ?? 0) < 1 || (plan.clients ?? 0) > 32) {
199
- throw new TypeError("Simulation client count is invalid");
200
- }
201
- if (!Number.isSafeInteger(plan.keySpace) ||
202
- (plan.keySpace ?? 0) < 1 ||
203
- (plan.keySpace ?? 0) > 10_000) {
204
- throw new TypeError("Simulation key space is invalid");
205
- }
206
- if ((plan.keySpace ?? 0) < (plan.clients ?? 0)) {
207
- throw new TypeError("Simulation key space must be at least the client count");
208
- }
209
- if (!Array.isArray(plan.steps) || plan.steps.length > 200_000) {
210
- throw new TypeError("Simulation steps must be a bounded array");
211
- }
212
- validateSteps(plan.steps, plan.clients ?? 0, plan.keySpace ?? 0);
213
- return plan;
179
+ function parseSimulationPlan(source) {
180
+ const value = JSON.parse(source);
181
+ if (typeof value !== "object" || value === null)
182
+ throw new TypeError("Simulation plan must be an object");
183
+ const plan = value;
184
+ if (plan.version !== 1)
185
+ throw new TypeError("Simulation plan version must be 1");
186
+ if (!Number.isSafeInteger(plan.seed))
187
+ throw new TypeError("Simulation seed must be a whole number");
188
+ if (!Number.isSafeInteger(plan.clients) || (plan.clients ?? 0) < 1 || (plan.clients ?? 0) > 32) {
189
+ throw new TypeError("Simulation client count is invalid");
190
+ }
191
+ if (!Number.isSafeInteger(plan.keySpace) || (plan.keySpace ?? 0) < 1 || (plan.keySpace ?? 0) > 1e4) {
192
+ throw new TypeError("Simulation key space is invalid");
193
+ }
194
+ if ((plan.keySpace ?? 0) < (plan.clients ?? 0)) {
195
+ throw new TypeError("Simulation key space must be at least the client count");
196
+ }
197
+ if (!Array.isArray(plan.steps) || plan.steps.length > 2e5) {
198
+ throw new TypeError("Simulation steps must be a bounded array");
199
+ }
200
+ validateSteps(plan.steps, plan.clients ?? 0, plan.keySpace ?? 0);
201
+ return plan;
214
202
  }
215
- /** Runs a replayable state-machine plan through production SQL, transactions, and block storage. */
216
- export async function runSimulation(plan, options = {}) {
217
- parseSimulationPlan(JSON.stringify(plan));
218
- const base = new MemoryBlockStore();
219
- const faults = new FaultController();
220
- const scheduler = new DeterministicScheduler(plan.seed ^ 0x5f3759df, options.traceEvents ?? 256);
221
- const store = scheduledBlockStore(new FaultInjectingBlockStore(base, (point) => faults.inject(point)), scheduler);
222
- let clock = Date.parse("2026-01-01T00:00:00.000Z");
223
- let id = 0;
224
- const createDatabase = () => new MinnowDatabase(store, {
225
- compression: "raw",
226
- rowsPerBlock: 8,
227
- maxCommitRetries: 16,
228
- now: () => new Date(clock),
229
- createId: () => `sim-${String(id++)}`,
230
- bufferPoolBytes: 0,
231
- autoCompact: false,
232
- autoCollect: false,
233
- });
234
- const setup = createDatabase();
235
- await drive(scheduler, setup.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, client INTEGER NOT NULL, amount INTEGER NOT NULL)"));
236
- await drive(scheduler, setup.close());
237
- const clients = Array.from({ length: plan.clients }, createDatabase);
238
- const model = new Map();
239
- let acceptedMutations = 0;
240
- let rejectedConflicts = 0;
241
- let injectedFaults = 0;
242
- let checkpoints = 0;
243
- for (const step of plan.steps) {
244
- clock += 1_000;
245
- if (step.kind === "concurrent") {
246
- const operations = step.operations.map((operation) => executeMutation(requireClient(clients, operation.client), operation)
247
- .then(() => {
248
- applyMutation(model, operation);
249
- acceptedMutations++;
250
- })
251
- .catch((error) => {
252
- if (/manifest changed|conflict/iu.test(errorMessage(error))) {
253
- rejectedConflicts++;
254
- return;
255
- }
256
- throw error;
257
- }));
258
- assertSettled(await scheduler.settle(operations));
259
- continue;
203
+ async function runSimulation(plan, options = {}) {
204
+ parseSimulationPlan(JSON.stringify(plan));
205
+ const base = new MemoryBlockStore();
206
+ const faults = new FaultController();
207
+ const scheduler = new DeterministicScheduler(plan.seed ^ 1597463007, options.traceEvents ?? 256);
208
+ const store = scheduledBlockStore(new FaultInjectingBlockStore(base, (point) => faults.inject(point)), scheduler);
209
+ let clock = Date.parse("2026-01-01T00:00:00.000Z");
210
+ let id = 0;
211
+ const createDatabase = () => new MinnowDatabase(store, {
212
+ compression: "raw",
213
+ rowsPerBlock: 8,
214
+ maxCommitRetries: 16,
215
+ now: () => new Date(clock),
216
+ createId: () => `sim-${String(id++)}`,
217
+ bufferPoolBytes: 0,
218
+ autoCompact: false,
219
+ autoCollect: false
220
+ });
221
+ const setup = createDatabase();
222
+ await drive(scheduler, setup.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, client INTEGER NOT NULL, amount INTEGER NOT NULL)"));
223
+ await drive(scheduler, setup.close());
224
+ const clients = Array.from({ length: plan.clients }, createDatabase);
225
+ const model = /* @__PURE__ */ new Map();
226
+ let acceptedMutations = 0;
227
+ let rejectedConflicts = 0;
228
+ let injectedFaults = 0;
229
+ let checkpoints = 0;
230
+ for (const step of plan.steps) {
231
+ clock += 1e3;
232
+ if (step.kind === "concurrent") {
233
+ const operations = step.operations.map((operation) => executeMutation(requireClient(clients, operation.client), operation).then(() => {
234
+ applyMutation(model, operation);
235
+ acceptedMutations++;
236
+ }).catch((error) => {
237
+ if (/manifest changed|conflict/iu.test(errorMessage(error))) {
238
+ rejectedConflicts++;
239
+ return;
260
240
  }
261
- if (step.kind === "checkpoint") {
262
- await assertModel(scheduler, clients[0] ?? setup, model);
263
- checkpoints++;
264
- continue;
265
- }
266
- if (step.kind === "maintenance") {
267
- const database = clients[0] ?? setup;
268
- await drive(scheduler, database.compactTable("items"));
269
- // One pass can remove obsolete manifests/segments while their transaction journals are
270
- // still provenance roots. The next pass can reclaim those now-unreferenced journals.
271
- await drive(scheduler, database.collectGarbage());
272
- await drive(scheduler, database.collectGarbage());
273
- continue;
274
- }
275
- const before = new Map(model);
276
- const after = new Map(model);
277
- applyMutation(after, step.operation);
278
- faults.arm(step.point, step.occurrence);
279
- const result = await scheduler.settle([
280
- executeMutation(requireClient(clients, step.operation.client), step.operation),
281
- ]);
282
- if (!faults.fired)
283
- throw new Error(`Simulation fault was not reached: ${step.point}`);
284
- injectedFaults++;
285
- faults.disarm();
286
- const failedClient = requireClient(clients, step.operation.client);
287
- await drive(scheduler, failedClient.close());
288
- const replacement = createDatabase();
289
- clients[step.operation.client] = replacement;
290
- const actual = await readRows(scheduler, replacement);
291
- if (sameRows(actual, before)) {
292
- if (result[0]?.status === "fulfilled") {
293
- throw new Error(`Fault ${step.point} returned success but did not persist its mutation`);
294
- }
295
- replaceModel(model, before);
296
- }
297
- else if (sameRows(actual, after))
298
- replaceModel(model, after);
299
- else
300
- throw new Error(`Fault ${step.point} left a state that was neither before nor after the mutation`);
241
+ throw error;
242
+ }));
243
+ assertSettled(await scheduler.settle(operations));
244
+ continue;
301
245
  }
302
- await assertModel(scheduler, clients[0] ?? setup, model);
303
- const closeResults = await scheduler.settle(clients.map((client) => client.close()));
304
- assertSettled(closeResults);
305
- // Measure eventual storage after the simulated tabs release their deliberately cached reader
306
- // leases. A fresh maintenance client can then collapse history without an artificial pin from
307
- // the workload whose final state has already been checked.
308
- const maintenance = createDatabase();
309
- const compacted = await compactToFixedPoint(scheduler, maintenance, plan.steps.length);
310
- const maintenanceCompactions = compacted.passes;
311
- const compactionStop = compacted.stop;
312
- const collected = await collectToFixedPoint(scheduler, maintenance, plan.steps.length);
313
- let collectionPasses = collected.passes;
314
- const collectionTail = [...collected.tail];
315
- await assertModel(scheduler, maintenance, model);
316
- await drive(scheduler, maintenance.close());
317
- const sweeper = createDatabase();
318
- const swept = await collectToFixedPoint(scheduler, sweeper, plan.steps.length);
319
- collectionPasses += swept.passes;
320
- collectionTail.push(...swept.tail);
321
- if (collectionTail.length > 8)
322
- collectionTail.splice(0, collectionTail.length - 8);
323
- await drive(scheduler, sweeper.close());
324
- const rows = sortedRows(model);
325
- const [segments, transactions, manifests, currentManifest, leases, compactions, storageBytes] = await Promise.all([
326
- collectStoragePages((cursor) => base.listSegmentPage(cursor, 256)),
327
- collectStoragePages((cursor) => base.listTransactionPage(cursor, 256)),
328
- collectStoragePages((cursor) => base.listManifestPage(cursor, 256)),
329
- base.getCurrentManifest(),
330
- base.listLeases(),
331
- base.listCompactionJobs(),
332
- base.getLogicalStorageBytes(),
333
- ]);
334
- const blockCount = currentManifest?.liveBlockCount ?? 0;
335
- const blockLimit = plan.keySpace * 16 + 64;
336
- const segmentLimit = plan.keySpace + 16;
337
- const transactionLimit = segmentLimit + 16;
338
- if (blockCount > blockLimit ||
339
- segments.length > segmentLimit ||
340
- transactions.length > transactionLimit) {
341
- const transactionStates = transactions.reduce((counts, transaction) => {
342
- counts[transaction.status] = (counts[transaction.status] ?? 0) + 1;
343
- return counts;
344
- }, {});
345
- const compactionStates = compactions.reduce((counts, compaction) => {
346
- counts[compaction.state] = (counts[compaction.state] ?? 0) + 1;
347
- return counts;
348
- }, {});
349
- throw new Error(`Storage did not compact to a bound: ${String(blockCount)}/${String(blockLimit)} live blocks, ` +
350
- `${String(segments.length)}/${String(segmentLimit)} segments, ` +
351
- `${String(transactions.length)}/${String(transactionLimit)} transactions; ` +
352
- `${JSON.stringify(transactionStates)} transaction states, ` +
353
- `${JSON.stringify(compactionStates)} compaction states, ` +
354
- `${String(currentManifest?.liveBlockCount ?? 0)} current blocks, compaction stopped at ${compactionStop}, ` +
355
- `${String(manifests.length)} manifests (${String(manifests.filter((manifest) => manifest.prunedAt !== undefined).length)} pruned) remain after ${String(maintenanceCompactions)} compactions ` +
356
- `and ${String(collectionPasses)} collection passes; collection tail ${JSON.stringify(collectionTail)}`);
246
+ if (step.kind === "checkpoint") {
247
+ await assertModel(scheduler, clients[0] ?? setup, model);
248
+ checkpoints++;
249
+ continue;
357
250
  }
358
- if (manifests.length > 2) {
359
- throw new Error(`Garbage collection left ${String(manifests.length)} manifests and ${String(leases.length)} leases`);
251
+ if (step.kind === "maintenance") {
252
+ const database = clients[0] ?? setup;
253
+ await drive(scheduler, database.compactTable("items"));
254
+ await drive(scheduler, database.collectGarbage());
255
+ await drive(scheduler, database.collectGarbage());
256
+ continue;
360
257
  }
361
- if (leases.length !== 0) {
362
- throw new Error(`Simulation clients leaked ${String(leases.length)} leases`);
363
- }
364
- const trace = [...scheduler.trace];
365
- return {
366
- seed: plan.seed,
367
- acceptedMutations,
368
- rejectedConflicts,
369
- injectedFaults,
370
- checkpoints,
371
- maintenanceCompactions,
372
- compactionStop,
373
- collectionPasses,
374
- collectionTail,
375
- rows,
376
- blockCount,
377
- segmentCount: segments.length,
378
- transactionCount: transactions.length,
379
- manifestCount: manifests.length,
380
- leaseCount: leases.length,
381
- storageBytes,
382
- schedulerHighWater: scheduler.highWater,
383
- trace,
384
- traceDigest: md5Hex(JSON.stringify(trace)),
385
- };
258
+ const before = new Map(model);
259
+ const after = new Map(model);
260
+ applyMutation(after, step.operation);
261
+ faults.arm(step.point, step.occurrence);
262
+ const result = await scheduler.settle([
263
+ executeMutation(requireClient(clients, step.operation.client), step.operation)
264
+ ]);
265
+ if (!faults.fired)
266
+ throw new Error(`Simulation fault was not reached: ${step.point}`);
267
+ injectedFaults++;
268
+ faults.disarm();
269
+ const failedClient = requireClient(clients, step.operation.client);
270
+ await drive(scheduler, failedClient.close());
271
+ const replacement = createDatabase();
272
+ clients[step.operation.client] = replacement;
273
+ const actual = await readRows(scheduler, replacement);
274
+ if (sameRows(actual, before)) {
275
+ if (result[0]?.status === "fulfilled") {
276
+ throw new Error(`Fault ${step.point} returned success but did not persist its mutation`);
277
+ }
278
+ replaceModel(model, before);
279
+ } else if (sameRows(actual, after))
280
+ replaceModel(model, after);
281
+ else
282
+ throw new Error(`Fault ${step.point} left a state that was neither before nor after the mutation`);
283
+ }
284
+ await assertModel(scheduler, clients[0] ?? setup, model);
285
+ const closeResults = await scheduler.settle(clients.map((client) => client.close()));
286
+ assertSettled(closeResults);
287
+ const maintenance = createDatabase();
288
+ const compacted = await compactToFixedPoint(scheduler, maintenance, plan.steps.length);
289
+ const maintenanceCompactions = compacted.passes;
290
+ const compactionStop = compacted.stop;
291
+ const collected = await collectToFixedPoint(scheduler, maintenance, plan.steps.length);
292
+ let collectionPasses = collected.passes;
293
+ const collectionTail = [...collected.tail];
294
+ await assertModel(scheduler, maintenance, model);
295
+ await drive(scheduler, maintenance.close());
296
+ const sweeper = createDatabase();
297
+ const swept = await collectToFixedPoint(scheduler, sweeper, plan.steps.length);
298
+ collectionPasses += swept.passes;
299
+ collectionTail.push(...swept.tail);
300
+ if (collectionTail.length > 8)
301
+ collectionTail.splice(0, collectionTail.length - 8);
302
+ await drive(scheduler, sweeper.close());
303
+ const rows = sortedRows(model);
304
+ const [segments, transactions, manifests, currentManifest, leases, compactions, storageBytes] = await Promise.all([
305
+ collectStoragePages((cursor) => base.listSegmentPage(cursor, 256)),
306
+ collectStoragePages((cursor) => base.listTransactionPage(cursor, 256)),
307
+ collectStoragePages((cursor) => base.listManifestPage(cursor, 256)),
308
+ base.getCurrentManifest(),
309
+ base.listLeases(),
310
+ base.listCompactionJobs(),
311
+ base.getLogicalStorageBytes()
312
+ ]);
313
+ const blockCount = currentManifest?.liveBlockCount ?? 0;
314
+ const blockLimit = plan.keySpace * 16 + 64;
315
+ const segmentLimit = plan.keySpace + 16;
316
+ const transactionLimit = segmentLimit + 16;
317
+ if (blockCount > blockLimit || segments.length > segmentLimit || transactions.length > transactionLimit) {
318
+ const transactionStates = transactions.reduce((counts, transaction) => {
319
+ counts[transaction.status] = (counts[transaction.status] ?? 0) + 1;
320
+ return counts;
321
+ }, {});
322
+ const compactionStates = compactions.reduce((counts, compaction) => {
323
+ counts[compaction.state] = (counts[compaction.state] ?? 0) + 1;
324
+ return counts;
325
+ }, {});
326
+ throw new Error(`Storage did not compact to a bound: ${String(blockCount)}/${String(blockLimit)} live blocks, ${String(segments.length)}/${String(segmentLimit)} segments, ${String(transactions.length)}/${String(transactionLimit)} transactions; ${JSON.stringify(transactionStates)} transaction states, ${JSON.stringify(compactionStates)} compaction states, ${String(currentManifest?.liveBlockCount ?? 0)} current blocks, compaction stopped at ${compactionStop}, ${String(manifests.length)} manifests (${String(manifests.filter((manifest) => manifest.prunedAt !== void 0).length)} pruned) remain after ${String(maintenanceCompactions)} compactions and ${String(collectionPasses)} collection passes; collection tail ${JSON.stringify(collectionTail)}`);
327
+ }
328
+ if (manifests.length > 2) {
329
+ throw new Error(`Garbage collection left ${String(manifests.length)} manifests and ${String(leases.length)} leases`);
330
+ }
331
+ if (leases.length !== 0) {
332
+ throw new Error(`Simulation clients leaked ${String(leases.length)} leases`);
333
+ }
334
+ const trace = [...scheduler.trace];
335
+ return {
336
+ seed: plan.seed,
337
+ acceptedMutations,
338
+ rejectedConflicts,
339
+ injectedFaults,
340
+ checkpoints,
341
+ maintenanceCompactions,
342
+ compactionStop,
343
+ collectionPasses,
344
+ collectionTail,
345
+ rows,
346
+ blockCount,
347
+ segmentCount: segments.length,
348
+ transactionCount: transactions.length,
349
+ manifestCount: manifests.length,
350
+ leaseCount: leases.length,
351
+ storageBytes,
352
+ schedulerHighWater: scheduler.highWater,
353
+ trace,
354
+ traceDigest: md5Hex(JSON.stringify(trace))
355
+ };
386
356
  }
387
357
  class FaultController {
388
- #point;
389
- #occurrence = 0;
390
- #seen = 0;
391
- fired = false;
392
- arm(point, occurrence) {
393
- this.#point = point;
394
- this.#occurrence = occurrence;
395
- this.#seen = 0;
396
- this.fired = false;
397
- }
398
- disarm() {
399
- this.#point = undefined;
400
- }
401
- inject(point) {
402
- if (point !== this.#point || this.fired)
403
- return;
404
- this.#seen++;
405
- if (this.#seen !== this.#occurrence)
406
- return;
407
- this.fired = true;
408
- throw new Error(`injected ${point} #${String(this.#occurrence)}`);
409
- }
358
+ #point;
359
+ #occurrence = 0;
360
+ #seen = 0;
361
+ fired = false;
362
+ arm(point, occurrence) {
363
+ this.#point = point;
364
+ this.#occurrence = occurrence;
365
+ this.#seen = 0;
366
+ this.fired = false;
367
+ }
368
+ disarm() {
369
+ this.#point = void 0;
370
+ }
371
+ inject(point) {
372
+ if (point !== this.#point || this.fired)
373
+ return;
374
+ this.#seen++;
375
+ if (this.#seen !== this.#occurrence)
376
+ return;
377
+ this.fired = true;
378
+ throw new Error(`injected ${point} #${String(this.#occurrence)}`);
379
+ }
410
380
  }
411
381
  async function drive(scheduler, promise) {
412
- const result = (await scheduler.settle([promise]))[0];
413
- if (result?.status === "fulfilled")
414
- return result.value;
415
- throw result?.reason ?? new Error("Scheduled operation did not settle");
382
+ const result = (await scheduler.settle([promise]))[0];
383
+ if (result?.status === "fulfilled")
384
+ return result.value;
385
+ throw result?.reason ?? new Error("Scheduled operation did not settle");
416
386
  }
417
387
  function assertSettled(results) {
418
- const failed = results.find((result) => result.status === "rejected");
419
- if (failed?.status === "rejected")
420
- throw failed.reason;
388
+ const failed = results.find((result) => result.status === "rejected");
389
+ if (failed?.status === "rejected")
390
+ throw failed.reason;
421
391
  }
422
392
  function executeMutation(database, operation) {
423
- if (operation.kind === "delete") {
424
- return database.execute("DELETE FROM items WHERE id = ?", [operation.key]);
425
- }
426
- return database.execute("INSERT INTO items (id, client, amount) VALUES (?, ?, ?) " +
427
- "ON CONFLICT (id) DO UPDATE SET client = excluded.client, amount = excluded.amount", [operation.key, operation.client, operation.amount]);
393
+ if (operation.kind === "delete") {
394
+ return database.execute("DELETE FROM items WHERE id = ?", [operation.key]);
395
+ }
396
+ return database.execute("INSERT INTO items (id, client, amount) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET client = excluded.client, amount = excluded.amount", [operation.key, operation.client, operation.amount]);
428
397
  }
429
398
  function applyMutation(model, operation) {
430
- if (operation.kind === "delete")
431
- model.delete(operation.key);
432
- else
433
- model.set(operation.key, {
434
- id: operation.key,
435
- client: operation.client,
436
- amount: operation.amount,
437
- });
399
+ if (operation.kind === "delete")
400
+ model.delete(operation.key);
401
+ else
402
+ model.set(operation.key, {
403
+ id: operation.key,
404
+ client: operation.client,
405
+ amount: operation.amount
406
+ });
438
407
  }
439
408
  async function assertModel(scheduler, database, model) {
440
- const actual = await readRows(scheduler, database);
441
- if (!sameRows(actual, model)) {
442
- throw new Error(`Model mismatch:\nexpected ${JSON.stringify(sortedRows(model))}\nreceived ${JSON.stringify(actual)}`);
443
- }
409
+ const actual = await readRows(scheduler, database);
410
+ if (!sameRows(actual, model)) {
411
+ throw new Error(`Model mismatch:
412
+ expected ${JSON.stringify(sortedRows(model))}
413
+ received ${JSON.stringify(actual)}`);
414
+ }
444
415
  }
445
416
  async function readRows(scheduler, database) {
446
- const result = await drive(scheduler, database.query("SELECT id, client, amount FROM items ORDER BY id", { memoize: false }));
447
- return result.rows;
417
+ const result = await drive(scheduler, database.query("SELECT id, client, amount FROM items ORDER BY id", { memoize: false }));
418
+ return result.rows;
448
419
  }
449
420
  function sameRows(rows, model) {
450
- return JSON.stringify(rows) === JSON.stringify(sortedRows(model));
421
+ return JSON.stringify(rows) === JSON.stringify(sortedRows(model));
451
422
  }
452
423
  function sortedRows(model) {
453
- return [...model.values()].sort((left, right) => left.id - right.id);
424
+ return [...model.values()].sort((left, right) => left.id - right.id);
454
425
  }
455
426
  function replaceModel(target, source) {
456
- target.clear();
457
- for (const [key, value] of source)
458
- target.set(key, value);
427
+ target.clear();
428
+ for (const [key, value] of source)
429
+ target.set(key, value);
459
430
  }
460
431
  function requireClient(clients, index) {
461
- const client = clients[index];
462
- if (client === undefined)
463
- throw new RangeError(`Simulation client is out of range: ${String(index)}`);
464
- return client;
432
+ const client = clients[index];
433
+ if (client === void 0)
434
+ throw new RangeError(`Simulation client is out of range: ${String(index)}`);
435
+ return client;
465
436
  }
466
437
  function validateSteps(steps, clients, keySpace) {
467
- const validPoint = new Set([
468
- "beforeBlockWrite",
469
- "afterBlockWrite",
470
- "beforeBlockRead",
471
- "afterBlockRead",
472
- "beforeTransactionCommit",
473
- "afterTransactionCommit",
474
- ]);
475
- const validateMutation = (value) => {
476
- if (typeof value !== "object" || value === null) {
477
- throw new TypeError("Simulation mutation must be an object");
478
- }
479
- const operation = value;
480
- if (operation.kind !== "put" && operation.kind !== "delete") {
481
- throw new TypeError("Simulation mutation kind is invalid");
482
- }
483
- const client = operation.client;
484
- if (typeof client !== "number" ||
485
- !Number.isSafeInteger(client) ||
486
- client < 0 ||
487
- client >= clients) {
488
- throw new TypeError("Simulation mutation has an invalid client");
489
- }
490
- const key = operation.key;
491
- if (typeof key !== "number" || !Number.isSafeInteger(key) || key < 1 || key > keySpace) {
492
- throw new TypeError("Simulation mutation has an invalid key");
493
- }
494
- if (operation.kind === "put" &&
495
- (typeof operation.amount !== "number" || !Number.isSafeInteger(operation.amount))) {
496
- throw new TypeError("Simulation put has an invalid amount");
497
- }
498
- };
499
- for (const value of steps) {
500
- if (typeof value !== "object" || value === null) {
501
- throw new TypeError("Simulation step must be an object");
502
- }
503
- const step = value;
504
- if (step.kind === "concurrent") {
505
- if (!Array.isArray(step.operations) || step.operations.length > clients * 4) {
506
- throw new TypeError("Simulation concurrent step is unbounded");
507
- }
508
- step.operations.forEach(validateMutation);
509
- }
510
- else if (step.kind === "fault") {
511
- if (typeof step.point !== "string" || !validPoint.has(step.point)) {
512
- throw new TypeError("Simulation fault point is invalid");
513
- }
514
- const occurrence = step.occurrence;
515
- if (typeof occurrence !== "number" || !Number.isSafeInteger(occurrence) || occurrence < 1) {
516
- throw new TypeError("Simulation fault occurrence is invalid");
517
- }
518
- validateMutation(step.operation);
519
- }
520
- else if (step.kind !== "checkpoint" && step.kind !== "maintenance") {
521
- throw new TypeError("Simulation step kind is invalid");
522
- }
438
+ const validPoint = /* @__PURE__ */ new Set([
439
+ "beforeBlockWrite",
440
+ "afterBlockWrite",
441
+ "beforeBlockRead",
442
+ "afterBlockRead",
443
+ "beforeTransactionCommit",
444
+ "afterTransactionCommit"
445
+ ]);
446
+ const validateMutation = (value) => {
447
+ if (typeof value !== "object" || value === null) {
448
+ throw new TypeError("Simulation mutation must be an object");
449
+ }
450
+ const operation = value;
451
+ if (operation.kind !== "put" && operation.kind !== "delete") {
452
+ throw new TypeError("Simulation mutation kind is invalid");
453
+ }
454
+ const client = operation.client;
455
+ if (typeof client !== "number" || !Number.isSafeInteger(client) || client < 0 || client >= clients) {
456
+ throw new TypeError("Simulation mutation has an invalid client");
457
+ }
458
+ const key = operation.key;
459
+ if (typeof key !== "number" || !Number.isSafeInteger(key) || key < 1 || key > keySpace) {
460
+ throw new TypeError("Simulation mutation has an invalid key");
461
+ }
462
+ if (operation.kind === "put" && (typeof operation.amount !== "number" || !Number.isSafeInteger(operation.amount))) {
463
+ throw new TypeError("Simulation put has an invalid amount");
523
464
  }
465
+ };
466
+ for (const value of steps) {
467
+ if (typeof value !== "object" || value === null) {
468
+ throw new TypeError("Simulation step must be an object");
469
+ }
470
+ const step = value;
471
+ if (step.kind === "concurrent") {
472
+ if (!Array.isArray(step.operations) || step.operations.length > clients * 4) {
473
+ throw new TypeError("Simulation concurrent step is unbounded");
474
+ }
475
+ step.operations.forEach(validateMutation);
476
+ } else if (step.kind === "fault") {
477
+ if (typeof step.point !== "string" || !validPoint.has(step.point)) {
478
+ throw new TypeError("Simulation fault point is invalid");
479
+ }
480
+ const occurrence = step.occurrence;
481
+ if (typeof occurrence !== "number" || !Number.isSafeInteger(occurrence) || occurrence < 1) {
482
+ throw new TypeError("Simulation fault occurrence is invalid");
483
+ }
484
+ validateMutation(step.operation);
485
+ } else if (step.kind !== "checkpoint" && step.kind !== "maintenance") {
486
+ throw new TypeError("Simulation step kind is invalid");
487
+ }
488
+ }
524
489
  }
525
490
  function errorMessage(error) {
526
- return error instanceof Error ? error.message : String(error);
491
+ return error instanceof Error ? error.message : String(error);
527
492
  }
528
493
  async function compactToFixedPoint(scheduler, database, workloadSteps) {
529
- const maximumPasses = Math.min(10_000, workloadSteps + 128);
530
- for (let pass = 0; pass < maximumPasses; pass++) {
531
- const result = await drive(scheduler, database.compactTable("items"));
532
- if (!result.compacted)
533
- return { passes: pass, stop: result.skipReason ?? "unknown" };
534
- }
535
- throw new Error(`Compaction did not converge within ${String(maximumPasses)} passes`);
494
+ const maximumPasses = Math.min(1e4, workloadSteps + 128);
495
+ for (let pass = 0; pass < maximumPasses; pass++) {
496
+ const result = await drive(scheduler, database.compactTable("items"));
497
+ if (!result.compacted)
498
+ return { passes: pass, stop: result.skipReason ?? "unknown" };
499
+ }
500
+ throw new Error(`Compaction did not converge within ${String(maximumPasses)} passes`);
536
501
  }
537
502
  async function collectToFixedPoint(scheduler, database, workloadSteps) {
538
- const maximumPasses = Math.min(10_000, workloadSteps + 128);
539
- let emptyPasses = 0;
540
- const tail = [];
541
- for (let pass = 1; pass <= maximumPasses; pass++) {
542
- const result = await drive(scheduler, database.collectGarbage());
543
- tail.push(summarizeCollection(result));
544
- if (tail.length > 8)
545
- tail.shift();
546
- const changed = result.prunedManifestCount +
547
- result.reclaimedSegmentCount +
548
- result.reclaimedBlockCount +
549
- result.reclaimedTransactionCount;
550
- // collectGarbage also prunes finished maintenance records after returning its counters. A
551
- // zero-artifact pass can therefore remove the final provenance root that lets the next pass
552
- // reclaim bytes. Two consecutive empty passes prove both layers have settled.
553
- if (changed === 0) {
554
- emptyPasses++;
555
- if (emptyPasses === 2)
556
- return { passes: pass, tail };
557
- }
558
- else
559
- emptyPasses = 0;
560
- }
561
- throw new Error(`Garbage collection did not converge within ${String(maximumPasses)} passes`);
503
+ const maximumPasses = Math.min(1e4, workloadSteps + 128);
504
+ let emptyPasses = 0;
505
+ const tail = [];
506
+ for (let pass = 1; pass <= maximumPasses; pass++) {
507
+ const result = await drive(scheduler, database.collectGarbage());
508
+ tail.push(summarizeCollection(result));
509
+ if (tail.length > 8)
510
+ tail.shift();
511
+ const changed = result.prunedManifestCount + result.reclaimedSegmentCount + result.reclaimedBlockCount + result.reclaimedTransactionCount;
512
+ if (changed === 0) {
513
+ emptyPasses++;
514
+ if (emptyPasses === 2)
515
+ return { passes: pass, tail };
516
+ } else
517
+ emptyPasses = 0;
518
+ }
519
+ throw new Error(`Garbage collection did not converge within ${String(maximumPasses)} passes`);
562
520
  }
563
521
  function summarizeCollection(result) {
564
- return {
565
- prunedManifests: result.prunedManifestCount,
566
- retainedManifests: result.retainedManifestCount,
567
- reclaimedSegments: result.reclaimedSegmentCount,
568
- retainedSegments: result.retainedSegmentCount,
569
- reclaimedBlocks: result.reclaimedBlockCount,
570
- retainedBlocks: result.retainedBlockCount,
571
- reclaimedTransactions: result.reclaimedTransactionCount,
572
- retainedTransactions: result.retainedTransactionCount,
573
- };
522
+ return {
523
+ prunedManifests: result.prunedManifestCount,
524
+ retainedManifests: result.retainedManifestCount,
525
+ reclaimedSegments: result.reclaimedSegmentCount,
526
+ retainedSegments: result.retainedSegmentCount,
527
+ reclaimedBlocks: result.reclaimedBlockCount,
528
+ retainedBlocks: result.retainedBlockCount,
529
+ reclaimedTransactions: result.reclaimedTransactionCount,
530
+ retainedTransactions: result.retainedTransactionCount
531
+ };
574
532
  }
575
- // Stream-identical copy of `mulberry32` in ./seeds.ts. This module ships in the tarball and
576
- // seeds.ts does not (it reads MINNOW_SEED and the unpublished regression-seeds.json), so the
577
- // published simulator cannot import the canonical copy.
578
533
  function mulberry32(seed) {
579
- let state = seed >>> 0;
580
- return () => {
581
- state = (state + 0x6d2b79f5) >>> 0;
582
- let mixed = state;
583
- mixed = Math.imul(mixed ^ (mixed >>> 15), mixed | 1);
584
- mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61);
585
- return ((mixed ^ (mixed >>> 14)) >>> 0) / 4_294_967_296;
586
- };
534
+ let state = seed >>> 0;
535
+ return () => {
536
+ state = state + 1831565813 >>> 0;
537
+ let mixed = state;
538
+ mixed = Math.imul(mixed ^ mixed >>> 15, mixed | 1);
539
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
540
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
541
+ };
587
542
  }
588
543
  function greatestCommonDivisor(left, right) {
589
- let a = left;
590
- let b = right;
591
- while (b !== 0)
592
- [a, b] = [b, a % b];
593
- return a;
544
+ let a = left;
545
+ let b = right;
546
+ while (b !== 0)
547
+ [a, b] = [b, a % b];
548
+ return a;
594
549
  }
550
+ export {
551
+ DeterministicScheduler,
552
+ generateSimulationPlan,
553
+ parseSimulationPlan,
554
+ runSimulation,
555
+ scheduledBlockStore
556
+ };