@rotorsoft/act-tck 1.16.0 → 1.17.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.
- package/README.md +66 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/cache-differential-tck.d.ts +80 -0
- package/dist/@types/cache-differential-tck.d.ts.map +1 -0
- package/dist/@types/index.d.ts +6 -0
- package/dist/@types/index.d.ts.map +1 -1
- package/dist/@types/logger-differential-tck.d.ts +61 -0
- package/dist/@types/logger-differential-tck.d.ts.map +1 -0
- package/dist/@types/store-differential-tck.d.ts +93 -0
- package/dist/@types/store-differential-tck.d.ts.map +1 -0
- package/dist/@types/store-tck.d.ts.map +1 -1
- package/dist/index.cjs +942 -507
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +915 -483
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -1,5 +1,139 @@
|
|
|
1
|
+
// src/cache-differential-tck.ts
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
// src/fixtures/helpers.ts
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
var uid = () => randomUUID().slice(0, 8);
|
|
7
|
+
var actor = (name = "tester") => ({ id: randomUUID(), name });
|
|
8
|
+
var make_meta = (opts = {}) => ({
|
|
9
|
+
correlation: opts.correlation ?? randomUUID(),
|
|
10
|
+
causation: opts.stream ? {
|
|
11
|
+
action: {
|
|
12
|
+
name: opts.action ?? "Test",
|
|
13
|
+
stream: opts.stream,
|
|
14
|
+
actor: actor()
|
|
15
|
+
}
|
|
16
|
+
} : {}
|
|
17
|
+
});
|
|
18
|
+
var inc = (amount = 1) => ({
|
|
19
|
+
name: "Incremented",
|
|
20
|
+
data: { amount }
|
|
21
|
+
});
|
|
22
|
+
var dec = (amount = 1) => ({
|
|
23
|
+
name: "Decremented",
|
|
24
|
+
data: { amount }
|
|
25
|
+
});
|
|
26
|
+
var reset = () => ({ name: "Reset", data: {} });
|
|
27
|
+
var seed_stream = async (store, stream, count, correlation) => {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (let i = 0; i < count; i++) {
|
|
30
|
+
const committed = await store.commit(
|
|
31
|
+
stream,
|
|
32
|
+
[inc(1)],
|
|
33
|
+
make_meta({ correlation, stream })
|
|
34
|
+
);
|
|
35
|
+
out.push(...committed);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
var collect = async (store, query) => {
|
|
40
|
+
const out = [];
|
|
41
|
+
await store.query((e) => {
|
|
42
|
+
out.push(e);
|
|
43
|
+
}, query);
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/cache-differential-tck.ts
|
|
48
|
+
var mulberry32 = (seed) => {
|
|
49
|
+
let a = seed >>> 0;
|
|
50
|
+
return () => {
|
|
51
|
+
a = a + 1831565813 | 0;
|
|
52
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
53
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
54
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
var build_cache_plan = (seed, stream_count) => {
|
|
58
|
+
const rng = mulberry32(seed);
|
|
59
|
+
const prefix = `cdiff-${uid()}-`;
|
|
60
|
+
const streams = Array.from(
|
|
61
|
+
{ length: stream_count },
|
|
62
|
+
(_, i) => `${prefix}${i}`
|
|
63
|
+
);
|
|
64
|
+
const make_entry = () => {
|
|
65
|
+
const v = Math.floor(rng() * 1e3);
|
|
66
|
+
return {
|
|
67
|
+
state: { count: v, label: `k${v % 7}` },
|
|
68
|
+
version: v,
|
|
69
|
+
event_id: v,
|
|
70
|
+
patches: 1 + Math.floor(rng() * 5),
|
|
71
|
+
snaps: Math.floor(rng() * 3)
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const pick_stream = () => streams[Math.floor(rng() * streams.length)];
|
|
75
|
+
const ops = [];
|
|
76
|
+
for (const stream of streams)
|
|
77
|
+
ops.push({ t: "set", stream, entry: make_entry() });
|
|
78
|
+
const middle = 6 + Math.floor(rng() * 14);
|
|
79
|
+
for (let i = 0; i < middle; i++) {
|
|
80
|
+
const kind = Math.floor(rng() * 3);
|
|
81
|
+
if (kind === 0)
|
|
82
|
+
ops.push({ t: "set", stream: pick_stream(), entry: make_entry() });
|
|
83
|
+
else if (kind === 1) ops.push({ t: "invalidate", stream: pick_stream() });
|
|
84
|
+
else ops.push({ t: "clear" });
|
|
85
|
+
}
|
|
86
|
+
return { streams, ops };
|
|
87
|
+
};
|
|
88
|
+
var apply_op = async (cache, op) => {
|
|
89
|
+
if (op.t === "set") await cache.set(op.stream, op.entry);
|
|
90
|
+
else if (op.t === "invalidate") await cache.invalidate(op.stream);
|
|
91
|
+
else await cache.clear();
|
|
92
|
+
};
|
|
93
|
+
var snapshot = async (cache, streams) => {
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const stream of streams)
|
|
96
|
+
out[stream] = await cache.get(stream);
|
|
97
|
+
return out;
|
|
98
|
+
};
|
|
99
|
+
var runCacheDifferentialTck = (options) => {
|
|
100
|
+
describe(`TCK / Cache differential / ${options.name}`, () => {
|
|
101
|
+
const base_seed = options.seed ?? 3244;
|
|
102
|
+
const stream_count = options.streams ?? 6;
|
|
103
|
+
const plans = Array.from(
|
|
104
|
+
{ length: options.runs ?? 8 },
|
|
105
|
+
(_, r) => build_cache_plan(base_seed + r, stream_count)
|
|
106
|
+
);
|
|
107
|
+
const live = [];
|
|
108
|
+
beforeAll(async () => {
|
|
109
|
+
for (const spec of options.caches) {
|
|
110
|
+
live.push({ name: spec.name, cache: await spec.factory() });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
afterAll(async () => {
|
|
114
|
+
for (const { cache } of live) await cache.dispose();
|
|
115
|
+
});
|
|
116
|
+
plans.forEach((plan, run) => {
|
|
117
|
+
const seed_hex = `0x${(base_seed + run).toString(16)}`;
|
|
118
|
+
it(`agrees on get() after every op (workload ${run}, seed ${seed_hex})`, async () => {
|
|
119
|
+
for (const op of plan.ops) {
|
|
120
|
+
for (const { cache } of live) await apply_op(cache, op);
|
|
121
|
+
const reference = await snapshot(live[0].cache, plan.streams);
|
|
122
|
+
for (let i = 1; i < live.length; i++) {
|
|
123
|
+
const actual = await snapshot(live[i].cache, plan.streams);
|
|
124
|
+
expect(
|
|
125
|
+
actual,
|
|
126
|
+
`${live[i].name} diverged from ${live[0].name} after ${op.t}`
|
|
127
|
+
).toEqual(reference);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
1
135
|
// src/cache-tck.ts
|
|
2
|
-
import { afterEach, beforeEach as beforeEach2, describe, expect, it } from "vitest";
|
|
136
|
+
import { afterEach, beforeEach as beforeEach2, describe as describe2, expect as expect2, it as it2 } from "vitest";
|
|
3
137
|
var entry = (event_id, state = {}) => ({
|
|
4
138
|
state,
|
|
5
139
|
version: event_id,
|
|
@@ -8,7 +142,7 @@ var entry = (event_id, state = {}) => ({
|
|
|
8
142
|
snaps: 0
|
|
9
143
|
});
|
|
10
144
|
var runCacheTck = (options) => {
|
|
11
|
-
|
|
145
|
+
describe2(`TCK / Cache / ${options.name}`, () => {
|
|
12
146
|
let cache;
|
|
13
147
|
beforeEach2(() => {
|
|
14
148
|
cache = options.factory();
|
|
@@ -16,55 +150,55 @@ var runCacheTck = (options) => {
|
|
|
16
150
|
afterEach(async () => {
|
|
17
151
|
await cache.dispose();
|
|
18
152
|
});
|
|
19
|
-
|
|
20
|
-
|
|
153
|
+
it2("returns undefined for an unset stream", async () => {
|
|
154
|
+
expect2(await cache.get("missing")).toBeUndefined();
|
|
21
155
|
});
|
|
22
|
-
|
|
156
|
+
it2("set then get round-trips an entry", async () => {
|
|
23
157
|
const e = entry(1, { count: 7 });
|
|
24
158
|
await cache.set("s1", e);
|
|
25
|
-
|
|
159
|
+
expect2(await cache.get("s1")).toEqual(e);
|
|
26
160
|
});
|
|
27
|
-
|
|
161
|
+
it2("set overwrites a prior entry on the same stream", async () => {
|
|
28
162
|
await cache.set("s1", entry(1, { count: 1 }));
|
|
29
163
|
await cache.set("s1", entry(2, { count: 2 }));
|
|
30
164
|
const got = await cache.get("s1");
|
|
31
|
-
|
|
32
|
-
|
|
165
|
+
expect2(got?.event_id).toBe(2);
|
|
166
|
+
expect2(got?.state).toEqual({ count: 2 });
|
|
33
167
|
});
|
|
34
|
-
|
|
168
|
+
it2("invalidate removes one stream and leaves others", async () => {
|
|
35
169
|
await cache.set("a", entry(1));
|
|
36
170
|
await cache.set("b", entry(2));
|
|
37
171
|
await cache.invalidate("a");
|
|
38
|
-
|
|
39
|
-
|
|
172
|
+
expect2(await cache.get("a")).toBeUndefined();
|
|
173
|
+
expect2(await cache.get("b")).toBeDefined();
|
|
40
174
|
});
|
|
41
|
-
|
|
42
|
-
await
|
|
175
|
+
it2("invalidate on an unknown stream is a no-op", async () => {
|
|
176
|
+
await expect2(cache.invalidate("never-set")).resolves.toBeUndefined();
|
|
43
177
|
});
|
|
44
|
-
|
|
178
|
+
it2("clear empties every stream", async () => {
|
|
45
179
|
await cache.set("a", entry(1));
|
|
46
180
|
await cache.set("b", entry(2));
|
|
47
181
|
await cache.set("c", entry(3));
|
|
48
182
|
await cache.clear();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
183
|
+
expect2(await cache.get("a")).toBeUndefined();
|
|
184
|
+
expect2(await cache.get("b")).toBeUndefined();
|
|
185
|
+
expect2(await cache.get("c")).toBeUndefined();
|
|
52
186
|
});
|
|
53
|
-
|
|
54
|
-
await
|
|
187
|
+
it2("clear on an empty cache is a no-op", async () => {
|
|
188
|
+
await expect2(cache.clear()).resolves.toBeUndefined();
|
|
55
189
|
});
|
|
56
|
-
|
|
190
|
+
it2("entries are isolated per stream", async () => {
|
|
57
191
|
const ea = entry(1, { id: "a" });
|
|
58
192
|
const eb = entry(2, { id: "b" });
|
|
59
193
|
await cache.set("a", ea);
|
|
60
194
|
await cache.set("b", eb);
|
|
61
|
-
|
|
62
|
-
|
|
195
|
+
expect2(await cache.get("a")).toEqual(ea);
|
|
196
|
+
expect2(await cache.get("b")).toEqual(eb);
|
|
63
197
|
});
|
|
64
|
-
|
|
198
|
+
it2("dispose is idempotent", async () => {
|
|
65
199
|
await cache.set("a", entry(1));
|
|
66
200
|
await cache.dispose();
|
|
67
|
-
await
|
|
201
|
+
await expect2(cache.dispose()).resolves.toBeUndefined();
|
|
68
202
|
});
|
|
69
203
|
});
|
|
70
204
|
};
|
|
@@ -85,105 +219,122 @@ var COUNTER_EVENT_NAMES = [
|
|
|
85
219
|
"Reset"
|
|
86
220
|
];
|
|
87
221
|
|
|
88
|
-
// src/
|
|
89
|
-
import {
|
|
90
|
-
var
|
|
91
|
-
var
|
|
92
|
-
var make_meta = (opts = {}) => ({
|
|
93
|
-
correlation: opts.correlation ?? randomUUID(),
|
|
94
|
-
causation: opts.stream ? {
|
|
95
|
-
action: {
|
|
96
|
-
name: opts.action ?? "Test",
|
|
97
|
-
stream: opts.stream,
|
|
98
|
-
actor: actor()
|
|
99
|
-
}
|
|
100
|
-
} : {}
|
|
101
|
-
});
|
|
102
|
-
var inc = (amount = 1) => ({
|
|
103
|
-
name: "Incremented",
|
|
104
|
-
data: { amount }
|
|
105
|
-
});
|
|
106
|
-
var dec = (amount = 1) => ({
|
|
107
|
-
name: "Decremented",
|
|
108
|
-
data: { amount }
|
|
109
|
-
});
|
|
110
|
-
var reset = () => ({ name: "Reset", data: {} });
|
|
111
|
-
var seed_stream = async (store, stream, count, correlation) => {
|
|
222
|
+
// src/logger-differential-tck.ts
|
|
223
|
+
import { afterEach as afterEach2, beforeEach as beforeEach3, describe as describe3, expect as expect3, it as it3 } from "vitest";
|
|
224
|
+
var LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"];
|
|
225
|
+
var drive = (logger) => {
|
|
112
226
|
const out = [];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
227
|
+
const ok = (fn) => {
|
|
228
|
+
try {
|
|
229
|
+
fn();
|
|
230
|
+
out.push(true);
|
|
231
|
+
} catch {
|
|
232
|
+
out.push(false);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
out.push(typeof logger.level === "string" && logger.level.length > 0);
|
|
236
|
+
for (const level of LEVELS) {
|
|
237
|
+
ok(() => logger[level]("message"));
|
|
238
|
+
ok(() => logger[level]({ k: "v", n: 1 }));
|
|
239
|
+
ok(() => logger[level]({ k: "v" }, "context"));
|
|
120
240
|
}
|
|
241
|
+
ok(() => logger.info(null, "null payload"));
|
|
242
|
+
ok(() => {
|
|
243
|
+
const cyclic = { name: "loop" };
|
|
244
|
+
cyclic.self = cyclic;
|
|
245
|
+
logger.info(cyclic, "cycle");
|
|
246
|
+
});
|
|
247
|
+
const child = logger.child({ request_id: "abc" });
|
|
248
|
+
out.push(typeof child.level === "string" && child.level.length > 0);
|
|
249
|
+
out.push(LEVELS.every((level) => typeof child[level] === "function"));
|
|
250
|
+
out.push(typeof child.child === "function");
|
|
251
|
+
ok(() => child.child({ nested: true }).info("nested"));
|
|
121
252
|
return out;
|
|
122
253
|
};
|
|
123
|
-
var
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
254
|
+
var runLoggerDifferentialTck = (options) => {
|
|
255
|
+
describe3(`TCK / Logger differential / ${options.name}`, () => {
|
|
256
|
+
let live = [];
|
|
257
|
+
let original_stdout;
|
|
258
|
+
beforeEach3(() => {
|
|
259
|
+
live = options.loggers.map((spec) => ({
|
|
260
|
+
name: spec.name,
|
|
261
|
+
logger: spec.factory()
|
|
262
|
+
}));
|
|
263
|
+
original_stdout = process.stdout.write.bind(process.stdout);
|
|
264
|
+
process.stdout.write = (() => true);
|
|
265
|
+
});
|
|
266
|
+
afterEach2(async () => {
|
|
267
|
+
process.stdout.write = original_stdout;
|
|
268
|
+
for (const { logger } of live) await logger.dispose();
|
|
269
|
+
});
|
|
270
|
+
it3("agrees on robustness and structural parity across the call surface", () => {
|
|
271
|
+
const reference = drive(live[0].logger);
|
|
272
|
+
for (let i = 1; i < live.length; i++) {
|
|
273
|
+
const actual = drive(live[i].logger);
|
|
274
|
+
expect3(actual, `${live[i].name} diverged from ${live[0].name}`).toEqual(
|
|
275
|
+
reference
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
129
280
|
};
|
|
130
281
|
|
|
131
282
|
// src/logger-tck.ts
|
|
132
|
-
import { afterEach as
|
|
133
|
-
var
|
|
283
|
+
import { afterEach as afterEach3, beforeEach as beforeEach4, describe as describe4, expect as expect4, it as it4 } from "vitest";
|
|
284
|
+
var LEVELS2 = ["fatal", "error", "warn", "info", "debug", "trace"];
|
|
134
285
|
var runLoggerTck = (options) => {
|
|
135
|
-
|
|
286
|
+
describe4(`TCK / Logger / ${options.name}`, () => {
|
|
136
287
|
let logger;
|
|
137
288
|
let original_stdout;
|
|
138
|
-
|
|
289
|
+
beforeEach4(() => {
|
|
139
290
|
logger = options.factory();
|
|
140
291
|
original_stdout = process.stdout.write.bind(process.stdout);
|
|
141
292
|
process.stdout.write = (() => true);
|
|
142
293
|
});
|
|
143
|
-
|
|
294
|
+
afterEach3(async () => {
|
|
144
295
|
process.stdout.write = original_stdout;
|
|
145
296
|
await logger.dispose();
|
|
146
297
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
298
|
+
it4("exposes a non-empty `level` string", () => {
|
|
299
|
+
expect4(typeof logger.level).toBe("string");
|
|
300
|
+
expect4(logger.level.length).toBeGreaterThan(0);
|
|
150
301
|
});
|
|
151
|
-
for (const level of
|
|
152
|
-
|
|
153
|
-
|
|
302
|
+
for (const level of LEVELS2) {
|
|
303
|
+
it4(`${level}(msg) does not throw`, () => {
|
|
304
|
+
expect4(() => logger[level]("hello")).not.toThrow();
|
|
154
305
|
});
|
|
155
|
-
|
|
156
|
-
|
|
306
|
+
it4(`${level}(obj) does not throw`, () => {
|
|
307
|
+
expect4(() => logger[level]({ k: "v" })).not.toThrow();
|
|
157
308
|
});
|
|
158
|
-
|
|
159
|
-
|
|
309
|
+
it4(`${level}(obj, msg) does not throw`, () => {
|
|
310
|
+
expect4(() => logger[level]({ k: "v" }, "context")).not.toThrow();
|
|
160
311
|
});
|
|
161
312
|
}
|
|
162
|
-
|
|
163
|
-
|
|
313
|
+
it4("accepts a null payload", () => {
|
|
314
|
+
expect4(() => logger.info(null, "null payload")).not.toThrow();
|
|
164
315
|
});
|
|
165
|
-
|
|
316
|
+
it4("accepts a cyclic payload without throwing", () => {
|
|
166
317
|
const cyclic = { name: "loop" };
|
|
167
318
|
cyclic.self = cyclic;
|
|
168
|
-
|
|
319
|
+
expect4(() => logger.info(cyclic, "cycle")).not.toThrow();
|
|
169
320
|
});
|
|
170
|
-
|
|
321
|
+
it4("child(bindings) returns a Logger satisfying the same contract", () => {
|
|
171
322
|
const child = logger.child({ request_id: "abc" });
|
|
172
|
-
|
|
173
|
-
for (const level of
|
|
174
|
-
|
|
323
|
+
expect4(typeof child.level).toBe("string");
|
|
324
|
+
for (const level of LEVELS2) {
|
|
325
|
+
expect4(typeof child[level]).toBe("function");
|
|
175
326
|
}
|
|
176
|
-
|
|
177
|
-
|
|
327
|
+
expect4(typeof child.child).toBe("function");
|
|
328
|
+
expect4(typeof child.dispose).toBe("function");
|
|
178
329
|
});
|
|
179
|
-
|
|
330
|
+
it4("child loggers can themselves spawn children", () => {
|
|
180
331
|
const c1 = logger.child({ a: 1 });
|
|
181
332
|
const c2 = c1.child({ b: 2 });
|
|
182
|
-
|
|
333
|
+
expect4(() => c2.info("nested")).not.toThrow();
|
|
183
334
|
});
|
|
184
|
-
|
|
185
|
-
await
|
|
186
|
-
await
|
|
335
|
+
it4("dispose is idempotent and awaitable", async () => {
|
|
336
|
+
await expect4(logger.dispose()).resolves.toBeUndefined();
|
|
337
|
+
await expect4(logger.dispose()).resolves.toBeUndefined();
|
|
187
338
|
});
|
|
188
339
|
});
|
|
189
340
|
};
|
|
@@ -191,14 +342,14 @@ var runLoggerTck = (options) => {
|
|
|
191
342
|
// src/stability-tck.ts
|
|
192
343
|
import { promises as fs } from "fs";
|
|
193
344
|
import path from "path";
|
|
194
|
-
import { describe as
|
|
345
|
+
import { describe as describe5, expect as expect5, it as it5 } from "vitest";
|
|
195
346
|
function runStabilityTck(options) {
|
|
196
|
-
|
|
347
|
+
describe5(`${options.name} \u2014 public API stability`, () => {
|
|
197
348
|
for (const [subpath, entry2] of Object.entries(options.entryPoints)) {
|
|
198
349
|
const label = subpath || "(root)";
|
|
199
|
-
|
|
350
|
+
it5(`stable public surface for ${label}`, async () => {
|
|
200
351
|
const surface = await load_surface(entry2);
|
|
201
|
-
|
|
352
|
+
expect5(surface).toMatchSnapshot();
|
|
202
353
|
});
|
|
203
354
|
}
|
|
204
355
|
});
|
|
@@ -228,9 +379,220 @@ ${content}`;
|
|
|
228
379
|
}).join("\n");
|
|
229
380
|
}
|
|
230
381
|
|
|
382
|
+
// src/store-differential-tck.ts
|
|
383
|
+
import { SNAP_EVENT } from "@rotorsoft/act";
|
|
384
|
+
import { afterAll as afterAll2, beforeAll as beforeAll2, describe as describe6, expect as expect6, it as it6 } from "vitest";
|
|
385
|
+
var mulberry322 = (seed) => {
|
|
386
|
+
let a = seed >>> 0;
|
|
387
|
+
return () => {
|
|
388
|
+
a = a + 1831565813 | 0;
|
|
389
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
390
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
391
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
392
|
+
};
|
|
393
|
+
};
|
|
394
|
+
var normalize_event = (e) => ({
|
|
395
|
+
stream: e.stream,
|
|
396
|
+
version: e.version,
|
|
397
|
+
name: e.name,
|
|
398
|
+
data: e.data
|
|
399
|
+
});
|
|
400
|
+
var build_plan = (seed, stream_count) => {
|
|
401
|
+
const rng = mulberry322(seed);
|
|
402
|
+
const tag = uid();
|
|
403
|
+
const event_prefix = `diff-${tag}-evt-`;
|
|
404
|
+
const sub_prefix = `diff-${tag}-sub-`;
|
|
405
|
+
const event_streams = Array.from(
|
|
406
|
+
{ length: stream_count },
|
|
407
|
+
(_, i) => `${event_prefix}${i}`
|
|
408
|
+
);
|
|
409
|
+
let type_cursor = 0;
|
|
410
|
+
const next_msg = () => {
|
|
411
|
+
const kind = type_cursor++ % 3;
|
|
412
|
+
const amount = 1 + Math.floor(rng() * 9);
|
|
413
|
+
return kind === 0 ? inc(amount) : kind === 1 ? dec(amount) : reset();
|
|
414
|
+
};
|
|
415
|
+
const batch = () => Array.from({ length: 1 + Math.floor(rng() * 3) }, next_msg);
|
|
416
|
+
const pick_stream = () => event_streams[Math.floor(rng() * event_streams.length)];
|
|
417
|
+
const ops = [];
|
|
418
|
+
for (const stream of event_streams)
|
|
419
|
+
ops.push({ t: "commit", stream, msgs: batch() });
|
|
420
|
+
const middle = 8 + Math.floor(rng() * 16);
|
|
421
|
+
for (let i = 0; i < middle; i++) {
|
|
422
|
+
const stream = pick_stream();
|
|
423
|
+
const kind = Math.floor(rng() * 3);
|
|
424
|
+
if (kind === 0) ops.push({ t: "commit", stream, msgs: batch() });
|
|
425
|
+
else if (kind === 1)
|
|
426
|
+
ops.push({ t: "snapshot", stream, count: Math.floor(rng() * 1e3) });
|
|
427
|
+
else ops.push({ t: "truncate", stream, count: Math.floor(rng() * 1e3) });
|
|
428
|
+
}
|
|
429
|
+
for (const stream of event_streams)
|
|
430
|
+
ops.push({ t: "commit", stream, msgs: batch() });
|
|
431
|
+
const lanes = ["default", "slow", "fast"];
|
|
432
|
+
const subs = event_streams.map((source, i) => ({
|
|
433
|
+
stream: `${sub_prefix}${i}`,
|
|
434
|
+
source,
|
|
435
|
+
lane: lanes[i % lanes.length],
|
|
436
|
+
priority: i % 4
|
|
437
|
+
}));
|
|
438
|
+
return { event_prefix, event_streams, sub_prefix, ops, subs };
|
|
439
|
+
};
|
|
440
|
+
var apply_plan = async (store, plan) => {
|
|
441
|
+
for (const op of plan.ops) {
|
|
442
|
+
if (op.t === "commit") {
|
|
443
|
+
await store.commit(
|
|
444
|
+
op.stream,
|
|
445
|
+
op.msgs,
|
|
446
|
+
make_meta({ stream: op.stream })
|
|
447
|
+
);
|
|
448
|
+
} else if (op.t === "snapshot") {
|
|
449
|
+
await store.commit(
|
|
450
|
+
op.stream,
|
|
451
|
+
[{ name: SNAP_EVENT, data: { count: op.count } }],
|
|
452
|
+
make_meta({ stream: op.stream })
|
|
453
|
+
);
|
|
454
|
+
} else {
|
|
455
|
+
await store.truncate([
|
|
456
|
+
{ stream: op.stream, snapshot: { count: op.count } }
|
|
457
|
+
]);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
await store.subscribe(
|
|
461
|
+
plan.subs.map((s) => ({
|
|
462
|
+
stream: s.stream,
|
|
463
|
+
source: s.source,
|
|
464
|
+
lane: s.lane,
|
|
465
|
+
priority: s.priority
|
|
466
|
+
}))
|
|
467
|
+
);
|
|
468
|
+
};
|
|
469
|
+
var runStoreDifferentialTck = (options) => {
|
|
470
|
+
describe6(`TCK / Store differential / ${options.name}`, () => {
|
|
471
|
+
const base_seed = options.seed ?? 2759;
|
|
472
|
+
const stream_count = options.streams ?? 4;
|
|
473
|
+
const plans = Array.from(
|
|
474
|
+
{ length: options.runs ?? 8 },
|
|
475
|
+
(_, r) => build_plan(base_seed + r, stream_count)
|
|
476
|
+
);
|
|
477
|
+
const live = [];
|
|
478
|
+
beforeAll2(async () => {
|
|
479
|
+
for (const spec of options.stores) {
|
|
480
|
+
const store = await spec.factory();
|
|
481
|
+
await store.drop();
|
|
482
|
+
await store.seed();
|
|
483
|
+
for (const plan of plans) await apply_plan(store, plan);
|
|
484
|
+
live.push({ name: spec.name, store });
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
afterAll2(async () => {
|
|
488
|
+
for (const { store } of live) await store.dispose();
|
|
489
|
+
});
|
|
490
|
+
const assert_identical = async (label, produce) => {
|
|
491
|
+
const reference = await produce(live[0].store);
|
|
492
|
+
for (let i = 1; i < live.length; i++) {
|
|
493
|
+
const actual = await produce(live[i].store);
|
|
494
|
+
expect6(
|
|
495
|
+
actual,
|
|
496
|
+
`${live[i].name} diverged from ${live[0].name} on "${label}"`
|
|
497
|
+
).toEqual(reference);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
plans.forEach((plan, run) => {
|
|
501
|
+
const seed_hex = `0x${(base_seed + run).toString(16)}`;
|
|
502
|
+
describe6(`workload ${run} (seed ${seed_hex})`, () => {
|
|
503
|
+
it6("yields identical event order under a global forward query", async () => {
|
|
504
|
+
await assert_identical("forward query", async (store) => {
|
|
505
|
+
const out = [];
|
|
506
|
+
await store.query(
|
|
507
|
+
(e) => {
|
|
508
|
+
out.push(normalize_event(e));
|
|
509
|
+
},
|
|
510
|
+
{ stream: `^${plan.event_prefix}` }
|
|
511
|
+
);
|
|
512
|
+
return out;
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
it6("yields identical snapshot floors under with_snaps", async () => {
|
|
516
|
+
await assert_identical("with_snaps floor", async (store) => {
|
|
517
|
+
const by_stream = {};
|
|
518
|
+
for (const stream of plan.event_streams) {
|
|
519
|
+
const out = [];
|
|
520
|
+
await store.query(
|
|
521
|
+
(e) => {
|
|
522
|
+
out.push(normalize_event(e));
|
|
523
|
+
},
|
|
524
|
+
{ stream, stream_exact: true, with_snaps: true }
|
|
525
|
+
);
|
|
526
|
+
by_stream[stream] = out;
|
|
527
|
+
}
|
|
528
|
+
return by_stream;
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
it6("yields identical order under backward traversal", async () => {
|
|
532
|
+
await assert_identical("backward query", async (store) => {
|
|
533
|
+
const by_stream = {};
|
|
534
|
+
for (const stream of plan.event_streams) {
|
|
535
|
+
const out = [];
|
|
536
|
+
await store.query(
|
|
537
|
+
(e) => {
|
|
538
|
+
out.push(normalize_event(e));
|
|
539
|
+
},
|
|
540
|
+
{ stream, stream_exact: true, backward: true }
|
|
541
|
+
);
|
|
542
|
+
by_stream[stream] = out;
|
|
543
|
+
}
|
|
544
|
+
return by_stream;
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
it6("yields identical query_stats output (head/tail/count/names)", async () => {
|
|
548
|
+
await assert_identical("query_stats", async (store) => {
|
|
549
|
+
const stats = await store.query_stats(
|
|
550
|
+
{ stream: `^${plan.event_prefix}` },
|
|
551
|
+
{ tail: true, count: true, names: true }
|
|
552
|
+
);
|
|
553
|
+
const keys = [...stats.keys()];
|
|
554
|
+
const content = {};
|
|
555
|
+
for (const [stream, s] of stats) {
|
|
556
|
+
content[stream] = {
|
|
557
|
+
head: normalize_event(s.head),
|
|
558
|
+
tail: normalize_event(
|
|
559
|
+
s.tail
|
|
560
|
+
),
|
|
561
|
+
count: s.count,
|
|
562
|
+
names: s.names
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return { keys, content };
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
it6("yields identical query_streams output", async () => {
|
|
569
|
+
await assert_identical("query_streams", async (store) => {
|
|
570
|
+
const rows = [];
|
|
571
|
+
const { count } = await store.query_streams(
|
|
572
|
+
(p) => {
|
|
573
|
+
rows.push({
|
|
574
|
+
stream: p.stream,
|
|
575
|
+
source: p.source,
|
|
576
|
+
at: p.at,
|
|
577
|
+
blocked: p.blocked,
|
|
578
|
+
priority: p.priority,
|
|
579
|
+
lane: p.lane
|
|
580
|
+
});
|
|
581
|
+
},
|
|
582
|
+
{ stream: `^${plan.sub_prefix}`, limit: 1e3 }
|
|
583
|
+
);
|
|
584
|
+
rows.sort((a, b) => a.stream.localeCompare(b.stream));
|
|
585
|
+
return { count, rows };
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
|
|
231
593
|
// src/store-property-tck.ts
|
|
232
594
|
import { fc, test } from "@fast-check/vitest";
|
|
233
|
-
import { afterAll, beforeAll, describe as
|
|
595
|
+
import { afterAll as afterAll3, beforeAll as beforeAll3, describe as describe7, expect as expect7 } from "vitest";
|
|
234
596
|
var streamArb = fc.constantFrom("s1", "s2", "s3");
|
|
235
597
|
var commitArb = fc.record({
|
|
236
598
|
stream: streamArb,
|
|
@@ -246,20 +608,20 @@ var opArb = fc.oneof(
|
|
|
246
608
|
var events = (count) => Array.from({ length: count }, () => inc(1));
|
|
247
609
|
var runStorePropertyTck = (options) => {
|
|
248
610
|
const numRuns = options.numRuns ?? 100;
|
|
249
|
-
|
|
611
|
+
describe7(`TCK / Store properties / ${options.name}`, () => {
|
|
250
612
|
let store;
|
|
251
|
-
|
|
613
|
+
beforeAll3(async () => {
|
|
252
614
|
store = await options.factory();
|
|
253
615
|
await store.seed();
|
|
254
616
|
});
|
|
255
|
-
|
|
617
|
+
afterAll3(async () => {
|
|
256
618
|
await store.dispose();
|
|
257
619
|
});
|
|
258
620
|
const reset2 = async () => {
|
|
259
621
|
await store.drop();
|
|
260
622
|
await store.seed();
|
|
261
623
|
};
|
|
262
|
-
|
|
624
|
+
describe7("commit version invariants", () => {
|
|
263
625
|
test.prop([fc.array(commitArb, { minLength: 0, maxLength: 30 })], {
|
|
264
626
|
numRuns
|
|
265
627
|
})(
|
|
@@ -275,14 +637,14 @@ var runStorePropertyTck = (options) => {
|
|
|
275
637
|
make_meta({ stream })
|
|
276
638
|
);
|
|
277
639
|
committed.forEach((e, i) => {
|
|
278
|
-
|
|
640
|
+
expect7(e.version).toBe(before + 1 + i);
|
|
279
641
|
});
|
|
280
642
|
expected.set(stream, before + count);
|
|
281
643
|
}
|
|
282
644
|
for (const stream of new Set(commits.map((c) => c.stream))) {
|
|
283
645
|
const seen = await collect(store, { stream, stream_exact: true });
|
|
284
646
|
seen.forEach((e, i) => {
|
|
285
|
-
|
|
647
|
+
expect7(e.version).toBe(i);
|
|
286
648
|
});
|
|
287
649
|
}
|
|
288
650
|
}
|
|
@@ -302,7 +664,7 @@ var runStorePropertyTck = (options) => {
|
|
|
302
664
|
}
|
|
303
665
|
const stream = commits[0].stream;
|
|
304
666
|
const before = await collect(store, { stream, stream_exact: true });
|
|
305
|
-
await
|
|
667
|
+
await expect7(
|
|
306
668
|
store.commit(
|
|
307
669
|
stream,
|
|
308
670
|
[inc(1)],
|
|
@@ -311,11 +673,11 @@ var runStorePropertyTck = (options) => {
|
|
|
311
673
|
)
|
|
312
674
|
).rejects.toThrow();
|
|
313
675
|
const after = await collect(store, { stream, stream_exact: true });
|
|
314
|
-
|
|
676
|
+
expect7(after.length).toBe(before.length);
|
|
315
677
|
}
|
|
316
678
|
);
|
|
317
679
|
});
|
|
318
|
-
|
|
680
|
+
describe7("claim/lease lifecycle invariants", () => {
|
|
319
681
|
test.prop([fc.array(opArb, { minLength: 1, maxLength: 30 })], {
|
|
320
682
|
numRuns
|
|
321
683
|
})(
|
|
@@ -357,7 +719,7 @@ var runStorePropertyTck = (options) => {
|
|
|
357
719
|
);
|
|
358
720
|
}
|
|
359
721
|
}
|
|
360
|
-
|
|
722
|
+
expect7(totalResolved + pending.length).toBe(totalClaims);
|
|
361
723
|
}
|
|
362
724
|
);
|
|
363
725
|
test.prop(
|
|
@@ -395,7 +757,7 @@ var runStorePropertyTck = (options) => {
|
|
|
395
757
|
await store.claim(10, 10, "worker", 6e4)
|
|
396
758
|
);
|
|
397
759
|
for (const lease of acked2) {
|
|
398
|
-
|
|
760
|
+
expect7(lease.at).toBeGreaterThanOrEqual(
|
|
399
761
|
watermark1.get(lease.stream)
|
|
400
762
|
);
|
|
401
763
|
}
|
|
@@ -424,7 +786,7 @@ var runStorePropertyTck = (options) => {
|
|
|
424
786
|
make_meta({ stream: "ctrl" })
|
|
425
787
|
);
|
|
426
788
|
const reclaim = await store.claim(10, 10, "worker2", 6e4);
|
|
427
|
-
for (const l of reclaim)
|
|
789
|
+
for (const l of reclaim) expect7(blockedSet.has(l.stream)).toBe(false);
|
|
428
790
|
});
|
|
429
791
|
});
|
|
430
792
|
});
|
|
@@ -435,41 +797,41 @@ import {
|
|
|
435
797
|
act,
|
|
436
798
|
ConcurrencyError,
|
|
437
799
|
InMemoryCache,
|
|
438
|
-
SNAP_EVENT,
|
|
800
|
+
SNAP_EVENT as SNAP_EVENT2,
|
|
439
801
|
TOMBSTONE_EVENT
|
|
440
802
|
} from "@rotorsoft/act";
|
|
441
|
-
import { afterAll as
|
|
803
|
+
import { afterAll as afterAll4, beforeAll as beforeAll4, describe as describe8, expect as expect8, it as it7 } from "vitest";
|
|
442
804
|
var runStoreTck = (options) => {
|
|
443
|
-
|
|
805
|
+
describe8(`TCK / Store / ${options.name}`, () => {
|
|
444
806
|
let store;
|
|
445
807
|
const caps = { ...options.capabilities };
|
|
446
|
-
|
|
808
|
+
beforeAll4(async () => {
|
|
447
809
|
store = await options.factory();
|
|
448
810
|
await store.drop();
|
|
449
811
|
await store.seed();
|
|
450
812
|
});
|
|
451
|
-
|
|
813
|
+
afterAll4(async () => {
|
|
452
814
|
await store.dispose();
|
|
453
815
|
});
|
|
454
|
-
|
|
455
|
-
|
|
816
|
+
describe8("commit", () => {
|
|
817
|
+
it7("returns committed events with sequenced ids and versions", async () => {
|
|
456
818
|
const s = `commit-seq-${uid()}`;
|
|
457
819
|
const committed = await store.commit(
|
|
458
820
|
s,
|
|
459
821
|
[inc(1), inc(2), dec(3)],
|
|
460
822
|
make_meta({ stream: s })
|
|
461
823
|
);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
824
|
+
expect8(committed).toHaveLength(3);
|
|
825
|
+
expect8(committed[0].version).toBe(0);
|
|
826
|
+
expect8(committed[1].version).toBe(1);
|
|
827
|
+
expect8(committed[2].version).toBe(2);
|
|
828
|
+
expect8(committed[0].name).toBe("Incremented");
|
|
829
|
+
expect8(committed[2].data).toEqual({ amount: 3 });
|
|
468
830
|
for (let i = 1; i < committed.length; i++) {
|
|
469
|
-
|
|
831
|
+
expect8(committed[i].id).toBeGreaterThan(committed[i - 1].id);
|
|
470
832
|
}
|
|
471
833
|
});
|
|
472
|
-
|
|
834
|
+
it7("attaches correlation and stream metadata", async () => {
|
|
473
835
|
const s = `commit-meta-${uid()}`;
|
|
474
836
|
const correlation = `cor-${uid()}`;
|
|
475
837
|
const committed = await store.commit(
|
|
@@ -477,10 +839,10 @@ var runStoreTck = (options) => {
|
|
|
477
839
|
[inc(1)],
|
|
478
840
|
make_meta({ stream: s, correlation })
|
|
479
841
|
);
|
|
480
|
-
|
|
481
|
-
|
|
842
|
+
expect8(committed[0].stream).toBe(s);
|
|
843
|
+
expect8(committed[0].meta.correlation).toBe(correlation);
|
|
482
844
|
});
|
|
483
|
-
|
|
845
|
+
it7("throws ConcurrencyError when expectedVersion is wrong", async () => {
|
|
484
846
|
const s = `commit-cc-${uid()}`;
|
|
485
847
|
await store.commit(
|
|
486
848
|
s,
|
|
@@ -493,26 +855,26 @@ var runStoreTck = (options) => {
|
|
|
493
855
|
make_meta({ stream: s }),
|
|
494
856
|
0
|
|
495
857
|
);
|
|
496
|
-
await
|
|
858
|
+
await expect8(
|
|
497
859
|
store.commit(s, [inc(1)], make_meta({ stream: s }), 0)
|
|
498
860
|
).rejects.toBeInstanceOf(ConcurrencyError);
|
|
499
861
|
});
|
|
500
|
-
|
|
862
|
+
it7("preserves prior events when a concurrent commit is rejected", async () => {
|
|
501
863
|
const s = `commit-cc-preserve-${uid()}`;
|
|
502
864
|
await store.commit(
|
|
503
865
|
s,
|
|
504
866
|
[inc(1), inc(2)],
|
|
505
867
|
make_meta({ stream: s })
|
|
506
868
|
);
|
|
507
|
-
await
|
|
869
|
+
await expect8(
|
|
508
870
|
store.commit(s, [inc(3)], make_meta({ stream: s }), 0)
|
|
509
871
|
).rejects.toBeInstanceOf(ConcurrencyError);
|
|
510
872
|
const found = await collect(store, { stream: s, stream_exact: true });
|
|
511
|
-
|
|
873
|
+
expect8(found).toHaveLength(2);
|
|
512
874
|
});
|
|
513
875
|
});
|
|
514
|
-
|
|
515
|
-
|
|
876
|
+
describe8("query", () => {
|
|
877
|
+
it7("filters by stream, names, correlation, limit, with_snaps", async () => {
|
|
516
878
|
const s1 = `q-s1-${uid()}`;
|
|
517
879
|
const s2 = `q-s2-${uid()}`;
|
|
518
880
|
const cor = `q-cor-${uid()}`;
|
|
@@ -530,23 +892,69 @@ var runStoreTck = (options) => {
|
|
|
530
892
|
stream: s1,
|
|
531
893
|
stream_exact: true
|
|
532
894
|
});
|
|
533
|
-
|
|
895
|
+
expect8(by_stream).toHaveLength(2);
|
|
534
896
|
const by_name = await collect(store, {
|
|
535
897
|
stream: s2,
|
|
536
898
|
stream_exact: true,
|
|
537
899
|
names: ["Reset"]
|
|
538
900
|
});
|
|
539
|
-
|
|
540
|
-
|
|
901
|
+
expect8(by_name).toHaveLength(1);
|
|
902
|
+
expect8(by_name[0].name).toBe("Reset");
|
|
541
903
|
const by_correlation = await collect(store, { correlation: cor });
|
|
542
|
-
|
|
904
|
+
expect8(by_correlation).toHaveLength(5);
|
|
543
905
|
const limited = await collect(store, {
|
|
544
906
|
correlation: cor,
|
|
545
907
|
limit: 2
|
|
546
908
|
});
|
|
547
|
-
|
|
909
|
+
expect8(limited).toHaveLength(2);
|
|
910
|
+
});
|
|
911
|
+
it7("with_snaps resumes from the latest snapshot per stream", async () => {
|
|
912
|
+
const s = `q-snap-${uid()}`;
|
|
913
|
+
await store.commit(
|
|
914
|
+
s,
|
|
915
|
+
[inc(1), inc(1)],
|
|
916
|
+
make_meta({ stream: s })
|
|
917
|
+
);
|
|
918
|
+
const [snap] = await store.commit(
|
|
919
|
+
s,
|
|
920
|
+
[{ name: SNAP_EVENT2, data: { count: 2 } }],
|
|
921
|
+
make_meta({ stream: s })
|
|
922
|
+
);
|
|
923
|
+
await store.commit(
|
|
924
|
+
s,
|
|
925
|
+
[inc(1), inc(1), inc(1)],
|
|
926
|
+
make_meta({ stream: s })
|
|
927
|
+
);
|
|
928
|
+
const from_snap = await collect(store, {
|
|
929
|
+
stream: s,
|
|
930
|
+
stream_exact: true,
|
|
931
|
+
with_snaps: true
|
|
932
|
+
});
|
|
933
|
+
expect8(from_snap).toHaveLength(4);
|
|
934
|
+
expect8(from_snap[0].name).toBe(SNAP_EVENT2);
|
|
935
|
+
const domain = await collect(store, { stream: s, stream_exact: true });
|
|
936
|
+
expect8(domain).toHaveLength(5);
|
|
937
|
+
const after_snap = await collect(store, {
|
|
938
|
+
stream: s,
|
|
939
|
+
stream_exact: true,
|
|
940
|
+
with_snaps: true,
|
|
941
|
+
after: snap.id
|
|
942
|
+
});
|
|
943
|
+
expect8(after_snap).toHaveLength(3);
|
|
944
|
+
const s2 = `q-nosnap-${uid()}`;
|
|
945
|
+
await store.commit(
|
|
946
|
+
s2,
|
|
947
|
+
[inc(1), inc(1)],
|
|
948
|
+
make_meta({ stream: s2 })
|
|
949
|
+
);
|
|
950
|
+
const full = await collect(store, {
|
|
951
|
+
stream: s2,
|
|
952
|
+
stream_exact: true,
|
|
953
|
+
with_snaps: true
|
|
954
|
+
});
|
|
955
|
+
expect8(full).toHaveLength(2);
|
|
548
956
|
});
|
|
549
|
-
|
|
957
|
+
it7("supports backward traversal", async () => {
|
|
550
958
|
const s = `q-back-${uid()}`;
|
|
551
959
|
const committed = await store.commit(
|
|
552
960
|
s,
|
|
@@ -559,8 +967,8 @@ var runStoreTck = (options) => {
|
|
|
559
967
|
stream_exact: true,
|
|
560
968
|
backward: true
|
|
561
969
|
});
|
|
562
|
-
|
|
563
|
-
|
|
970
|
+
expect8(forward.map((e) => e.id)).toEqual(committed.map((c) => c.id));
|
|
971
|
+
expect8(backward.map((e) => e.id)).toEqual(
|
|
564
972
|
[...committed].reverse().map((c) => c.id)
|
|
565
973
|
);
|
|
566
974
|
const latest = await collect(store, {
|
|
@@ -569,10 +977,10 @@ var runStoreTck = (options) => {
|
|
|
569
977
|
backward: true,
|
|
570
978
|
limit: 1
|
|
571
979
|
});
|
|
572
|
-
|
|
573
|
-
|
|
980
|
+
expect8(latest).toHaveLength(1);
|
|
981
|
+
expect8(latest[0].id).toBe(committed.at(-1).id);
|
|
574
982
|
});
|
|
575
|
-
|
|
983
|
+
it7("after/before bound the id range", async () => {
|
|
576
984
|
const s = `q-bounds-${uid()}`;
|
|
577
985
|
const committed = await store.commit(
|
|
578
986
|
s,
|
|
@@ -584,7 +992,7 @@ var runStoreTck = (options) => {
|
|
|
584
992
|
stream_exact: true,
|
|
585
993
|
after: committed[0].id
|
|
586
994
|
});
|
|
587
|
-
|
|
995
|
+
expect8(after_first.map((e) => e.id)).toEqual(
|
|
588
996
|
committed.slice(1).map((c) => c.id)
|
|
589
997
|
);
|
|
590
998
|
const before_last = await collect(store, {
|
|
@@ -592,11 +1000,11 @@ var runStoreTck = (options) => {
|
|
|
592
1000
|
stream_exact: true,
|
|
593
1001
|
before: committed[committed.length - 1].id
|
|
594
1002
|
});
|
|
595
|
-
|
|
1003
|
+
expect8(before_last.map((e) => e.id)).toEqual(
|
|
596
1004
|
committed.slice(0, -1).map((c) => c.id)
|
|
597
1005
|
);
|
|
598
1006
|
});
|
|
599
|
-
|
|
1007
|
+
it7("created_after/created_before filter by timestamp", async () => {
|
|
600
1008
|
const s = `q-ts-${uid()}`;
|
|
601
1009
|
const committed = await store.commit(
|
|
602
1010
|
s,
|
|
@@ -612,15 +1020,15 @@ var runStoreTck = (options) => {
|
|
|
612
1020
|
created_after: before,
|
|
613
1021
|
created_before: future
|
|
614
1022
|
});
|
|
615
|
-
|
|
1023
|
+
expect8(in_window.length).toBe(1);
|
|
616
1024
|
const out_of_window = await collect(store, {
|
|
617
1025
|
stream: s,
|
|
618
1026
|
stream_exact: true,
|
|
619
1027
|
created_after: future
|
|
620
1028
|
});
|
|
621
|
-
|
|
1029
|
+
expect8(out_of_window.length).toBe(0);
|
|
622
1030
|
});
|
|
623
|
-
|
|
1031
|
+
it7("backward traversal short-circuits at `after` id boundary", async () => {
|
|
624
1032
|
const s = `q-back-after-${uid()}`;
|
|
625
1033
|
const committed = await store.commit(
|
|
626
1034
|
s,
|
|
@@ -633,12 +1041,12 @@ var runStoreTck = (options) => {
|
|
|
633
1041
|
backward: true,
|
|
634
1042
|
after: committed[0].id
|
|
635
1043
|
});
|
|
636
|
-
|
|
1044
|
+
expect8(got.map((e) => e.id)).toEqual([
|
|
637
1045
|
committed[2].id,
|
|
638
1046
|
committed[1].id
|
|
639
1047
|
]);
|
|
640
1048
|
});
|
|
641
|
-
|
|
1049
|
+
it7("backward traversal short-circuits at `created_after` boundary", async () => {
|
|
642
1050
|
const s = `q-back-cafter-${uid()}`;
|
|
643
1051
|
await store.commit(
|
|
644
1052
|
s,
|
|
@@ -652,9 +1060,9 @@ var runStoreTck = (options) => {
|
|
|
652
1060
|
backward: true,
|
|
653
1061
|
created_after: future
|
|
654
1062
|
});
|
|
655
|
-
|
|
1063
|
+
expect8(got).toHaveLength(0);
|
|
656
1064
|
});
|
|
657
|
-
|
|
1065
|
+
it7("backward traversal honors created_before by skipping newer events", async () => {
|
|
658
1066
|
const s = `q-back-ts-${uid()}`;
|
|
659
1067
|
const committed = await store.commit(
|
|
660
1068
|
s,
|
|
@@ -668,9 +1076,9 @@ var runStoreTck = (options) => {
|
|
|
668
1076
|
backward: true,
|
|
669
1077
|
created_before: past
|
|
670
1078
|
});
|
|
671
|
-
|
|
1079
|
+
expect8(got).toHaveLength(0);
|
|
672
1080
|
});
|
|
673
|
-
|
|
1081
|
+
it7("stream_exact disables regex matching", async () => {
|
|
674
1082
|
const tag = uid();
|
|
675
1083
|
const a = `q-exact-${tag}`;
|
|
676
1084
|
const b = `q-exact-${tag}-extra`;
|
|
@@ -685,10 +1093,10 @@ var runStoreTck = (options) => {
|
|
|
685
1093
|
make_meta({ stream: b })
|
|
686
1094
|
);
|
|
687
1095
|
const exact = await collect(store, { stream: a, stream_exact: true });
|
|
688
|
-
|
|
689
|
-
|
|
1096
|
+
expect8(exact).toHaveLength(1);
|
|
1097
|
+
expect8(exact[0].data).toEqual({ amount: 1 });
|
|
690
1098
|
});
|
|
691
|
-
|
|
1099
|
+
it7("plain regex without anchors is a substring match", async () => {
|
|
692
1100
|
const tag = uid();
|
|
693
1101
|
const inner = `qr-${tag}-inner`;
|
|
694
1102
|
const longer = `qr-${tag}-inner-extra`;
|
|
@@ -703,9 +1111,9 @@ var runStoreTck = (options) => {
|
|
|
703
1111
|
make_meta({ stream: longer })
|
|
704
1112
|
);
|
|
705
1113
|
const got = await collect(store, { stream: `qr-${tag}-inner` });
|
|
706
|
-
|
|
1114
|
+
expect8(got.map((e) => e.stream).sort()).toEqual([inner, longer].sort());
|
|
707
1115
|
});
|
|
708
|
-
|
|
1116
|
+
it7("caller-anchored `^name$` matches only the whole string", async () => {
|
|
709
1117
|
const tag = uid();
|
|
710
1118
|
const inner = `qr-${tag}-anchor`;
|
|
711
1119
|
const longer = `qr-${tag}-anchor-extra`;
|
|
@@ -720,10 +1128,10 @@ var runStoreTck = (options) => {
|
|
|
720
1128
|
make_meta({ stream: longer })
|
|
721
1129
|
);
|
|
722
1130
|
const got = await collect(store, { stream: `^qr-${tag}-anchor$` });
|
|
723
|
-
|
|
724
|
-
|
|
1131
|
+
expect8(got).toHaveLength(1);
|
|
1132
|
+
expect8(got[0].stream).toBe(inner);
|
|
725
1133
|
});
|
|
726
|
-
|
|
1134
|
+
it7("caller-anchored `^prefix` matches by prefix", async () => {
|
|
727
1135
|
const tag = uid();
|
|
728
1136
|
const a = `qr-${tag}-pfx-a`;
|
|
729
1137
|
const b = `qr-${tag}-pfx-b`;
|
|
@@ -744,18 +1152,39 @@ var runStoreTck = (options) => {
|
|
|
744
1152
|
make_meta({ stream: other })
|
|
745
1153
|
);
|
|
746
1154
|
const got = await collect(store, { stream: `^qr-${tag}-pfx-` });
|
|
747
|
-
|
|
1155
|
+
expect8(got.map((e) => e.stream).sort()).toEqual([a, b].sort());
|
|
748
1156
|
});
|
|
749
1157
|
});
|
|
750
|
-
|
|
751
|
-
|
|
1158
|
+
describe8("subscribe + claim + ack", () => {
|
|
1159
|
+
it7("subscribes new streams and is idempotent on repeat", async () => {
|
|
752
1160
|
const s = `sub-${uid()}`;
|
|
753
1161
|
const first = await store.subscribe([{ stream: s }]);
|
|
754
|
-
|
|
1162
|
+
expect8(first.subscribed).toBe(1);
|
|
755
1163
|
const second = await store.subscribe([{ stream: s }]);
|
|
756
|
-
|
|
1164
|
+
expect8(second.subscribed).toBe(0);
|
|
1165
|
+
});
|
|
1166
|
+
it7("keeps the maximum priority when a stream is re-subscribed", async () => {
|
|
1167
|
+
const s = `sub-pri-${uid()}`;
|
|
1168
|
+
const read = async () => {
|
|
1169
|
+
const got = {};
|
|
1170
|
+
await store.query_streams(
|
|
1171
|
+
(p) => {
|
|
1172
|
+
got.priority = p.priority;
|
|
1173
|
+
},
|
|
1174
|
+
{ stream: s, stream_exact: true }
|
|
1175
|
+
);
|
|
1176
|
+
return got.priority;
|
|
1177
|
+
};
|
|
1178
|
+
await store.subscribe([{ stream: s, priority: 3 }]);
|
|
1179
|
+
expect8(await read()).toBe(3);
|
|
1180
|
+
await store.subscribe([{ stream: s, priority: 10 }]);
|
|
1181
|
+
expect8(await read()).toBe(10);
|
|
1182
|
+
await store.subscribe([{ stream: s, priority: 1 }]);
|
|
1183
|
+
expect8(await read()).toBe(10);
|
|
1184
|
+
await store.subscribe([{ stream: s }]);
|
|
1185
|
+
expect8(await read()).toBe(10);
|
|
757
1186
|
});
|
|
758
|
-
|
|
1187
|
+
it7("claims a subscribed stream and ack releases the lease", async () => {
|
|
759
1188
|
const s = `claim-${uid()}`;
|
|
760
1189
|
await store.subscribe([{ stream: s }]);
|
|
761
1190
|
await store.commit(
|
|
@@ -766,11 +1195,11 @@ var runStoreTck = (options) => {
|
|
|
766
1195
|
const by = `worker-${uid()}`;
|
|
767
1196
|
const leased = await store.claim(100, 0, by, 1e4);
|
|
768
1197
|
const mine = leased.find((l) => l.stream === s);
|
|
769
|
-
|
|
770
|
-
|
|
1198
|
+
expect8(mine).toBeDefined();
|
|
1199
|
+
expect8(mine.by).toBe(by);
|
|
771
1200
|
await store.ack([{ ...mine, at: mine.at + 1 }]);
|
|
772
1201
|
});
|
|
773
|
-
|
|
1202
|
+
it7("does not double-claim a held lease", async () => {
|
|
774
1203
|
const s = `claim-held-${uid()}`;
|
|
775
1204
|
const other = `claim-other-${uid()}`;
|
|
776
1205
|
await store.subscribe([{ stream: s }]);
|
|
@@ -781,7 +1210,7 @@ var runStoreTck = (options) => {
|
|
|
781
1210
|
);
|
|
782
1211
|
const leasedA = await store.claim(100, 0, `wA-${uid()}`, 1e5);
|
|
783
1212
|
const targetA = leasedA.find((l) => l.stream === s);
|
|
784
|
-
|
|
1213
|
+
expect8(targetA).toBeDefined();
|
|
785
1214
|
await store.subscribe([{ stream: other }]);
|
|
786
1215
|
await store.commit(
|
|
787
1216
|
other,
|
|
@@ -789,11 +1218,11 @@ var runStoreTck = (options) => {
|
|
|
789
1218
|
make_meta({ stream: other })
|
|
790
1219
|
);
|
|
791
1220
|
const leasedB = await store.claim(100, 0, `wB-${uid()}`, 1e5);
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1221
|
+
expect8(leasedB.length).toBeGreaterThan(0);
|
|
1222
|
+
expect8(leasedB.find((l) => l.stream === s)).toBeUndefined();
|
|
1223
|
+
expect8(leasedB.find((l) => l.stream === other)).toBeDefined();
|
|
795
1224
|
});
|
|
796
|
-
|
|
1225
|
+
it7("supports dual frontiers (lagging + leading)", async () => {
|
|
797
1226
|
const s = `claim-dual-${uid()}`;
|
|
798
1227
|
await store.subscribe([{ stream: s }]);
|
|
799
1228
|
await store.commit(
|
|
@@ -803,12 +1232,12 @@ var runStoreTck = (options) => {
|
|
|
803
1232
|
);
|
|
804
1233
|
const first = await store.claim(100, 0, `w-${uid()}`, 1);
|
|
805
1234
|
const mine = first.find((l) => l.stream === s);
|
|
806
|
-
|
|
1235
|
+
expect8(mine).toBeDefined();
|
|
807
1236
|
await store.ack([{ ...mine, at: mine.at + 1 }]);
|
|
808
1237
|
const second = await store.claim(0, 100, `w-${uid()}`, 1);
|
|
809
|
-
|
|
1238
|
+
expect8(second.find((l) => l.stream === s)).toBeDefined();
|
|
810
1239
|
});
|
|
811
|
-
|
|
1240
|
+
it7("dedupes when both frontiers would return the same stream", async () => {
|
|
812
1241
|
const s = `claim-dedup-${uid()}`;
|
|
813
1242
|
await store.subscribe([{ stream: s }]);
|
|
814
1243
|
await store.commit(
|
|
@@ -818,9 +1247,9 @@ var runStoreTck = (options) => {
|
|
|
818
1247
|
);
|
|
819
1248
|
const claimed = await store.claim(100, 100, `w-${uid()}`, 1e5);
|
|
820
1249
|
const matches = claimed.filter((l) => l.stream === s);
|
|
821
|
-
|
|
1250
|
+
expect8(matches).toHaveLength(1);
|
|
822
1251
|
});
|
|
823
|
-
|
|
1252
|
+
it7("silently ignores ack from the wrong holder", async () => {
|
|
824
1253
|
const s = `ack-wrong-${uid()}`;
|
|
825
1254
|
const sibling = `ack-sibling-${uid()}`;
|
|
826
1255
|
await store.subscribe([{ stream: s }, { stream: sibling }]);
|
|
@@ -837,40 +1266,40 @@ var runStoreTck = (options) => {
|
|
|
837
1266
|
const leased = await store.claim(100, 0, `right-${uid()}`, 1e5);
|
|
838
1267
|
const mine = leased.find((l) => l.stream === s);
|
|
839
1268
|
const sibling_lease = leased.find((l) => l.stream === sibling);
|
|
840
|
-
|
|
841
|
-
|
|
1269
|
+
expect8(mine).toBeDefined();
|
|
1270
|
+
expect8(sibling_lease).toBeDefined();
|
|
842
1271
|
const acked = await store.ack([
|
|
843
1272
|
{ ...mine, by: "imposter" },
|
|
844
1273
|
sibling_lease
|
|
845
1274
|
]);
|
|
846
|
-
|
|
847
|
-
|
|
1275
|
+
expect8(acked.length).toBeGreaterThan(0);
|
|
1276
|
+
expect8(acked.find((l) => l.stream === s)).toBeUndefined();
|
|
848
1277
|
});
|
|
849
|
-
|
|
1278
|
+
it7("ack with a stale (lower) watermark does not throw", async () => {
|
|
850
1279
|
const s = `ack-stale-${uid()}`;
|
|
851
1280
|
await store.subscribe([{ stream: s }]);
|
|
852
1281
|
const by = `w-${uid()}`;
|
|
853
1282
|
const leased = await store.claim(100, 0, by, 1e5);
|
|
854
1283
|
const mine = leased.find((l) => l.stream === s);
|
|
855
|
-
|
|
856
|
-
await
|
|
1284
|
+
expect8(mine).toBeDefined();
|
|
1285
|
+
await expect8(
|
|
857
1286
|
store.ack([{ ...mine, at: -5 }])
|
|
858
1287
|
).resolves.toBeDefined();
|
|
859
1288
|
});
|
|
860
|
-
|
|
1289
|
+
it7("claim with no subscribed streams returns an empty array", async () => {
|
|
861
1290
|
const fresh = await options.factory();
|
|
862
1291
|
try {
|
|
863
1292
|
await fresh.drop();
|
|
864
1293
|
await fresh.seed();
|
|
865
1294
|
const claimed = await fresh.claim(1, 1, `w-${uid()}`, 1e3);
|
|
866
|
-
|
|
1295
|
+
expect8(claimed).toEqual([]);
|
|
867
1296
|
} finally {
|
|
868
1297
|
await fresh.dispose();
|
|
869
1298
|
}
|
|
870
1299
|
});
|
|
871
1300
|
});
|
|
872
|
-
|
|
873
|
-
|
|
1301
|
+
describe8("lease semantics", () => {
|
|
1302
|
+
it7("returns retry=0 on first claim and increments on re-claim without ack", async () => {
|
|
874
1303
|
const fresh = await options.factory();
|
|
875
1304
|
try {
|
|
876
1305
|
await fresh.drop();
|
|
@@ -884,17 +1313,17 @@ var runStoreTck = (options) => {
|
|
|
884
1313
|
);
|
|
885
1314
|
const first = await fresh.claim(1, 0, `w-${uid()}`, 0);
|
|
886
1315
|
const f = first.find((l) => l.stream === s);
|
|
887
|
-
|
|
888
|
-
|
|
1316
|
+
expect8(f).toBeDefined();
|
|
1317
|
+
expect8(f.retry).toBe(0);
|
|
889
1318
|
const second = await fresh.claim(1, 0, `w-${uid()}`, 1e5);
|
|
890
1319
|
const sec = second.find((l) => l.stream === s);
|
|
891
|
-
|
|
892
|
-
|
|
1320
|
+
expect8(sec).toBeDefined();
|
|
1321
|
+
expect8(sec.retry).toBe(1);
|
|
893
1322
|
} finally {
|
|
894
1323
|
await fresh.dispose();
|
|
895
1324
|
}
|
|
896
1325
|
});
|
|
897
|
-
|
|
1326
|
+
it7("reports lagging=true from the lagging frontier and false from the leading frontier", async () => {
|
|
898
1327
|
const fresh = await options.factory();
|
|
899
1328
|
try {
|
|
900
1329
|
await fresh.drop();
|
|
@@ -907,16 +1336,16 @@ var runStoreTck = (options) => {
|
|
|
907
1336
|
make_meta({ stream: s })
|
|
908
1337
|
);
|
|
909
1338
|
const lag = await fresh.claim(1, 0, `w-${uid()}`, 0);
|
|
910
|
-
|
|
1339
|
+
expect8(lag.find((l) => l.stream === s)?.lagging).toBe(true);
|
|
911
1340
|
const lead = await fresh.claim(0, 1, `w-${uid()}`, 1e5);
|
|
912
|
-
|
|
1341
|
+
expect8(lead.find((l) => l.stream === s)?.lagging).toBe(false);
|
|
913
1342
|
} finally {
|
|
914
1343
|
await fresh.dispose();
|
|
915
1344
|
}
|
|
916
1345
|
});
|
|
917
1346
|
});
|
|
918
|
-
|
|
919
|
-
|
|
1347
|
+
describe8.skipIf(!caps.concurrent_claim)("concurrency (capability)", () => {
|
|
1348
|
+
it7("never double-leases a stream across concurrent claimers", async () => {
|
|
920
1349
|
const fresh = await options.factory();
|
|
921
1350
|
try {
|
|
922
1351
|
await fresh.drop();
|
|
@@ -939,15 +1368,15 @@ var runStoreTck = (options) => {
|
|
|
939
1368
|
fresh.claim(100, 100, `wB-${uid()}`, 6e4)
|
|
940
1369
|
]);
|
|
941
1370
|
const claimed = [...a, ...b].map((l) => l.stream).filter((stream) => owned.has(stream));
|
|
942
|
-
|
|
943
|
-
|
|
1371
|
+
expect8(new Set(claimed).size).toBe(claimed.length);
|
|
1372
|
+
expect8(claimed.length).toBe(owned.size);
|
|
944
1373
|
} finally {
|
|
945
1374
|
await fresh.dispose();
|
|
946
1375
|
}
|
|
947
1376
|
});
|
|
948
1377
|
});
|
|
949
|
-
|
|
950
|
-
|
|
1378
|
+
describe8("block", () => {
|
|
1379
|
+
it7("hides blocked streams from claim", async () => {
|
|
951
1380
|
const s = `block-${uid()}`;
|
|
952
1381
|
await store.subscribe([{ stream: s }]);
|
|
953
1382
|
await store.commit(
|
|
@@ -957,18 +1386,18 @@ var runStoreTck = (options) => {
|
|
|
957
1386
|
);
|
|
958
1387
|
const leased = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
959
1388
|
const mine = leased.find((l) => l.stream === s);
|
|
960
|
-
|
|
1389
|
+
expect8(mine).toBeDefined();
|
|
961
1390
|
const others = leased.filter((l) => l.stream !== s);
|
|
962
1391
|
await store.ack(others);
|
|
963
1392
|
const blocked = await store.block([
|
|
964
1393
|
{ ...mine, error: "boom" }
|
|
965
1394
|
]);
|
|
966
|
-
|
|
967
|
-
|
|
1395
|
+
expect8(blocked).toHaveLength(1);
|
|
1396
|
+
expect8(blocked[0].error).toBe("boom");
|
|
968
1397
|
const again = await store.claim(100, 100, `w2-${uid()}`, 1e5);
|
|
969
|
-
|
|
1398
|
+
expect8(again.find((l) => l.stream === s)).toBeUndefined();
|
|
970
1399
|
});
|
|
971
|
-
|
|
1400
|
+
it7("rejects block calls from a different holder", async () => {
|
|
972
1401
|
const s = `block-wrong-${uid()}`;
|
|
973
1402
|
await store.subscribe([{ stream: s }]);
|
|
974
1403
|
await store.commit(
|
|
@@ -978,17 +1407,17 @@ var runStoreTck = (options) => {
|
|
|
978
1407
|
);
|
|
979
1408
|
const leased = await store.claim(100, 0, `right-${uid()}`, 1e5);
|
|
980
1409
|
const mine = leased.find((l) => l.stream === s);
|
|
981
|
-
|
|
1410
|
+
expect8(mine).toBeDefined();
|
|
982
1411
|
const others = leased.filter((l) => l.stream !== s);
|
|
983
1412
|
await store.ack(others);
|
|
984
1413
|
const blocked = await store.block([
|
|
985
1414
|
{ ...mine, by: "imposter", error: "no" }
|
|
986
1415
|
]);
|
|
987
|
-
|
|
1416
|
+
expect8(blocked).toHaveLength(0);
|
|
988
1417
|
});
|
|
989
1418
|
});
|
|
990
|
-
|
|
991
|
-
|
|
1419
|
+
describe8("reset", () => {
|
|
1420
|
+
it7("rewinds a stream watermark to -1", async () => {
|
|
992
1421
|
const s = `reset-${uid()}`;
|
|
993
1422
|
await store.subscribe([{ stream: s }]);
|
|
994
1423
|
await store.commit(
|
|
@@ -998,15 +1427,15 @@ var runStoreTck = (options) => {
|
|
|
998
1427
|
);
|
|
999
1428
|
const leased = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
1000
1429
|
const mine = leased.find((l) => l.stream === s);
|
|
1001
|
-
|
|
1430
|
+
expect8(mine).toBeDefined();
|
|
1002
1431
|
await store.ack([{ ...mine, at: 99 }]);
|
|
1003
|
-
|
|
1432
|
+
expect8(await store.reset([s])).toBe(1);
|
|
1004
1433
|
const after = await store.claim(100, 0, `w2-${uid()}`, 1e5);
|
|
1005
1434
|
const back = after.find((l) => l.stream === s);
|
|
1006
|
-
|
|
1007
|
-
|
|
1435
|
+
expect8(back).toBeDefined();
|
|
1436
|
+
expect8(back.at).toBe(-1);
|
|
1008
1437
|
});
|
|
1009
|
-
|
|
1438
|
+
it7("clears blocked status when resetting", async () => {
|
|
1010
1439
|
const s = `reset-blk-${uid()}`;
|
|
1011
1440
|
await store.subscribe([{ stream: s }]);
|
|
1012
1441
|
await store.commit(
|
|
@@ -1019,17 +1448,17 @@ var runStoreTck = (options) => {
|
|
|
1019
1448
|
const others = leased.filter((l) => l.stream !== s);
|
|
1020
1449
|
await store.ack(others);
|
|
1021
1450
|
await store.block([{ ...mine, error: "boom" }]);
|
|
1022
|
-
|
|
1451
|
+
expect8(await store.reset([s])).toBe(1);
|
|
1023
1452
|
const after = await store.claim(100, 0, `w2-${uid()}`, 1e5);
|
|
1024
|
-
|
|
1453
|
+
expect8(after.find((l) => l.stream === s)).toBeDefined();
|
|
1025
1454
|
});
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1455
|
+
it7("returns 0 for unknown streams and empty input", async () => {
|
|
1456
|
+
expect8(await store.reset([`missing-${uid()}`])).toBe(0);
|
|
1457
|
+
expect8(await store.reset([])).toBe(0);
|
|
1029
1458
|
});
|
|
1030
1459
|
});
|
|
1031
|
-
|
|
1032
|
-
|
|
1460
|
+
describe8("unblock", () => {
|
|
1461
|
+
it7("clears blocked flag and preserves the watermark", async () => {
|
|
1033
1462
|
const s = `unblock-${uid()}`;
|
|
1034
1463
|
await store.subscribe([{ stream: s }]);
|
|
1035
1464
|
await store.commit(
|
|
@@ -1047,7 +1476,7 @@ var runStoreTck = (options) => {
|
|
|
1047
1476
|
await store.ack([{ ...m1, at: m1.at }]);
|
|
1048
1477
|
const before_block = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
1049
1478
|
const m2 = before_block.find((l) => l.stream === s);
|
|
1050
|
-
|
|
1479
|
+
expect8(m2).toBeDefined();
|
|
1051
1480
|
const watermark_before = m2.at;
|
|
1052
1481
|
await store.block([{ ...m2, error: "permanent" }]);
|
|
1053
1482
|
let blocked_flag;
|
|
@@ -1057,15 +1486,15 @@ var runStoreTck = (options) => {
|
|
|
1057
1486
|
},
|
|
1058
1487
|
{ stream: s, stream_exact: true, limit: 1 }
|
|
1059
1488
|
);
|
|
1060
|
-
|
|
1061
|
-
|
|
1489
|
+
expect8(blocked_flag).toBe(true);
|
|
1490
|
+
expect8(await store.unblock([s])).toBe(1);
|
|
1062
1491
|
const after = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
1063
1492
|
const back = after.find((l) => l.stream === s);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1493
|
+
expect8(back).toBeDefined();
|
|
1494
|
+
expect8(back.at).toBe(watermark_before);
|
|
1495
|
+
expect8(back.retry).toBe(0);
|
|
1067
1496
|
});
|
|
1068
|
-
|
|
1497
|
+
it7("returns 0 when the stream is not blocked", async () => {
|
|
1069
1498
|
const s = `unblock-noop-${uid()}`;
|
|
1070
1499
|
await store.subscribe([{ stream: s }]);
|
|
1071
1500
|
await store.commit(
|
|
@@ -1073,13 +1502,13 @@ var runStoreTck = (options) => {
|
|
|
1073
1502
|
[inc(1)],
|
|
1074
1503
|
make_meta({ stream: s })
|
|
1075
1504
|
);
|
|
1076
|
-
|
|
1505
|
+
expect8(await store.unblock([s])).toBe(0);
|
|
1077
1506
|
});
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1507
|
+
it7("returns 0 for unknown streams and empty input", async () => {
|
|
1508
|
+
expect8(await store.unblock([`missing-${uid()}`])).toBe(0);
|
|
1509
|
+
expect8(await store.unblock([])).toBe(0);
|
|
1081
1510
|
});
|
|
1082
|
-
|
|
1511
|
+
it7("only counts streams that were actually blocked", async () => {
|
|
1083
1512
|
const s1 = `unblock-mix-a-${uid()}`;
|
|
1084
1513
|
const s2 = `unblock-mix-b-${uid()}`;
|
|
1085
1514
|
await store.subscribe([{ stream: s1 }, { stream: s2 }]);
|
|
@@ -1098,9 +1527,9 @@ var runStoreTck = (options) => {
|
|
|
1098
1527
|
const others = leased.filter((l) => l.stream !== s1);
|
|
1099
1528
|
await store.ack(others);
|
|
1100
1529
|
await store.block([{ ...m1, error: "boom" }]);
|
|
1101
|
-
|
|
1530
|
+
expect8(await store.unblock([s1, s2])).toBe(1);
|
|
1102
1531
|
});
|
|
1103
|
-
|
|
1532
|
+
it7("filter form: unblocks by stream pattern", async () => {
|
|
1104
1533
|
const tag = uid();
|
|
1105
1534
|
const s1 = `unblock-filter-${tag}-a`;
|
|
1106
1535
|
const s2 = `unblock-filter-${tag}-b`;
|
|
@@ -1132,13 +1561,13 @@ var runStoreTck = (options) => {
|
|
|
1132
1561
|
const count = await store.unblock({
|
|
1133
1562
|
stream: `^unblock-filter-${tag}-`
|
|
1134
1563
|
});
|
|
1135
|
-
|
|
1564
|
+
expect8(count).toBe(2);
|
|
1136
1565
|
const after = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1566
|
+
expect8(after.find((l) => l.stream === s3)).toBeUndefined();
|
|
1567
|
+
expect8(after.find((l) => l.stream === s1)).toBeDefined();
|
|
1568
|
+
expect8(after.find((l) => l.stream === s2)).toBeDefined();
|
|
1140
1569
|
});
|
|
1141
|
-
|
|
1570
|
+
it7("filter form: empty filter unblocks every blocked stream", async () => {
|
|
1142
1571
|
const tag = uid();
|
|
1143
1572
|
const s1 = `unblock-empty-${tag}-a`;
|
|
1144
1573
|
const s2 = `unblock-empty-${tag}-b`;
|
|
@@ -1162,9 +1591,9 @@ var runStoreTck = (options) => {
|
|
|
1162
1591
|
const count = await store.unblock({
|
|
1163
1592
|
stream: `^unblock-empty-${tag}-`
|
|
1164
1593
|
});
|
|
1165
|
-
|
|
1594
|
+
expect8(count).toBe(2);
|
|
1166
1595
|
});
|
|
1167
|
-
|
|
1596
|
+
it7("filter form: explicit blocked:false matches nothing", async () => {
|
|
1168
1597
|
const tag = uid();
|
|
1169
1598
|
const s = `unblock-blocked-false-${tag}`;
|
|
1170
1599
|
await store.subscribe([{ stream: s }]);
|
|
@@ -1173,7 +1602,7 @@ var runStoreTck = (options) => {
|
|
|
1173
1602
|
[inc(1)],
|
|
1174
1603
|
make_meta({ stream: s })
|
|
1175
1604
|
);
|
|
1176
|
-
|
|
1605
|
+
expect8(
|
|
1177
1606
|
await store.unblock({
|
|
1178
1607
|
stream: `^unblock-blocked-false-${tag}`,
|
|
1179
1608
|
blocked: false
|
|
@@ -1181,8 +1610,8 @@ var runStoreTck = (options) => {
|
|
|
1181
1610
|
).toBe(0);
|
|
1182
1611
|
});
|
|
1183
1612
|
});
|
|
1184
|
-
|
|
1185
|
-
|
|
1613
|
+
describe8("reset filter form", () => {
|
|
1614
|
+
it7("resets streams matching a stream pattern", async () => {
|
|
1186
1615
|
const tag = uid();
|
|
1187
1616
|
const s1 = `reset-filter-${tag}-a`;
|
|
1188
1617
|
const s2 = `reset-filter-${tag}-b`;
|
|
@@ -1213,7 +1642,7 @@ var runStoreTck = (options) => {
|
|
|
1213
1642
|
);
|
|
1214
1643
|
await store.ack(mine.map((l) => ({ ...l, at: l.at + 100 })));
|
|
1215
1644
|
const count = await store.reset({ stream: `^reset-filter-${tag}-` });
|
|
1216
|
-
|
|
1645
|
+
expect8(count).toBe(2);
|
|
1217
1646
|
const position_for = async (name) => {
|
|
1218
1647
|
let at = null;
|
|
1219
1648
|
await store.query_streams(
|
|
@@ -1224,11 +1653,11 @@ var runStoreTck = (options) => {
|
|
|
1224
1653
|
);
|
|
1225
1654
|
return at;
|
|
1226
1655
|
};
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1656
|
+
expect8(await position_for(s1)).toBe(-1);
|
|
1657
|
+
expect8(await position_for(s2)).toBe(-1);
|
|
1658
|
+
expect8(await position_for(other)).toBeGreaterThan(-1);
|
|
1230
1659
|
});
|
|
1231
|
-
|
|
1660
|
+
it7("filter form: resets only blocked streams when blocked:true", async () => {
|
|
1232
1661
|
const tag = uid();
|
|
1233
1662
|
const s1 = `reset-blocked-${tag}-blocked`;
|
|
1234
1663
|
const s2 = `reset-blocked-${tag}-fine`;
|
|
@@ -1251,11 +1680,11 @@ var runStoreTck = (options) => {
|
|
|
1251
1680
|
stream: `^reset-blocked-${tag}-`,
|
|
1252
1681
|
blocked: true
|
|
1253
1682
|
});
|
|
1254
|
-
|
|
1683
|
+
expect8(count).toBe(1);
|
|
1255
1684
|
});
|
|
1256
1685
|
});
|
|
1257
|
-
|
|
1258
|
-
|
|
1686
|
+
describe8("prioritize", () => {
|
|
1687
|
+
it7("sets priority directly, overriding subscribe's max() rule", async () => {
|
|
1259
1688
|
const tag = uid();
|
|
1260
1689
|
const s1 = `pri-${tag}-a`;
|
|
1261
1690
|
const s2 = `pri-${tag}-b`;
|
|
@@ -1267,7 +1696,7 @@ var runStoreTck = (options) => {
|
|
|
1267
1696
|
{ stream: s1, stream_exact: true },
|
|
1268
1697
|
3
|
|
1269
1698
|
);
|
|
1270
|
-
|
|
1699
|
+
expect8(updated).toBe(1);
|
|
1271
1700
|
const got1 = {};
|
|
1272
1701
|
const got2 = {};
|
|
1273
1702
|
await store.query_streams(
|
|
@@ -1277,12 +1706,12 @@ var runStoreTck = (options) => {
|
|
|
1277
1706
|
},
|
|
1278
1707
|
{ stream: `pri-${tag}-.*`, limit: 100 }
|
|
1279
1708
|
);
|
|
1280
|
-
|
|
1281
|
-
|
|
1709
|
+
expect8(got1.priority).toBe(3);
|
|
1710
|
+
expect8(got2.priority).toBe(5);
|
|
1282
1711
|
});
|
|
1283
1712
|
});
|
|
1284
|
-
|
|
1285
|
-
|
|
1713
|
+
describe8("lanes", () => {
|
|
1714
|
+
it7("subscribe defaults lane to 'default' when omitted", async () => {
|
|
1286
1715
|
const s = `lane-default-${uid()}`;
|
|
1287
1716
|
await store.subscribe([{ stream: s }]);
|
|
1288
1717
|
const seen = [];
|
|
@@ -1290,9 +1719,9 @@ var runStoreTck = (options) => {
|
|
|
1290
1719
|
stream: s,
|
|
1291
1720
|
stream_exact: true
|
|
1292
1721
|
});
|
|
1293
|
-
|
|
1722
|
+
expect8(seen).toEqual(["default"]);
|
|
1294
1723
|
});
|
|
1295
|
-
|
|
1724
|
+
it7("subscribe records the lane passed in", async () => {
|
|
1296
1725
|
const s = `lane-set-${uid()}`;
|
|
1297
1726
|
await store.subscribe([{ stream: s, lane: "slow" }]);
|
|
1298
1727
|
const seen = [];
|
|
@@ -1300,9 +1729,9 @@ var runStoreTck = (options) => {
|
|
|
1300
1729
|
stream: s,
|
|
1301
1730
|
stream_exact: true
|
|
1302
1731
|
});
|
|
1303
|
-
|
|
1732
|
+
expect8(seen).toEqual(["slow"]);
|
|
1304
1733
|
});
|
|
1305
|
-
|
|
1734
|
+
it7("subscribe re-lanes existing streams on subsequent calls", async () => {
|
|
1306
1735
|
const s = `lane-upsert-${uid()}`;
|
|
1307
1736
|
await store.subscribe([{ stream: s, lane: "slow" }]);
|
|
1308
1737
|
await store.subscribe([{ stream: s, lane: "fast" }]);
|
|
@@ -1311,9 +1740,9 @@ var runStoreTck = (options) => {
|
|
|
1311
1740
|
stream: s,
|
|
1312
1741
|
stream_exact: true
|
|
1313
1742
|
});
|
|
1314
|
-
|
|
1743
|
+
expect8(seen).toEqual(["fast"]);
|
|
1315
1744
|
});
|
|
1316
|
-
|
|
1745
|
+
it7("claim() filters by lane when supplied and returns lane on the Lease", async () => {
|
|
1317
1746
|
const tag = uid();
|
|
1318
1747
|
const src1 = `lane-claim-src1-${tag}`;
|
|
1319
1748
|
const src2 = `lane-claim-src2-${tag}`;
|
|
@@ -1337,19 +1766,19 @@ var runStoreTck = (options) => {
|
|
|
1337
1766
|
const slow_mine = slow.filter(
|
|
1338
1767
|
(l) => l.stream === sub_default || l.stream === sub_slow
|
|
1339
1768
|
);
|
|
1340
|
-
|
|
1341
|
-
|
|
1769
|
+
expect8(slow_mine.map((l) => l.stream)).toEqual([sub_slow]);
|
|
1770
|
+
expect8(slow_mine[0]?.lane).toBe("slow");
|
|
1342
1771
|
await store.ack(slow_mine.map((l) => ({ ...l, at: l.at + 1 })));
|
|
1343
1772
|
const all = await store.claim(50, 0, `w-all-${tag}`, 1e3);
|
|
1344
1773
|
const all_mine = all.filter((l) => l.stream === sub_default || l.stream === sub_slow).map((l) => ({ stream: l.stream, lane: l.lane }));
|
|
1345
|
-
|
|
1346
|
-
|
|
1774
|
+
expect8(all_mine).toEqual(
|
|
1775
|
+
expect8.arrayContaining([
|
|
1347
1776
|
{ stream: sub_default, lane: "default" },
|
|
1348
1777
|
{ stream: sub_slow, lane: "slow" }
|
|
1349
1778
|
])
|
|
1350
1779
|
);
|
|
1351
1780
|
});
|
|
1352
|
-
|
|
1781
|
+
it7("query_streams filters by lane", async () => {
|
|
1353
1782
|
const tag = uid();
|
|
1354
1783
|
const a = `lane-q-a-${tag}`;
|
|
1355
1784
|
const b = `lane-q-b-${tag}`;
|
|
@@ -1365,9 +1794,9 @@ var runStoreTck = (options) => {
|
|
|
1365
1794
|
stream: `lane-q-.*-${tag}`,
|
|
1366
1795
|
limit: 100
|
|
1367
1796
|
});
|
|
1368
|
-
|
|
1797
|
+
expect8(seen.sort()).toEqual([a, c]);
|
|
1369
1798
|
});
|
|
1370
|
-
|
|
1799
|
+
it7("prioritize filters by lane", async () => {
|
|
1371
1800
|
const tag = uid();
|
|
1372
1801
|
const a = `lane-pri-a-${tag}`;
|
|
1373
1802
|
const b = `lane-pri-b-${tag}`;
|
|
@@ -1376,16 +1805,16 @@ var runStoreTck = (options) => {
|
|
|
1376
1805
|
{ stream: b, lane: `pfast-${tag}` }
|
|
1377
1806
|
]);
|
|
1378
1807
|
const updated = await store.prioritize({ lane: `pslow-${tag}` }, 7);
|
|
1379
|
-
|
|
1808
|
+
expect8(updated).toBe(1);
|
|
1380
1809
|
const seen = /* @__PURE__ */ new Map();
|
|
1381
1810
|
await store.query_streams((p) => seen.set(p.stream, p.priority), {
|
|
1382
1811
|
stream: `lane-pri-.*-${tag}`,
|
|
1383
1812
|
limit: 100
|
|
1384
1813
|
});
|
|
1385
|
-
|
|
1386
|
-
|
|
1814
|
+
expect8(seen.get(a)).toBe(7);
|
|
1815
|
+
expect8(seen.get(b)).toBe(0);
|
|
1387
1816
|
});
|
|
1388
|
-
|
|
1817
|
+
it7("reset filters by lane", async () => {
|
|
1389
1818
|
const tag = uid();
|
|
1390
1819
|
const src = `lane-reset-src-${tag}`;
|
|
1391
1820
|
const a = `lane-reset-a-${tag}`;
|
|
@@ -1403,7 +1832,7 @@ var runStoreTck = (options) => {
|
|
|
1403
1832
|
const mine = leases.filter((l) => l.stream === a || l.stream === b);
|
|
1404
1833
|
await store.ack(mine.map((l) => ({ ...l, at: l.at + 1 })));
|
|
1405
1834
|
const count = await store.reset({ lane: `rslow-${tag}` });
|
|
1406
|
-
|
|
1835
|
+
expect8(count).toBe(1);
|
|
1407
1836
|
const ats = /* @__PURE__ */ new Map();
|
|
1408
1837
|
for (const name of [a, b]) {
|
|
1409
1838
|
await store.query_streams((p) => ats.set(p.stream, p.at), {
|
|
@@ -1411,10 +1840,10 @@ var runStoreTck = (options) => {
|
|
|
1411
1840
|
stream_exact: true
|
|
1412
1841
|
});
|
|
1413
1842
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1843
|
+
expect8(ats.get(a)).toBe(-1);
|
|
1844
|
+
expect8(ats.get(b)).toBeGreaterThanOrEqual(0);
|
|
1416
1845
|
});
|
|
1417
|
-
|
|
1846
|
+
it7("unblock filters by lane", async () => {
|
|
1418
1847
|
const tag = uid();
|
|
1419
1848
|
const src = `lane-ub-src-${tag}`;
|
|
1420
1849
|
const a = `lane-ub-a-${tag}`;
|
|
@@ -1432,7 +1861,7 @@ var runStoreTck = (options) => {
|
|
|
1432
1861
|
const mine = leases.filter((l) => l.stream === a || l.stream === b);
|
|
1433
1862
|
await store.block(mine.map((l) => ({ ...l, error: "boom" })));
|
|
1434
1863
|
const count = await store.unblock({ lane: `uslow-${tag}` });
|
|
1435
|
-
|
|
1864
|
+
expect8(count).toBe(1);
|
|
1436
1865
|
const blocked = /* @__PURE__ */ new Map();
|
|
1437
1866
|
for (const name of [a, b]) {
|
|
1438
1867
|
await store.query_streams((p) => blocked.set(p.stream, p.blocked), {
|
|
@@ -1440,12 +1869,12 @@ var runStoreTck = (options) => {
|
|
|
1440
1869
|
stream_exact: true
|
|
1441
1870
|
});
|
|
1442
1871
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1872
|
+
expect8(blocked.get(a)).toBe(false);
|
|
1873
|
+
expect8(blocked.get(b)).toBe(true);
|
|
1445
1874
|
});
|
|
1446
1875
|
});
|
|
1447
|
-
|
|
1448
|
-
|
|
1876
|
+
describe8("truncate", () => {
|
|
1877
|
+
it7("seeds a tombstone when no snapshot is provided", async () => {
|
|
1449
1878
|
const s = `trunc-tomb-${uid()}`;
|
|
1450
1879
|
await store.commit(
|
|
1451
1880
|
s,
|
|
@@ -1453,7 +1882,7 @@ var runStoreTck = (options) => {
|
|
|
1453
1882
|
make_meta({ stream: s })
|
|
1454
1883
|
);
|
|
1455
1884
|
const result = await store.truncate([{ stream: s }]);
|
|
1456
|
-
|
|
1885
|
+
expect8(result.get(s)?.deleted).toBe(2);
|
|
1457
1886
|
const remaining = [];
|
|
1458
1887
|
await store.query(
|
|
1459
1888
|
(e) => {
|
|
@@ -1461,12 +1890,12 @@ var runStoreTck = (options) => {
|
|
|
1461
1890
|
},
|
|
1462
1891
|
{ stream: s, stream_exact: true }
|
|
1463
1892
|
);
|
|
1464
|
-
|
|
1465
|
-
|
|
1893
|
+
expect8(remaining).toHaveLength(1);
|
|
1894
|
+
expect8(remaining[0].name).toBe(
|
|
1466
1895
|
"__tombstone__"
|
|
1467
1896
|
);
|
|
1468
1897
|
});
|
|
1469
|
-
|
|
1898
|
+
it7("seeds a snapshot when one is provided", async () => {
|
|
1470
1899
|
const s = `trunc-snap-${uid()}`;
|
|
1471
1900
|
await store.commit(
|
|
1472
1901
|
s,
|
|
@@ -1476,7 +1905,7 @@ var runStoreTck = (options) => {
|
|
|
1476
1905
|
const result = await store.truncate([
|
|
1477
1906
|
{ stream: s, snapshot: { count: 7 } }
|
|
1478
1907
|
]);
|
|
1479
|
-
|
|
1908
|
+
expect8(result.get(s)?.deleted).toBe(1);
|
|
1480
1909
|
const remaining = [];
|
|
1481
1910
|
await store.query(
|
|
1482
1911
|
(e) => {
|
|
@@ -1484,24 +1913,24 @@ var runStoreTck = (options) => {
|
|
|
1484
1913
|
},
|
|
1485
1914
|
{ stream: s, stream_exact: true, with_snaps: true }
|
|
1486
1915
|
);
|
|
1487
|
-
|
|
1488
|
-
|
|
1916
|
+
expect8(remaining).toHaveLength(1);
|
|
1917
|
+
expect8(remaining[0].name).toBe(
|
|
1489
1918
|
"__snapshot__"
|
|
1490
1919
|
);
|
|
1491
|
-
|
|
1920
|
+
expect8(remaining[0].data).toEqual({ count: 7 });
|
|
1492
1921
|
});
|
|
1493
|
-
|
|
1922
|
+
it7("returns an empty map for empty input", async () => {
|
|
1494
1923
|
const result = await store.truncate([]);
|
|
1495
|
-
|
|
1924
|
+
expect8(result.size).toBe(0);
|
|
1496
1925
|
});
|
|
1497
|
-
|
|
1926
|
+
it7("returns 0 deleted for streams that don't exist", async () => {
|
|
1498
1927
|
const s = `trunc-missing-${uid()}`;
|
|
1499
1928
|
const result = await store.truncate([{ stream: s }]);
|
|
1500
|
-
|
|
1929
|
+
expect8(result.get(s)?.deleted).toBe(0);
|
|
1501
1930
|
});
|
|
1502
1931
|
});
|
|
1503
|
-
|
|
1504
|
-
|
|
1932
|
+
describe8("query_streams", () => {
|
|
1933
|
+
it7("returns positions filtered by stream regex, exact, source, and source_exact", async () => {
|
|
1505
1934
|
const tag = uid();
|
|
1506
1935
|
const proj1 = `qs-${tag}-projection-tickets`;
|
|
1507
1936
|
const proj2 = `qs-${tag}-projection-users`;
|
|
@@ -1520,37 +1949,37 @@ var runStoreTck = (options) => {
|
|
|
1520
1949
|
(p) => all.push({ stream: p.stream, source: p.source }),
|
|
1521
1950
|
{ stream: `qs-${tag}-.*` }
|
|
1522
1951
|
);
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1952
|
+
expect8(all_result.count).toBe(4);
|
|
1953
|
+
expect8(all_result.maxEventId).toBeGreaterThanOrEqual(-1);
|
|
1954
|
+
expect8(all.map((p) => p.stream).sort()).toEqual(
|
|
1526
1955
|
[proj1, proj2, dyn1, dyn2].sort()
|
|
1527
1956
|
);
|
|
1528
1957
|
const projections = [];
|
|
1529
1958
|
await store.query_streams((p) => projections.push(p.stream), {
|
|
1530
1959
|
stream: `qs-${tag}-projection-.*`
|
|
1531
1960
|
});
|
|
1532
|
-
|
|
1961
|
+
expect8(projections.sort()).toEqual([proj1, proj2].sort());
|
|
1533
1962
|
const exact = [];
|
|
1534
1963
|
await store.query_streams((p) => exact.push(p.stream), {
|
|
1535
1964
|
stream: dyn1,
|
|
1536
1965
|
stream_exact: true
|
|
1537
1966
|
});
|
|
1538
|
-
|
|
1967
|
+
expect8(exact).toEqual([dyn1]);
|
|
1539
1968
|
const by_source = [];
|
|
1540
1969
|
await store.query_streams((p) => by_source.push(p.stream), {
|
|
1541
1970
|
stream: `qs-${tag}-.*`,
|
|
1542
1971
|
source: `qs-${tag}-src-.*`
|
|
1543
1972
|
});
|
|
1544
|
-
|
|
1973
|
+
expect8(by_source.sort()).toEqual([dyn1, dyn2].sort());
|
|
1545
1974
|
const exact_source = [];
|
|
1546
1975
|
await store.query_streams((p) => exact_source.push(p.stream), {
|
|
1547
1976
|
stream: `qs-${tag}-.*`,
|
|
1548
1977
|
source: src2,
|
|
1549
1978
|
source_exact: true
|
|
1550
1979
|
});
|
|
1551
|
-
|
|
1980
|
+
expect8(exact_source).toEqual([dyn2]);
|
|
1552
1981
|
});
|
|
1553
|
-
|
|
1982
|
+
it7("paginates with limit + after (keyset)", async () => {
|
|
1554
1983
|
const tag = uid();
|
|
1555
1984
|
const streams = [
|
|
1556
1985
|
`qp-${tag}-a`,
|
|
@@ -1564,17 +1993,17 @@ var runStoreTck = (options) => {
|
|
|
1564
1993
|
stream: `qp-${tag}-.*`,
|
|
1565
1994
|
limit: 2
|
|
1566
1995
|
});
|
|
1567
|
-
|
|
1996
|
+
expect8(page1).toHaveLength(2);
|
|
1568
1997
|
const page2 = [];
|
|
1569
1998
|
await store.query_streams((p) => page2.push(p.stream), {
|
|
1570
1999
|
stream: `qp-${tag}-.*`,
|
|
1571
2000
|
limit: 2,
|
|
1572
2001
|
after: page1.at(-1)
|
|
1573
2002
|
});
|
|
1574
|
-
|
|
1575
|
-
|
|
2003
|
+
expect8(page2).toHaveLength(2);
|
|
2004
|
+
expect8([...page1, ...page2].sort()).toEqual([...streams].sort());
|
|
1576
2005
|
});
|
|
1577
|
-
|
|
2006
|
+
it7("filters by blocked status", async () => {
|
|
1578
2007
|
const tag = uid();
|
|
1579
2008
|
const s = `qb-${tag}`;
|
|
1580
2009
|
const sibling = `qb-${tag}-other`;
|
|
@@ -1594,18 +2023,18 @@ var runStoreTck = (options) => {
|
|
|
1594
2023
|
(p) => blocked.push({ stream: p.stream, error: p.error }),
|
|
1595
2024
|
{ stream: `qb-${tag}.*`, blocked: true }
|
|
1596
2025
|
);
|
|
1597
|
-
|
|
1598
|
-
|
|
2026
|
+
expect8(blocked).toHaveLength(1);
|
|
2027
|
+
expect8(blocked[0].error).toBe("boom");
|
|
1599
2028
|
const unblocked = [];
|
|
1600
2029
|
await store.query_streams((p) => unblocked.push(p.stream), {
|
|
1601
2030
|
stream: `qb-${tag}.*`,
|
|
1602
2031
|
blocked: false
|
|
1603
2032
|
});
|
|
1604
|
-
|
|
2033
|
+
expect8(unblocked).toEqual([sibling]);
|
|
1605
2034
|
});
|
|
1606
2035
|
});
|
|
1607
|
-
|
|
1608
|
-
|
|
2036
|
+
describe8("query_stats", () => {
|
|
2037
|
+
it7("array input \u2014 returns head per stream, absent when not in input", async () => {
|
|
1609
2038
|
const tag = uid();
|
|
1610
2039
|
const sA = `qst-${tag}-a`;
|
|
1611
2040
|
const sB = `qst-${tag}-b`;
|
|
@@ -1626,18 +2055,18 @@ var runStoreTck = (options) => {
|
|
|
1626
2055
|
make_meta({ stream: sUnasked })
|
|
1627
2056
|
);
|
|
1628
2057
|
const stats = await store.query_stats([sA, sB]);
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
2058
|
+
expect8(stats.size).toBe(2);
|
|
2059
|
+
expect8(stats.get(sA)?.head.name).toBe("Incremented");
|
|
2060
|
+
expect8((stats.get(sA)?.head.data).amount).toBe(2);
|
|
2061
|
+
expect8(stats.get(sB)?.head.name).toBe("Decremented");
|
|
2062
|
+
expect8((stats.get(sB)?.head.data).amount).toBe(5);
|
|
2063
|
+
expect8(stats.has(sUnasked)).toBe(false);
|
|
1635
2064
|
const empty = await store.query_stats([]);
|
|
1636
|
-
|
|
2065
|
+
expect8(empty.size).toBe(0);
|
|
1637
2066
|
const unknown = await store.query_stats([`qst-${tag}-missing`]);
|
|
1638
|
-
|
|
2067
|
+
expect8(unknown.size).toBe(0);
|
|
1639
2068
|
});
|
|
1640
|
-
|
|
2069
|
+
it7("tail returns the earliest event per stream", async () => {
|
|
1641
2070
|
const tag = uid();
|
|
1642
2071
|
const s = `qst-tail-${tag}`;
|
|
1643
2072
|
await store.commit(
|
|
@@ -1659,12 +2088,12 @@ var runStoreTck = (options) => {
|
|
|
1659
2088
|
tail: true
|
|
1660
2089
|
});
|
|
1661
2090
|
const r = stats.get(s);
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2091
|
+
expect8(r?.head.name).toBe("Incremented");
|
|
2092
|
+
expect8((r?.head.data).amount).toBe(3);
|
|
2093
|
+
expect8(r?.tail?.name).toBe("Incremented");
|
|
2094
|
+
expect8((r?.tail?.data).amount).toBe(1);
|
|
1666
2095
|
});
|
|
1667
|
-
|
|
2096
|
+
it7("count + names \u2014 full aggregates including framework markers", async () => {
|
|
1668
2097
|
const tag = uid();
|
|
1669
2098
|
const s = `qst-cn-${tag}`;
|
|
1670
2099
|
await store.commit(
|
|
@@ -1683,13 +2112,13 @@ var runStoreTck = (options) => {
|
|
|
1683
2112
|
names: true
|
|
1684
2113
|
});
|
|
1685
2114
|
const r = stats.get(s);
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
2115
|
+
expect8(r?.count).toBe(4);
|
|
2116
|
+
expect8(r?.names?.[SNAP_EVENT2]).toBe(1);
|
|
2117
|
+
expect8(r?.names?.Incremented).toBe(2);
|
|
2118
|
+
expect8(r?.names?.Decremented).toBe(1);
|
|
2119
|
+
expect8(r?.names?.[SNAP_EVENT2]).toBe(1);
|
|
1691
2120
|
});
|
|
1692
|
-
|
|
2121
|
+
it7("exclude shifts head past filtered events; stream absent when all filtered", async () => {
|
|
1693
2122
|
const tag = uid();
|
|
1694
2123
|
const s = `qst-excl-${tag}`;
|
|
1695
2124
|
const sAllOut = `qst-allout-${tag}`;
|
|
@@ -1704,23 +2133,23 @@ var runStoreTck = (options) => {
|
|
|
1704
2133
|
make_meta({ stream: sAllOut })
|
|
1705
2134
|
);
|
|
1706
2135
|
const all = await store.query_stats([s]);
|
|
1707
|
-
|
|
1708
|
-
|
|
2136
|
+
expect8(all.get(s)?.head.name).toBe("Incremented");
|
|
2137
|
+
expect8((all.get(s)?.head.data).amount).toBe(3);
|
|
1709
2138
|
const excl = await store.query_stats([s], {
|
|
1710
2139
|
exclude: ["Incremented"]
|
|
1711
2140
|
});
|
|
1712
|
-
|
|
1713
|
-
|
|
2141
|
+
expect8(excl.get(s)?.head.name).toBe("Decremented");
|
|
2142
|
+
expect8((excl.get(s)?.head.data).amount).toBe(2);
|
|
1714
2143
|
const wipe = await store.query_stats([sAllOut], {
|
|
1715
2144
|
exclude: ["Incremented", "Decremented", "Reset"]
|
|
1716
2145
|
});
|
|
1717
|
-
|
|
2146
|
+
expect8(wipe.has(sAllOut)).toBe(false);
|
|
1718
2147
|
const no_tomb = await store.query_stats([s], {
|
|
1719
2148
|
exclude: [TOMBSTONE_EVENT]
|
|
1720
2149
|
});
|
|
1721
|
-
|
|
2150
|
+
expect8(no_tomb.get(s)?.head.name).toBe("Incremented");
|
|
1722
2151
|
});
|
|
1723
|
-
|
|
2152
|
+
it7("before \u2014 time travel narrows head/tail/count", async () => {
|
|
1724
2153
|
const tag = uid();
|
|
1725
2154
|
const s = `qst-tt-${tag}`;
|
|
1726
2155
|
const c1 = await store.commit(
|
|
@@ -1745,15 +2174,15 @@ var runStoreTck = (options) => {
|
|
|
1745
2174
|
before
|
|
1746
2175
|
});
|
|
1747
2176
|
const r = stats.get(s);
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2177
|
+
expect8(r?.count).toBe(1);
|
|
2178
|
+
expect8(r?.head.id).toBe(c1[0].id);
|
|
2179
|
+
expect8(r?.tail?.id).toBe(c1[0].id);
|
|
1751
2180
|
const empty = await store.query_stats([s], {
|
|
1752
2181
|
before: 0
|
|
1753
2182
|
});
|
|
1754
|
-
|
|
2183
|
+
expect8(empty.has(s)).toBe(false);
|
|
1755
2184
|
});
|
|
1756
|
-
|
|
2185
|
+
it7("filter form \u2014 stream regex, stream_exact, empty {} match", async () => {
|
|
1757
2186
|
const tag = uid();
|
|
1758
2187
|
const sA = `qsf-${tag}-orders-1`;
|
|
1759
2188
|
const sB = `qsf-${tag}-orders-2`;
|
|
@@ -1776,18 +2205,18 @@ var runStoreTck = (options) => {
|
|
|
1776
2205
|
const orders = await store.query_stats({
|
|
1777
2206
|
stream: `^qsf-${tag}-orders-`
|
|
1778
2207
|
});
|
|
1779
|
-
|
|
2208
|
+
expect8([...orders.keys()].sort()).toEqual([sA, sB].sort());
|
|
1780
2209
|
const exact = await store.query_stats({
|
|
1781
2210
|
stream: sA,
|
|
1782
2211
|
stream_exact: true
|
|
1783
2212
|
});
|
|
1784
|
-
|
|
2213
|
+
expect8([...exact.keys()]).toEqual([sA]);
|
|
1785
2214
|
const all = await store.query_stats({
|
|
1786
2215
|
stream: `^qsf-${tag}-`
|
|
1787
2216
|
});
|
|
1788
|
-
|
|
2217
|
+
expect8([...all.keys()].sort()).toEqual([sA, sB, sOther].sort());
|
|
1789
2218
|
});
|
|
1790
|
-
|
|
2219
|
+
it7("compose with query_streams for subscription-level filters", async () => {
|
|
1791
2220
|
const tag = uid();
|
|
1792
2221
|
const a = `qsc-${tag}-a`;
|
|
1793
2222
|
const b = `qsc-${tag}-b`;
|
|
@@ -1804,7 +2233,7 @@ var runStoreTck = (options) => {
|
|
|
1804
2233
|
);
|
|
1805
2234
|
const leased = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
1806
2235
|
const mine = leased.find((l) => l.stream === a);
|
|
1807
|
-
|
|
2236
|
+
expect8(mine).toBeDefined();
|
|
1808
2237
|
const others = leased.filter((l) => l.stream !== a);
|
|
1809
2238
|
await store.ack(others);
|
|
1810
2239
|
await store.block([{ ...mine, error: "boom" }]);
|
|
@@ -1813,12 +2242,12 @@ var runStoreTck = (options) => {
|
|
|
1813
2242
|
stream: `^qsc-${tag}-`,
|
|
1814
2243
|
blocked: true
|
|
1815
2244
|
});
|
|
1816
|
-
|
|
2245
|
+
expect8(blocked_names).toEqual([a]);
|
|
1817
2246
|
const stats = await store.query_stats(blocked_names);
|
|
1818
|
-
|
|
1819
|
-
|
|
2247
|
+
expect8(stats.get(a)?.head.name).toBe("Incremented");
|
|
2248
|
+
expect8(stats.has(b)).toBe(false);
|
|
1820
2249
|
});
|
|
1821
|
-
|
|
2250
|
+
it7("empty filter {} \u2014 matches every event-bearing stream", async () => {
|
|
1822
2251
|
const tag = uid();
|
|
1823
2252
|
const a = `qse-${tag}-a`;
|
|
1824
2253
|
const b = `qse-${tag}-b`;
|
|
@@ -1833,10 +2262,10 @@ var runStoreTck = (options) => {
|
|
|
1833
2262
|
make_meta({ stream: b })
|
|
1834
2263
|
);
|
|
1835
2264
|
const all = await store.query_stats({});
|
|
1836
|
-
|
|
1837
|
-
|
|
2265
|
+
expect8(all.has(a)).toBe(true);
|
|
2266
|
+
expect8(all.has(b)).toBe(true);
|
|
1838
2267
|
});
|
|
1839
|
-
|
|
2268
|
+
it7("stat-flag combinations \u2014 count-only, names-only, tail-only", async () => {
|
|
1840
2269
|
const tag = uid();
|
|
1841
2270
|
const s = `qsfl-${tag}`;
|
|
1842
2271
|
await store.commit(
|
|
@@ -1847,22 +2276,22 @@ var runStoreTck = (options) => {
|
|
|
1847
2276
|
const c = await store.query_stats([s], {
|
|
1848
2277
|
count: true
|
|
1849
2278
|
});
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2279
|
+
expect8(c.get(s)?.count).toBe(3);
|
|
2280
|
+
expect8(c.get(s)?.names).toBeUndefined();
|
|
2281
|
+
expect8(c.get(s)?.tail).toBeUndefined();
|
|
1853
2282
|
const n = await store.query_stats([s], {
|
|
1854
2283
|
names: true
|
|
1855
2284
|
});
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2285
|
+
expect8(n.get(s)?.names).toEqual({ Incremented: 2, Decremented: 1 });
|
|
2286
|
+
expect8(n.get(s)?.count).toBeUndefined();
|
|
2287
|
+
expect8(n.get(s)?.tail).toBeUndefined();
|
|
1859
2288
|
const t = await store.query_stats([s], { tail: true });
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2289
|
+
expect8(t.get(s)?.tail?.name).toBe("Incremented");
|
|
2290
|
+
expect8((t.get(s)?.tail?.data).amount).toBe(1);
|
|
2291
|
+
expect8(t.get(s)?.count).toBeUndefined();
|
|
2292
|
+
expect8(t.get(s)?.names).toBeUndefined();
|
|
1864
2293
|
});
|
|
1865
|
-
|
|
2294
|
+
it7("paginates with limit + after (keyset), ordered by stream name", async () => {
|
|
1866
2295
|
const tag = uid();
|
|
1867
2296
|
const streams = [
|
|
1868
2297
|
`qsp-${tag}-a`,
|
|
@@ -1882,28 +2311,28 @@ var runStoreTck = (options) => {
|
|
|
1882
2311
|
{ limit: 2 }
|
|
1883
2312
|
);
|
|
1884
2313
|
const k1 = [...page1.keys()];
|
|
1885
|
-
|
|
2314
|
+
expect8(k1).toEqual([`qsp-${tag}-a`, `qsp-${tag}-b`]);
|
|
1886
2315
|
const page2 = await store.query_stats(
|
|
1887
2316
|
{ stream: `qsp-${tag}-.*` },
|
|
1888
2317
|
{ limit: 2, after: k1.at(-1) }
|
|
1889
2318
|
);
|
|
1890
2319
|
const k2 = [...page2.keys()];
|
|
1891
|
-
|
|
2320
|
+
expect8(k2).toEqual([`qsp-${tag}-c`, `qsp-${tag}-d`]);
|
|
1892
2321
|
const page3 = await store.query_stats(
|
|
1893
2322
|
{ stream: `qsp-${tag}-.*` },
|
|
1894
2323
|
{ limit: 2, after: k2.at(-1) }
|
|
1895
2324
|
);
|
|
1896
|
-
|
|
2325
|
+
expect8(page3.size).toBe(0);
|
|
1897
2326
|
const all = await store.query_stats({
|
|
1898
2327
|
stream: `qsp-${tag}-.*`
|
|
1899
2328
|
});
|
|
1900
|
-
|
|
2329
|
+
expect8([...all.keys()].sort()).toEqual([...streams].sort());
|
|
1901
2330
|
});
|
|
1902
2331
|
});
|
|
1903
|
-
|
|
2332
|
+
describe8.skipIf(!caps.source_matches)(
|
|
1904
2333
|
"query_streams source_matches (capability)",
|
|
1905
2334
|
() => {
|
|
1906
|
-
|
|
2335
|
+
it7("returns only subscriptions whose source pattern matches a name", async () => {
|
|
1907
2336
|
const tag = uid();
|
|
1908
2337
|
const subConcreteA = `sm-${tag}-sub-a`;
|
|
1909
2338
|
const subConcreteB = `sm-${tag}-sub-b`;
|
|
@@ -1924,7 +2353,7 @@ var runStoreTck = (options) => {
|
|
|
1924
2353
|
stream: `sm-${tag}-sub-.*`,
|
|
1925
2354
|
source_matches: [srcA]
|
|
1926
2355
|
});
|
|
1927
|
-
|
|
2356
|
+
expect8(matched.sort()).toEqual(
|
|
1928
2357
|
[subConcreteA, subRegex, subNoSource].sort()
|
|
1929
2358
|
);
|
|
1930
2359
|
const none = [];
|
|
@@ -1932,20 +2361,20 @@ var runStoreTck = (options) => {
|
|
|
1932
2361
|
stream: `sm-${tag}-sub-.*`,
|
|
1933
2362
|
source_matches: [`sm-${tag}-unrelated`]
|
|
1934
2363
|
});
|
|
1935
|
-
|
|
2364
|
+
expect8(none).toEqual([subNoSource]);
|
|
1936
2365
|
const both = [];
|
|
1937
2366
|
await store.query_streams((p) => both.push(p.stream), {
|
|
1938
2367
|
stream: `sm-${tag}-sub-.*`,
|
|
1939
2368
|
source_matches: [srcA, srcB]
|
|
1940
2369
|
});
|
|
1941
|
-
|
|
2370
|
+
expect8(both.sort()).toEqual(
|
|
1942
2371
|
[subConcreteA, subConcreteB, subRegex, subNoSource].sort()
|
|
1943
2372
|
);
|
|
1944
2373
|
});
|
|
1945
2374
|
}
|
|
1946
2375
|
);
|
|
1947
|
-
|
|
1948
|
-
|
|
2376
|
+
describe8("query_streams anchor contract", () => {
|
|
2377
|
+
it7("plain regex without anchors is a substring match", async () => {
|
|
1949
2378
|
const tag = uid();
|
|
1950
2379
|
const inner = `qsr-${tag}-inner`;
|
|
1951
2380
|
const longer = `qsr-${tag}-inner-extra`;
|
|
@@ -1959,9 +2388,9 @@ var runStoreTck = (options) => {
|
|
|
1959
2388
|
await store.query_streams((p) => seen.push(p.stream), {
|
|
1960
2389
|
stream: `qsr-${tag}-inner`
|
|
1961
2390
|
});
|
|
1962
|
-
|
|
2391
|
+
expect8(seen.sort()).toEqual([inner, longer].sort());
|
|
1963
2392
|
});
|
|
1964
|
-
|
|
2393
|
+
it7("caller-anchored `^name$` matches only the whole string", async () => {
|
|
1965
2394
|
const tag = uid();
|
|
1966
2395
|
const inner = `qsr-${tag}-anchor`;
|
|
1967
2396
|
const longer = `qsr-${tag}-anchor-extra`;
|
|
@@ -1970,9 +2399,9 @@ var runStoreTck = (options) => {
|
|
|
1970
2399
|
await store.query_streams((p) => seen.push(p.stream), {
|
|
1971
2400
|
stream: `^qsr-${tag}-anchor$`
|
|
1972
2401
|
});
|
|
1973
|
-
|
|
2402
|
+
expect8(seen).toEqual([inner]);
|
|
1974
2403
|
});
|
|
1975
|
-
|
|
2404
|
+
it7("caller-anchored `^prefix` matches by prefix", async () => {
|
|
1976
2405
|
const tag = uid();
|
|
1977
2406
|
const a = `qsr-${tag}-pfx-a`;
|
|
1978
2407
|
const b = `qsr-${tag}-pfx-b`;
|
|
@@ -1986,11 +2415,11 @@ var runStoreTck = (options) => {
|
|
|
1986
2415
|
await store.query_streams((p) => seen.push(p.stream), {
|
|
1987
2416
|
stream: `^qsr-${tag}-pfx-`
|
|
1988
2417
|
});
|
|
1989
|
-
|
|
2418
|
+
expect8(seen.sort()).toEqual([a, b].sort());
|
|
1990
2419
|
});
|
|
1991
2420
|
});
|
|
1992
|
-
|
|
1993
|
-
|
|
2421
|
+
describe8("prioritize anchor contract", () => {
|
|
2422
|
+
it7("caller-anchored `^name$` filter matches only the whole string", async () => {
|
|
1994
2423
|
const tag = uid();
|
|
1995
2424
|
const inner = `pr-${tag}-anchor`;
|
|
1996
2425
|
const longer = `pr-${tag}-anchor-extra`;
|
|
@@ -2002,17 +2431,17 @@ var runStoreTck = (options) => {
|
|
|
2002
2431
|
{ stream: `^pr-${tag}-anchor$` },
|
|
2003
2432
|
7
|
|
2004
2433
|
);
|
|
2005
|
-
|
|
2434
|
+
expect8(updated).toBe(1);
|
|
2006
2435
|
const seen = /* @__PURE__ */ new Map();
|
|
2007
2436
|
await store.query_streams((p) => seen.set(p.stream, p.priority), {
|
|
2008
2437
|
stream: `pr-${tag}-anchor`
|
|
2009
2438
|
});
|
|
2010
|
-
|
|
2011
|
-
|
|
2439
|
+
expect8(seen.get(inner)).toBe(7);
|
|
2440
|
+
expect8(seen.get(longer)).toBe(0);
|
|
2012
2441
|
});
|
|
2013
2442
|
});
|
|
2014
|
-
|
|
2015
|
-
|
|
2443
|
+
describe8("query_streams head", () => {
|
|
2444
|
+
it7("maxEventId tracks the highest committed id", async () => {
|
|
2016
2445
|
const s = `head-${uid()}`;
|
|
2017
2446
|
await store.subscribe([{ stream: s }]);
|
|
2018
2447
|
await store.commit(
|
|
@@ -2025,21 +2454,21 @@ var runStoreTck = (options) => {
|
|
|
2025
2454
|
(p) => positions.push(p.stream),
|
|
2026
2455
|
{ stream: s, stream_exact: true, limit: 1 }
|
|
2027
2456
|
);
|
|
2028
|
-
|
|
2029
|
-
|
|
2457
|
+
expect8(maxEventId).toBeGreaterThanOrEqual(0);
|
|
2458
|
+
expect8(positions).toEqual([s]);
|
|
2030
2459
|
});
|
|
2031
2460
|
});
|
|
2032
|
-
|
|
2033
|
-
|
|
2461
|
+
describe8("seed_stream helper coverage", () => {
|
|
2462
|
+
it7("commits N events with monotonically increasing ids", async () => {
|
|
2034
2463
|
const s = `seed-${uid()}`;
|
|
2035
2464
|
const committed = await seed_stream(store, s, 3);
|
|
2036
|
-
|
|
2465
|
+
expect8(committed).toHaveLength(3);
|
|
2037
2466
|
for (let i = 1; i < committed.length; i++) {
|
|
2038
|
-
|
|
2467
|
+
expect8(committed[i].id).toBeGreaterThan(committed[i - 1].id);
|
|
2039
2468
|
}
|
|
2040
2469
|
});
|
|
2041
2470
|
});
|
|
2042
|
-
|
|
2471
|
+
describe8.skipIf(!caps.restore)("restore (capability)", () => {
|
|
2043
2472
|
beforeEach(async () => {
|
|
2044
2473
|
await store.drop();
|
|
2045
2474
|
await store.seed();
|
|
@@ -2076,18 +2505,18 @@ var runStoreTck = (options) => {
|
|
|
2076
2505
|
await cache.dispose();
|
|
2077
2506
|
}
|
|
2078
2507
|
};
|
|
2079
|
-
|
|
2508
|
+
it7("returns kept=0 on an empty source", async () => {
|
|
2080
2509
|
const result = await restore(as_source([]));
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2510
|
+
expect8(result.kept).toBe(0);
|
|
2511
|
+
expect8(result.duration_ms).toBeGreaterThanOrEqual(0);
|
|
2512
|
+
expect8(result.dropped).toEqual({
|
|
2084
2513
|
closed_streams: 0,
|
|
2085
2514
|
snapshots: 0
|
|
2086
2515
|
});
|
|
2087
2516
|
const events2 = await collect(store, { limit: 10 });
|
|
2088
|
-
|
|
2517
|
+
expect8(events2).toHaveLength(0);
|
|
2089
2518
|
});
|
|
2090
|
-
|
|
2519
|
+
it7("rebuilds a single stream and preserves `created` verbatim", async () => {
|
|
2091
2520
|
const s = `restore-single-${uid()}`;
|
|
2092
2521
|
const t0 = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
|
|
2093
2522
|
const t1 = /* @__PURE__ */ new Date("2020-01-02T00:00:00.000Z");
|
|
@@ -2098,7 +2527,7 @@ var runStoreTck = (options) => {
|
|
|
2098
2527
|
event(3, s, 2, "Decremented", t2, { amount: 1 })
|
|
2099
2528
|
];
|
|
2100
2529
|
const result = await restore(as_source(events2));
|
|
2101
|
-
|
|
2530
|
+
expect8(result.kept).toBe(3);
|
|
2102
2531
|
const back = [];
|
|
2103
2532
|
await store.query(
|
|
2104
2533
|
(e) => {
|
|
@@ -2106,8 +2535,8 @@ var runStoreTck = (options) => {
|
|
|
2106
2535
|
},
|
|
2107
2536
|
{ stream: s, stream_exact: true }
|
|
2108
2537
|
);
|
|
2109
|
-
|
|
2110
|
-
|
|
2538
|
+
expect8(back).toHaveLength(3);
|
|
2539
|
+
expect8(
|
|
2111
2540
|
back.map((e) => ({
|
|
2112
2541
|
stream: e.stream,
|
|
2113
2542
|
version: e.version,
|
|
@@ -2139,7 +2568,7 @@ var runStoreTck = (options) => {
|
|
|
2139
2568
|
}
|
|
2140
2569
|
]);
|
|
2141
2570
|
});
|
|
2142
|
-
|
|
2571
|
+
it7("rebuilds multiple streams interleaved", async () => {
|
|
2143
2572
|
const a = `restore-multi-a-${uid()}`;
|
|
2144
2573
|
const b = `restore-multi-b-${uid()}`;
|
|
2145
2574
|
const t = /* @__PURE__ */ new Date("2020-06-01T00:00:00.000Z");
|
|
@@ -2150,7 +2579,7 @@ var runStoreTck = (options) => {
|
|
|
2150
2579
|
event(4, b, 1, "Incremented", t, { amount: 30 })
|
|
2151
2580
|
];
|
|
2152
2581
|
const result = await restore(as_source(events2));
|
|
2153
|
-
|
|
2582
|
+
expect8(result.kept).toBe(4);
|
|
2154
2583
|
const aBack = [];
|
|
2155
2584
|
const bBack = [];
|
|
2156
2585
|
await store.query(
|
|
@@ -2165,10 +2594,10 @@ var runStoreTck = (options) => {
|
|
|
2165
2594
|
},
|
|
2166
2595
|
{ stream: b, stream_exact: true }
|
|
2167
2596
|
);
|
|
2168
|
-
|
|
2169
|
-
|
|
2597
|
+
expect8(aBack.map((e) => e.version)).toEqual([0, 1]);
|
|
2598
|
+
expect8(bBack.map((e) => e.version)).toEqual([0, 1]);
|
|
2170
2599
|
});
|
|
2171
|
-
|
|
2600
|
+
it7("preserves Date `created` verbatim", async () => {
|
|
2172
2601
|
const s = `restore-isoc-${uid()}`;
|
|
2173
2602
|
const iso = "2021-07-15T12:34:56.789Z";
|
|
2174
2603
|
await restore(
|
|
@@ -2191,10 +2620,10 @@ var runStoreTck = (options) => {
|
|
|
2191
2620
|
},
|
|
2192
2621
|
{ stream: s, stream_exact: true }
|
|
2193
2622
|
);
|
|
2194
|
-
|
|
2195
|
-
|
|
2623
|
+
expect8(back).toHaveLength(1);
|
|
2624
|
+
expect8(back[0].created.toISOString()).toBe(iso);
|
|
2196
2625
|
});
|
|
2197
|
-
|
|
2626
|
+
it7("wipes pre-existing events before inserting", async () => {
|
|
2198
2627
|
const old = `restore-old-${uid()}`;
|
|
2199
2628
|
await store.commit(
|
|
2200
2629
|
old,
|
|
@@ -2210,14 +2639,14 @@ var runStoreTck = (options) => {
|
|
|
2210
2639
|
stream: old,
|
|
2211
2640
|
stream_exact: true
|
|
2212
2641
|
});
|
|
2213
|
-
|
|
2642
|
+
expect8(old_back).toHaveLength(0);
|
|
2214
2643
|
const fresh_back = await collect(store, {
|
|
2215
2644
|
stream: fresh,
|
|
2216
2645
|
stream_exact: true
|
|
2217
2646
|
});
|
|
2218
|
-
|
|
2647
|
+
expect8(fresh_back).toHaveLength(1);
|
|
2219
2648
|
});
|
|
2220
|
-
|
|
2649
|
+
it7("clears subscription/stream-position metadata", async () => {
|
|
2221
2650
|
const sub = `restore-sub-${uid()}`;
|
|
2222
2651
|
await store.subscribe([{ stream: sub, source: "anything" }]);
|
|
2223
2652
|
const collect_streams = async () => {
|
|
@@ -2228,12 +2657,12 @@ var runStoreTck = (options) => {
|
|
|
2228
2657
|
return out;
|
|
2229
2658
|
};
|
|
2230
2659
|
const before = await collect_streams();
|
|
2231
|
-
|
|
2660
|
+
expect8(before.includes(sub)).toBe(true);
|
|
2232
2661
|
await restore(as_source([]));
|
|
2233
2662
|
const after = await collect_streams();
|
|
2234
|
-
|
|
2663
|
+
expect8(after.includes(sub)).toBe(false);
|
|
2235
2664
|
});
|
|
2236
|
-
|
|
2665
|
+
it7("preserves snapshot events through restore", async () => {
|
|
2237
2666
|
const s = `restore-snap-${uid()}`;
|
|
2238
2667
|
const t = /* @__PURE__ */ new Date("2020-04-01T00:00:00.000Z");
|
|
2239
2668
|
await restore(
|
|
@@ -2242,7 +2671,7 @@ var runStoreTck = (options) => {
|
|
|
2242
2671
|
id: 1,
|
|
2243
2672
|
stream: s,
|
|
2244
2673
|
version: 0,
|
|
2245
|
-
name:
|
|
2674
|
+
name: SNAP_EVENT2,
|
|
2246
2675
|
data: { count: 42 },
|
|
2247
2676
|
created: t,
|
|
2248
2677
|
meta: { correlation: "snap", causation: {} }
|
|
@@ -2254,10 +2683,10 @@ var runStoreTck = (options) => {
|
|
|
2254
2683
|
stream_exact: true,
|
|
2255
2684
|
with_snaps: true
|
|
2256
2685
|
});
|
|
2257
|
-
|
|
2258
|
-
|
|
2686
|
+
expect8(back).toHaveLength(1);
|
|
2687
|
+
expect8(back[0].name).toBe(SNAP_EVENT2);
|
|
2259
2688
|
});
|
|
2260
|
-
|
|
2689
|
+
it7("rewrites causation refs through the old\u2192new id map", async () => {
|
|
2261
2690
|
const s = `restore-caus-${uid()}`;
|
|
2262
2691
|
const t = /* @__PURE__ */ new Date("2020-08-01T00:00:00.000Z");
|
|
2263
2692
|
const events2 = [
|
|
@@ -2307,12 +2736,12 @@ var runStoreTck = (options) => {
|
|
|
2307
2736
|
},
|
|
2308
2737
|
{ stream: s, stream_exact: true }
|
|
2309
2738
|
);
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2739
|
+
expect8(back).toHaveLength(3);
|
|
2740
|
+
expect8(back[0].meta.causation.event).toBeUndefined();
|
|
2741
|
+
expect8(back[1].meta.causation.event?.id).toBe(back[0].id);
|
|
2742
|
+
expect8(back[2].meta.causation.event?.id).toBe(back[1].id);
|
|
2314
2743
|
});
|
|
2315
|
-
|
|
2744
|
+
it7("leaves causation refs unmapped when the target isn't in the source", async () => {
|
|
2316
2745
|
const s = `restore-orphan-${uid()}`;
|
|
2317
2746
|
const t = /* @__PURE__ */ new Date("2020-09-01T00:00:00.000Z");
|
|
2318
2747
|
await restore(
|
|
@@ -2340,9 +2769,9 @@ var runStoreTck = (options) => {
|
|
|
2340
2769
|
},
|
|
2341
2770
|
{ stream: s, stream_exact: true }
|
|
2342
2771
|
);
|
|
2343
|
-
|
|
2772
|
+
expect8(back[0].meta.causation.event?.id).toBe(999);
|
|
2344
2773
|
});
|
|
2345
|
-
|
|
2774
|
+
it7("rolls back atomically when the source throws mid-iteration", async () => {
|
|
2346
2775
|
const original = `restore-pre-${uid()}`;
|
|
2347
2776
|
const committed = await store.commit(
|
|
2348
2777
|
original,
|
|
@@ -2368,7 +2797,7 @@ var runStoreTck = (options) => {
|
|
|
2368
2797
|
async dispose() {
|
|
2369
2798
|
}
|
|
2370
2799
|
};
|
|
2371
|
-
await
|
|
2800
|
+
await expect8(restore(explosive)).rejects.toThrow("boom");
|
|
2372
2801
|
const back = [];
|
|
2373
2802
|
await store.query(
|
|
2374
2803
|
(e) => {
|
|
@@ -2376,10 +2805,10 @@ var runStoreTck = (options) => {
|
|
|
2376
2805
|
},
|
|
2377
2806
|
{ stream: original, stream_exact: true }
|
|
2378
2807
|
);
|
|
2379
|
-
|
|
2380
|
-
|
|
2808
|
+
expect8(back).toHaveLength(3);
|
|
2809
|
+
expect8(back.map((e) => e.id)).toEqual(committed.map((c) => c.id));
|
|
2381
2810
|
});
|
|
2382
|
-
|
|
2811
|
+
it7("drop_snapshots: skips SNAP_EVENT rows and counts them", async () => {
|
|
2383
2812
|
const s = `restore-drop-snap-${uid()}`;
|
|
2384
2813
|
const t = /* @__PURE__ */ new Date("2020-10-01T00:00:00.000Z");
|
|
2385
2814
|
const result = await restore(
|
|
@@ -2389,7 +2818,7 @@ var runStoreTck = (options) => {
|
|
|
2389
2818
|
id: 2,
|
|
2390
2819
|
stream: s,
|
|
2391
2820
|
version: 1,
|
|
2392
|
-
name:
|
|
2821
|
+
name: SNAP_EVENT2,
|
|
2393
2822
|
data: { count: 1 },
|
|
2394
2823
|
created: t,
|
|
2395
2824
|
meta: { correlation: "snap", causation: {} }
|
|
@@ -2398,19 +2827,19 @@ var runStoreTck = (options) => {
|
|
|
2398
2827
|
]),
|
|
2399
2828
|
{ drop_snapshots: true }
|
|
2400
2829
|
);
|
|
2401
|
-
|
|
2402
|
-
|
|
2830
|
+
expect8(result.kept).toBe(2);
|
|
2831
|
+
expect8(result.dropped.snapshots).toBe(1);
|
|
2403
2832
|
const back = await collect(store, {
|
|
2404
2833
|
stream: s,
|
|
2405
2834
|
stream_exact: true,
|
|
2406
2835
|
with_snaps: true
|
|
2407
2836
|
});
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
back.every((e) => e.name !==
|
|
2837
|
+
expect8(back).toHaveLength(2);
|
|
2838
|
+
expect8(
|
|
2839
|
+
back.every((e) => e.name !== SNAP_EVENT2)
|
|
2411
2840
|
).toBe(true);
|
|
2412
2841
|
});
|
|
2413
|
-
|
|
2842
|
+
it7("on_progress fires once per event (caller throttles)", async () => {
|
|
2414
2843
|
const calls = [];
|
|
2415
2844
|
const s = `restore-progress-${uid()}`;
|
|
2416
2845
|
const t = /* @__PURE__ */ new Date("2021-02-01T00:00:00.000Z");
|
|
@@ -2421,11 +2850,11 @@ var runStoreTck = (options) => {
|
|
|
2421
2850
|
]),
|
|
2422
2851
|
{ on_progress: (p) => calls.push(p.processed) }
|
|
2423
2852
|
);
|
|
2424
|
-
|
|
2853
|
+
expect8(calls).toEqual([1, 2]);
|
|
2425
2854
|
});
|
|
2426
2855
|
});
|
|
2427
|
-
|
|
2428
|
-
|
|
2856
|
+
describe8.skipIf(!caps.pii_isolation)("pii_isolation (capability)", () => {
|
|
2857
|
+
it7("commits and loads pii alongside data", async () => {
|
|
2429
2858
|
const s = `pii-roundtrip-${uid()}`;
|
|
2430
2859
|
const committed = await store.commit(
|
|
2431
2860
|
s,
|
|
@@ -2438,8 +2867,8 @@ var runStoreTck = (options) => {
|
|
|
2438
2867
|
],
|
|
2439
2868
|
make_meta({ stream: s })
|
|
2440
2869
|
);
|
|
2441
|
-
|
|
2442
|
-
|
|
2870
|
+
expect8(committed).toHaveLength(1);
|
|
2871
|
+
expect8(committed[0].pii).toEqual({
|
|
2443
2872
|
email: "u@example.com",
|
|
2444
2873
|
name: "Ursula"
|
|
2445
2874
|
});
|
|
@@ -2450,11 +2879,11 @@ var runStoreTck = (options) => {
|
|
|
2450
2879
|
},
|
|
2451
2880
|
{ stream: s, stream_exact: true }
|
|
2452
2881
|
);
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2882
|
+
expect8(seen).toHaveLength(1);
|
|
2883
|
+
expect8(seen[0].pii).toEqual({ email: "u@example.com", name: "Ursula" });
|
|
2884
|
+
expect8(seen[0].data).toEqual({ amount: 1 });
|
|
2456
2885
|
});
|
|
2457
|
-
|
|
2886
|
+
it7("passes through events without pii (pii is null or undefined on load)", async () => {
|
|
2458
2887
|
const s = `pii-none-${uid()}`;
|
|
2459
2888
|
await store.commit(
|
|
2460
2889
|
s,
|
|
@@ -2468,10 +2897,10 @@ var runStoreTck = (options) => {
|
|
|
2468
2897
|
},
|
|
2469
2898
|
{ stream: s, stream_exact: true }
|
|
2470
2899
|
);
|
|
2471
|
-
|
|
2472
|
-
|
|
2900
|
+
expect8(seen).toHaveLength(1);
|
|
2901
|
+
expect8(seen[0].pii == null).toBe(true);
|
|
2473
2902
|
});
|
|
2474
|
-
|
|
2903
|
+
it7("wipes pii for every event on the stream via forget_pii", async () => {
|
|
2475
2904
|
const s = `pii-forget-${uid()}`;
|
|
2476
2905
|
await store.commit(
|
|
2477
2906
|
s,
|
|
@@ -2490,9 +2919,9 @@ var runStoreTck = (options) => {
|
|
|
2490
2919
|
make_meta({ stream: s })
|
|
2491
2920
|
);
|
|
2492
2921
|
const forget = store.forget_pii;
|
|
2493
|
-
|
|
2922
|
+
expect8(forget).toBeDefined();
|
|
2494
2923
|
const wiped = await forget.call(store, s);
|
|
2495
|
-
|
|
2924
|
+
expect8(wiped).toBe(2);
|
|
2496
2925
|
const seen = [];
|
|
2497
2926
|
await store.query(
|
|
2498
2927
|
(e) => {
|
|
@@ -2500,13 +2929,13 @@ var runStoreTck = (options) => {
|
|
|
2500
2929
|
},
|
|
2501
2930
|
{ stream: s, stream_exact: true }
|
|
2502
2931
|
);
|
|
2503
|
-
|
|
2932
|
+
expect8(seen).toHaveLength(2);
|
|
2504
2933
|
for (const e of seen) {
|
|
2505
|
-
|
|
2506
|
-
|
|
2934
|
+
expect8(e.pii == null).toBe(true);
|
|
2935
|
+
expect8(e.data).toBeDefined();
|
|
2507
2936
|
}
|
|
2508
2937
|
});
|
|
2509
|
-
|
|
2938
|
+
it7("is idempotent \u2014 second forget_pii returns 0, no error", async () => {
|
|
2510
2939
|
const s = `pii-forget-idem-${uid()}`;
|
|
2511
2940
|
await store.commit(
|
|
2512
2941
|
s,
|
|
@@ -2521,11 +2950,11 @@ var runStoreTck = (options) => {
|
|
|
2521
2950
|
);
|
|
2522
2951
|
const forget = store.forget_pii;
|
|
2523
2952
|
const first = await forget.call(store, s);
|
|
2524
|
-
|
|
2953
|
+
expect8(first).toBe(1);
|
|
2525
2954
|
const second = await forget.call(store, s);
|
|
2526
|
-
|
|
2955
|
+
expect8(second).toBe(0);
|
|
2527
2956
|
});
|
|
2528
|
-
|
|
2957
|
+
it7("only wipes the targeted stream \u2014 siblings untouched", async () => {
|
|
2529
2958
|
const sA = `pii-iso-a-${uid()}`;
|
|
2530
2959
|
const sB = `pii-iso-b-${uid()}`;
|
|
2531
2960
|
await store.commit(
|
|
@@ -2558,7 +2987,7 @@ var runStoreTck = (options) => {
|
|
|
2558
2987
|
},
|
|
2559
2988
|
{ stream: sA, stream_exact: true }
|
|
2560
2989
|
);
|
|
2561
|
-
|
|
2990
|
+
expect8(a[0].pii == null).toBe(true);
|
|
2562
2991
|
const b = [];
|
|
2563
2992
|
await store.query(
|
|
2564
2993
|
(e) => {
|
|
@@ -2566,9 +2995,9 @@ var runStoreTck = (options) => {
|
|
|
2566
2995
|
},
|
|
2567
2996
|
{ stream: sB, stream_exact: true }
|
|
2568
2997
|
);
|
|
2569
|
-
|
|
2998
|
+
expect8(b[0].pii).toEqual({ email: "bob@example.com" });
|
|
2570
2999
|
});
|
|
2571
|
-
|
|
3000
|
+
it7("forget_pii on a stream with no pii events returns 0", async () => {
|
|
2572
3001
|
const s = `pii-forget-empty-${uid()}`;
|
|
2573
3002
|
await store.commit(
|
|
2574
3003
|
s,
|
|
@@ -2576,14 +3005,14 @@ var runStoreTck = (options) => {
|
|
|
2576
3005
|
make_meta({ stream: s })
|
|
2577
3006
|
);
|
|
2578
3007
|
const wiped = await store.forget_pii.call(store, s);
|
|
2579
|
-
|
|
3008
|
+
expect8(wiped).toBe(0);
|
|
2580
3009
|
});
|
|
2581
3010
|
});
|
|
2582
3011
|
if (caps.notify) {
|
|
2583
|
-
|
|
2584
|
-
|
|
3012
|
+
describe8("notify (capability)", () => {
|
|
3013
|
+
it7("delivers a notification when a different instance commits", async () => {
|
|
2585
3014
|
const notify = store.notify;
|
|
2586
|
-
|
|
3015
|
+
expect8(notify).toBeDefined();
|
|
2587
3016
|
const received = [];
|
|
2588
3017
|
let resolve_arrived;
|
|
2589
3018
|
const arrived = new Promise((res) => {
|
|
@@ -2602,9 +3031,9 @@ var runStoreTck = (options) => {
|
|
|
2602
3031
|
make_meta({ stream })
|
|
2603
3032
|
);
|
|
2604
3033
|
await arrived;
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
3034
|
+
expect8(received.length).toBeGreaterThanOrEqual(1);
|
|
3035
|
+
expect8(received[0].stream).toBe(stream);
|
|
3036
|
+
expect8(received[0].events.length).toBeGreaterThanOrEqual(1);
|
|
2608
3037
|
} finally {
|
|
2609
3038
|
await writer.dispose();
|
|
2610
3039
|
await Promise.resolve(disposer());
|
|
@@ -2625,9 +3054,12 @@ export {
|
|
|
2625
3054
|
dec,
|
|
2626
3055
|
inc,
|
|
2627
3056
|
reset,
|
|
3057
|
+
runCacheDifferentialTck,
|
|
2628
3058
|
runCacheTck,
|
|
3059
|
+
runLoggerDifferentialTck,
|
|
2629
3060
|
runLoggerTck,
|
|
2630
3061
|
runStabilityTck,
|
|
3062
|
+
runStoreDifferentialTck,
|
|
2631
3063
|
runStorePropertyTck,
|
|
2632
3064
|
runStoreTck,
|
|
2633
3065
|
uid
|