@rotorsoft/act-tck 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/@types/cache-tck.d.ts +49 -0
- package/dist/@types/cache-tck.d.ts.map +1 -0
- package/dist/@types/fixtures/events.d.ts +24 -0
- package/dist/@types/fixtures/events.d.ts.map +1 -0
- package/dist/@types/fixtures/helpers.d.ts +33 -0
- package/dist/@types/fixtures/helpers.d.ts.map +1 -0
- package/dist/@types/fixtures/index.d.ts +3 -0
- package/dist/@types/fixtures/index.d.ts.map +1 -0
- package/dist/@types/index.d.ts +72 -0
- package/dist/@types/index.d.ts.map +1 -0
- package/dist/@types/logger-tck.d.ts +47 -0
- package/dist/@types/logger-tck.d.ts.map +1 -0
- package/dist/@types/store-tck.d.ts +75 -0
- package/dist/@types/store-tck.d.ts.map +1 -0
- package/dist/index.cjs +998 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +956 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
// src/cache-tck.ts
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
var entry = (event_id, state = {}) => ({
|
|
4
|
+
state,
|
|
5
|
+
version: event_id,
|
|
6
|
+
event_id,
|
|
7
|
+
patches: 1,
|
|
8
|
+
snaps: 0
|
|
9
|
+
});
|
|
10
|
+
var runCacheTck = (options) => {
|
|
11
|
+
describe(`TCK / Cache / ${options.name}`, () => {
|
|
12
|
+
let cache;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
cache = options.factory();
|
|
15
|
+
});
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await cache.dispose();
|
|
18
|
+
});
|
|
19
|
+
it("returns undefined for an unset stream", async () => {
|
|
20
|
+
expect(await cache.get("missing")).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
it("set then get round-trips an entry", async () => {
|
|
23
|
+
const e = entry(1, { count: 7 });
|
|
24
|
+
await cache.set("s1", e);
|
|
25
|
+
expect(await cache.get("s1")).toEqual(e);
|
|
26
|
+
});
|
|
27
|
+
it("set overwrites a prior entry on the same stream", async () => {
|
|
28
|
+
await cache.set("s1", entry(1, { count: 1 }));
|
|
29
|
+
await cache.set("s1", entry(2, { count: 2 }));
|
|
30
|
+
const got = await cache.get("s1");
|
|
31
|
+
expect(got?.event_id).toBe(2);
|
|
32
|
+
expect(got?.state).toEqual({ count: 2 });
|
|
33
|
+
});
|
|
34
|
+
it("invalidate removes one stream and leaves others", async () => {
|
|
35
|
+
await cache.set("a", entry(1));
|
|
36
|
+
await cache.set("b", entry(2));
|
|
37
|
+
await cache.invalidate("a");
|
|
38
|
+
expect(await cache.get("a")).toBeUndefined();
|
|
39
|
+
expect(await cache.get("b")).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
it("invalidate on an unknown stream is a no-op", async () => {
|
|
42
|
+
await expect(cache.invalidate("never-set")).resolves.toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
it("clear empties every stream", async () => {
|
|
45
|
+
await cache.set("a", entry(1));
|
|
46
|
+
await cache.set("b", entry(2));
|
|
47
|
+
await cache.set("c", entry(3));
|
|
48
|
+
await cache.clear();
|
|
49
|
+
expect(await cache.get("a")).toBeUndefined();
|
|
50
|
+
expect(await cache.get("b")).toBeUndefined();
|
|
51
|
+
expect(await cache.get("c")).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
it("clear on an empty cache is a no-op", async () => {
|
|
54
|
+
await expect(cache.clear()).resolves.toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
it("entries are isolated per stream", async () => {
|
|
57
|
+
const ea = entry(1, { id: "a" });
|
|
58
|
+
const eb = entry(2, { id: "b" });
|
|
59
|
+
await cache.set("a", ea);
|
|
60
|
+
await cache.set("b", eb);
|
|
61
|
+
expect(await cache.get("a")).toEqual(ea);
|
|
62
|
+
expect(await cache.get("b")).toEqual(eb);
|
|
63
|
+
});
|
|
64
|
+
it("dispose is idempotent", async () => {
|
|
65
|
+
await cache.set("a", entry(1));
|
|
66
|
+
await cache.dispose();
|
|
67
|
+
await expect(cache.dispose()).resolves.toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/fixtures/events.ts
|
|
73
|
+
import { z } from "zod";
|
|
74
|
+
var Incremented = z.object({ amount: z.number().int() });
|
|
75
|
+
var Decremented = z.object({ amount: z.number().int() });
|
|
76
|
+
var Reset = z.object({});
|
|
77
|
+
var CounterSchemas = {
|
|
78
|
+
Incremented,
|
|
79
|
+
Decremented,
|
|
80
|
+
Reset
|
|
81
|
+
};
|
|
82
|
+
var COUNTER_EVENT_NAMES = [
|
|
83
|
+
"Incremented",
|
|
84
|
+
"Decremented",
|
|
85
|
+
"Reset"
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// src/fixtures/helpers.ts
|
|
89
|
+
import { randomUUID } from "crypto";
|
|
90
|
+
var uid = () => randomUUID().slice(0, 8);
|
|
91
|
+
var actor = (name = "tester") => ({ id: randomUUID(), name });
|
|
92
|
+
var makeMeta = (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 seedStream = async (store, stream, count, correlation) => {
|
|
112
|
+
const out = [];
|
|
113
|
+
for (let i = 0; i < count; i++) {
|
|
114
|
+
const committed = await store.commit(
|
|
115
|
+
stream,
|
|
116
|
+
[inc(1)],
|
|
117
|
+
makeMeta({ correlation, stream })
|
|
118
|
+
);
|
|
119
|
+
out.push(...committed);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
};
|
|
123
|
+
var collect = async (store, query) => {
|
|
124
|
+
const out = [];
|
|
125
|
+
await store.query((e) => {
|
|
126
|
+
out.push(e);
|
|
127
|
+
}, query);
|
|
128
|
+
return out;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/logger-tck.ts
|
|
132
|
+
import { afterEach as afterEach2, beforeEach as beforeEach2, describe as describe2, expect as expect2, it as it2 } from "vitest";
|
|
133
|
+
var LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"];
|
|
134
|
+
var runLoggerTck = (options) => {
|
|
135
|
+
describe2(`TCK / Logger / ${options.name}`, () => {
|
|
136
|
+
let logger;
|
|
137
|
+
let originalStdout;
|
|
138
|
+
beforeEach2(() => {
|
|
139
|
+
logger = options.factory();
|
|
140
|
+
originalStdout = process.stdout.write.bind(process.stdout);
|
|
141
|
+
process.stdout.write = (() => true);
|
|
142
|
+
});
|
|
143
|
+
afterEach2(async () => {
|
|
144
|
+
process.stdout.write = originalStdout;
|
|
145
|
+
await logger.dispose();
|
|
146
|
+
});
|
|
147
|
+
it2("exposes a non-empty `level` string", () => {
|
|
148
|
+
expect2(typeof logger.level).toBe("string");
|
|
149
|
+
expect2(logger.level.length).toBeGreaterThan(0);
|
|
150
|
+
});
|
|
151
|
+
for (const level of LEVELS) {
|
|
152
|
+
it2(`${level}(msg) does not throw`, () => {
|
|
153
|
+
expect2(() => logger[level]("hello")).not.toThrow();
|
|
154
|
+
});
|
|
155
|
+
it2(`${level}(obj) does not throw`, () => {
|
|
156
|
+
expect2(() => logger[level]({ k: "v" })).not.toThrow();
|
|
157
|
+
});
|
|
158
|
+
it2(`${level}(obj, msg) does not throw`, () => {
|
|
159
|
+
expect2(() => logger[level]({ k: "v" }, "context")).not.toThrow();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
it2("accepts a null payload", () => {
|
|
163
|
+
expect2(() => logger.info(null, "null payload")).not.toThrow();
|
|
164
|
+
});
|
|
165
|
+
it2("accepts a cyclic payload without throwing", () => {
|
|
166
|
+
const cyclic = { name: "loop" };
|
|
167
|
+
cyclic.self = cyclic;
|
|
168
|
+
expect2(() => logger.info(cyclic, "cycle")).not.toThrow();
|
|
169
|
+
});
|
|
170
|
+
it2("child(bindings) returns a Logger satisfying the same contract", () => {
|
|
171
|
+
const child = logger.child({ requestId: "abc" });
|
|
172
|
+
expect2(typeof child.level).toBe("string");
|
|
173
|
+
for (const level of LEVELS) {
|
|
174
|
+
expect2(typeof child[level]).toBe("function");
|
|
175
|
+
}
|
|
176
|
+
expect2(typeof child.child).toBe("function");
|
|
177
|
+
expect2(typeof child.dispose).toBe("function");
|
|
178
|
+
});
|
|
179
|
+
it2("child loggers can themselves spawn children", () => {
|
|
180
|
+
const c1 = logger.child({ a: 1 });
|
|
181
|
+
const c2 = c1.child({ b: 2 });
|
|
182
|
+
expect2(() => c2.info("nested")).not.toThrow();
|
|
183
|
+
});
|
|
184
|
+
it2("dispose is idempotent and awaitable", async () => {
|
|
185
|
+
await expect2(logger.dispose()).resolves.toBeUndefined();
|
|
186
|
+
await expect2(logger.dispose()).resolves.toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/store-tck.ts
|
|
192
|
+
import { ConcurrencyError } from "@rotorsoft/act";
|
|
193
|
+
import { afterAll, beforeAll, describe as describe3, expect as expect3, it as it3 } from "vitest";
|
|
194
|
+
var runStoreTck = (options) => {
|
|
195
|
+
describe3(`TCK / Store / ${options.name}`, () => {
|
|
196
|
+
let store;
|
|
197
|
+
const caps = options.capabilities ?? {};
|
|
198
|
+
beforeAll(async () => {
|
|
199
|
+
store = await options.factory();
|
|
200
|
+
await store.drop();
|
|
201
|
+
await store.seed();
|
|
202
|
+
});
|
|
203
|
+
afterAll(async () => {
|
|
204
|
+
await store.dispose();
|
|
205
|
+
});
|
|
206
|
+
describe3("commit", () => {
|
|
207
|
+
it3("returns committed events with sequenced ids and versions", async () => {
|
|
208
|
+
const s = `commit-seq-${uid()}`;
|
|
209
|
+
const committed = await store.commit(
|
|
210
|
+
s,
|
|
211
|
+
[inc(1), inc(2), dec(3)],
|
|
212
|
+
makeMeta({ stream: s })
|
|
213
|
+
);
|
|
214
|
+
expect3(committed).toHaveLength(3);
|
|
215
|
+
expect3(committed[0].version).toBe(0);
|
|
216
|
+
expect3(committed[1].version).toBe(1);
|
|
217
|
+
expect3(committed[2].version).toBe(2);
|
|
218
|
+
expect3(committed[0].name).toBe("Incremented");
|
|
219
|
+
expect3(committed[2].data).toEqual({ amount: 3 });
|
|
220
|
+
for (let i = 1; i < committed.length; i++) {
|
|
221
|
+
expect3(committed[i].id).toBeGreaterThan(committed[i - 1].id);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
it3("attaches correlation and stream metadata", async () => {
|
|
225
|
+
const s = `commit-meta-${uid()}`;
|
|
226
|
+
const correlation = `cor-${uid()}`;
|
|
227
|
+
const committed = await store.commit(
|
|
228
|
+
s,
|
|
229
|
+
[inc(1)],
|
|
230
|
+
makeMeta({ stream: s, correlation })
|
|
231
|
+
);
|
|
232
|
+
expect3(committed[0].stream).toBe(s);
|
|
233
|
+
expect3(committed[0].meta.correlation).toBe(correlation);
|
|
234
|
+
});
|
|
235
|
+
it3("throws ConcurrencyError when expectedVersion is wrong", async () => {
|
|
236
|
+
const s = `commit-cc-${uid()}`;
|
|
237
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
238
|
+
await store.commit(
|
|
239
|
+
s,
|
|
240
|
+
[inc(1)],
|
|
241
|
+
makeMeta({ stream: s }),
|
|
242
|
+
0
|
|
243
|
+
);
|
|
244
|
+
await expect3(
|
|
245
|
+
store.commit(s, [inc(1)], makeMeta({ stream: s }), 0)
|
|
246
|
+
).rejects.toBeInstanceOf(ConcurrencyError);
|
|
247
|
+
});
|
|
248
|
+
it3("preserves prior events when a concurrent commit is rejected", async () => {
|
|
249
|
+
const s = `commit-cc-preserve-${uid()}`;
|
|
250
|
+
await store.commit(
|
|
251
|
+
s,
|
|
252
|
+
[inc(1), inc(2)],
|
|
253
|
+
makeMeta({ stream: s })
|
|
254
|
+
);
|
|
255
|
+
await expect3(
|
|
256
|
+
store.commit(s, [inc(3)], makeMeta({ stream: s }), 0)
|
|
257
|
+
).rejects.toBeInstanceOf(ConcurrencyError);
|
|
258
|
+
const found = await collect(store, { stream: s, stream_exact: true });
|
|
259
|
+
expect3(found).toHaveLength(2);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
describe3("query", () => {
|
|
263
|
+
it3("filters by stream, names, correlation, limit, with_snaps", async () => {
|
|
264
|
+
const s1 = `q-s1-${uid()}`;
|
|
265
|
+
const s2 = `q-s2-${uid()}`;
|
|
266
|
+
const cor = `q-cor-${uid()}`;
|
|
267
|
+
await store.commit(
|
|
268
|
+
s1,
|
|
269
|
+
[inc(1), dec(1)],
|
|
270
|
+
makeMeta({ stream: s1, correlation: cor })
|
|
271
|
+
);
|
|
272
|
+
await store.commit(
|
|
273
|
+
s2,
|
|
274
|
+
[inc(2), dec(2), reset()],
|
|
275
|
+
makeMeta({ stream: s2, correlation: cor })
|
|
276
|
+
);
|
|
277
|
+
const byStream = await collect(store, {
|
|
278
|
+
stream: s1,
|
|
279
|
+
stream_exact: true
|
|
280
|
+
});
|
|
281
|
+
expect3(byStream).toHaveLength(2);
|
|
282
|
+
const byName = await collect(store, {
|
|
283
|
+
stream: s2,
|
|
284
|
+
stream_exact: true,
|
|
285
|
+
names: ["Reset"]
|
|
286
|
+
});
|
|
287
|
+
expect3(byName).toHaveLength(1);
|
|
288
|
+
expect3(byName[0].name).toBe("Reset");
|
|
289
|
+
const byCorrelation = await collect(store, { correlation: cor });
|
|
290
|
+
expect3(byCorrelation).toHaveLength(5);
|
|
291
|
+
const limited = await collect(store, {
|
|
292
|
+
correlation: cor,
|
|
293
|
+
limit: 2
|
|
294
|
+
});
|
|
295
|
+
expect3(limited).toHaveLength(2);
|
|
296
|
+
});
|
|
297
|
+
it3("supports backward traversal", async () => {
|
|
298
|
+
const s = `q-back-${uid()}`;
|
|
299
|
+
const committed = await store.commit(
|
|
300
|
+
s,
|
|
301
|
+
[inc(1), inc(2), inc(3)],
|
|
302
|
+
makeMeta({ stream: s })
|
|
303
|
+
);
|
|
304
|
+
const forward = await collect(store, { stream: s, stream_exact: true });
|
|
305
|
+
const backward = await collect(store, {
|
|
306
|
+
stream: s,
|
|
307
|
+
stream_exact: true,
|
|
308
|
+
backward: true
|
|
309
|
+
});
|
|
310
|
+
expect3(forward.map((e) => e.id)).toEqual(committed.map((c) => c.id));
|
|
311
|
+
expect3(backward.map((e) => e.id)).toEqual(
|
|
312
|
+
[...committed].reverse().map((c) => c.id)
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
it3("after/before bound the id range", async () => {
|
|
316
|
+
const s = `q-bounds-${uid()}`;
|
|
317
|
+
const committed = await store.commit(
|
|
318
|
+
s,
|
|
319
|
+
[inc(1), inc(2), inc(3), inc(4)],
|
|
320
|
+
makeMeta({ stream: s })
|
|
321
|
+
);
|
|
322
|
+
const afterFirst = await collect(store, {
|
|
323
|
+
stream: s,
|
|
324
|
+
stream_exact: true,
|
|
325
|
+
after: committed[0].id
|
|
326
|
+
});
|
|
327
|
+
expect3(afterFirst.map((e) => e.id)).toEqual(
|
|
328
|
+
committed.slice(1).map((c) => c.id)
|
|
329
|
+
);
|
|
330
|
+
const beforeLast = await collect(store, {
|
|
331
|
+
stream: s,
|
|
332
|
+
stream_exact: true,
|
|
333
|
+
before: committed[committed.length - 1].id
|
|
334
|
+
});
|
|
335
|
+
expect3(beforeLast.map((e) => e.id)).toEqual(
|
|
336
|
+
committed.slice(0, -1).map((c) => c.id)
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
it3("created_after/created_before filter by timestamp", async () => {
|
|
340
|
+
const s = `q-ts-${uid()}`;
|
|
341
|
+
const before = new Date(Date.now() - 6e4);
|
|
342
|
+
const future = new Date(Date.now() + 6e4);
|
|
343
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
344
|
+
const inWindow = await collect(store, {
|
|
345
|
+
stream: s,
|
|
346
|
+
stream_exact: true,
|
|
347
|
+
created_after: before,
|
|
348
|
+
created_before: future
|
|
349
|
+
});
|
|
350
|
+
expect3(inWindow.length).toBe(1);
|
|
351
|
+
const outOfWindow = await collect(store, {
|
|
352
|
+
stream: s,
|
|
353
|
+
stream_exact: true,
|
|
354
|
+
created_after: future
|
|
355
|
+
});
|
|
356
|
+
expect3(outOfWindow.length).toBe(0);
|
|
357
|
+
});
|
|
358
|
+
it3("backward traversal short-circuits at `after` id boundary", async () => {
|
|
359
|
+
const s = `q-back-after-${uid()}`;
|
|
360
|
+
const committed = await store.commit(
|
|
361
|
+
s,
|
|
362
|
+
[inc(1), inc(2), inc(3)],
|
|
363
|
+
makeMeta({ stream: s })
|
|
364
|
+
);
|
|
365
|
+
const got = await collect(store, {
|
|
366
|
+
stream: s,
|
|
367
|
+
stream_exact: true,
|
|
368
|
+
backward: true,
|
|
369
|
+
after: committed[0].id
|
|
370
|
+
});
|
|
371
|
+
expect3(got.map((e) => e.id)).toEqual([
|
|
372
|
+
committed[2].id,
|
|
373
|
+
committed[1].id
|
|
374
|
+
]);
|
|
375
|
+
});
|
|
376
|
+
it3("backward traversal short-circuits at `created_after` boundary", async () => {
|
|
377
|
+
const s = `q-back-cafter-${uid()}`;
|
|
378
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
379
|
+
const future = new Date(Date.now() + 6e4);
|
|
380
|
+
const got = await collect(store, {
|
|
381
|
+
stream: s,
|
|
382
|
+
stream_exact: true,
|
|
383
|
+
backward: true,
|
|
384
|
+
created_after: future
|
|
385
|
+
});
|
|
386
|
+
expect3(got).toHaveLength(0);
|
|
387
|
+
});
|
|
388
|
+
it3("backward traversal honors created_before by skipping newer events", async () => {
|
|
389
|
+
const s = `q-back-ts-${uid()}`;
|
|
390
|
+
await store.commit(s, [inc(1)], makeMeta());
|
|
391
|
+
const past = new Date(Date.now() - 6e4);
|
|
392
|
+
const got = await collect(store, {
|
|
393
|
+
stream: s,
|
|
394
|
+
stream_exact: true,
|
|
395
|
+
backward: true,
|
|
396
|
+
created_before: past
|
|
397
|
+
});
|
|
398
|
+
expect3(got).toHaveLength(0);
|
|
399
|
+
});
|
|
400
|
+
it3("stream_exact disables regex matching", async () => {
|
|
401
|
+
const tag = uid();
|
|
402
|
+
const a = `q-exact-${tag}`;
|
|
403
|
+
const b = `q-exact-${tag}-extra`;
|
|
404
|
+
await store.commit(a, [inc(1)], makeMeta({ stream: a }));
|
|
405
|
+
await store.commit(b, [inc(2)], makeMeta({ stream: b }));
|
|
406
|
+
const exact = await collect(store, { stream: a, stream_exact: true });
|
|
407
|
+
expect3(exact).toHaveLength(1);
|
|
408
|
+
expect3(exact[0].data).toEqual({ amount: 1 });
|
|
409
|
+
});
|
|
410
|
+
it3("plain regex without anchors is a substring match", async () => {
|
|
411
|
+
const tag = uid();
|
|
412
|
+
const inner = `qr-${tag}-inner`;
|
|
413
|
+
const longer = `qr-${tag}-inner-extra`;
|
|
414
|
+
await store.commit(
|
|
415
|
+
inner,
|
|
416
|
+
[inc(1)],
|
|
417
|
+
makeMeta({ stream: inner })
|
|
418
|
+
);
|
|
419
|
+
await store.commit(
|
|
420
|
+
longer,
|
|
421
|
+
[inc(2)],
|
|
422
|
+
makeMeta({ stream: longer })
|
|
423
|
+
);
|
|
424
|
+
const got = await collect(store, { stream: `qr-${tag}-inner` });
|
|
425
|
+
expect3(got.map((e) => e.stream).sort()).toEqual([inner, longer].sort());
|
|
426
|
+
});
|
|
427
|
+
it3("caller-anchored `^name$` matches only the whole string", async () => {
|
|
428
|
+
const tag = uid();
|
|
429
|
+
const inner = `qr-${tag}-anchor`;
|
|
430
|
+
const longer = `qr-${tag}-anchor-extra`;
|
|
431
|
+
await store.commit(
|
|
432
|
+
inner,
|
|
433
|
+
[inc(1)],
|
|
434
|
+
makeMeta({ stream: inner })
|
|
435
|
+
);
|
|
436
|
+
await store.commit(
|
|
437
|
+
longer,
|
|
438
|
+
[inc(2)],
|
|
439
|
+
makeMeta({ stream: longer })
|
|
440
|
+
);
|
|
441
|
+
const got = await collect(store, { stream: `^qr-${tag}-anchor$` });
|
|
442
|
+
expect3(got).toHaveLength(1);
|
|
443
|
+
expect3(got[0].stream).toBe(inner);
|
|
444
|
+
});
|
|
445
|
+
it3("caller-anchored `^prefix` matches by prefix", async () => {
|
|
446
|
+
const tag = uid();
|
|
447
|
+
const a = `qr-${tag}-pfx-a`;
|
|
448
|
+
const b = `qr-${tag}-pfx-b`;
|
|
449
|
+
const other = `zz-${tag}-other`;
|
|
450
|
+
await store.commit(a, [inc(1)], makeMeta({ stream: a }));
|
|
451
|
+
await store.commit(b, [inc(2)], makeMeta({ stream: b }));
|
|
452
|
+
await store.commit(
|
|
453
|
+
other,
|
|
454
|
+
[inc(3)],
|
|
455
|
+
makeMeta({ stream: other })
|
|
456
|
+
);
|
|
457
|
+
const got = await collect(store, { stream: `^qr-${tag}-pfx-` });
|
|
458
|
+
expect3(got.map((e) => e.stream).sort()).toEqual([a, b].sort());
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
describe3("subscribe + claim + ack", () => {
|
|
462
|
+
it3("subscribes new streams and is idempotent on repeat", async () => {
|
|
463
|
+
const s = `sub-${uid()}`;
|
|
464
|
+
const first = await store.subscribe([{ stream: s }]);
|
|
465
|
+
expect3(first.subscribed).toBe(1);
|
|
466
|
+
const second = await store.subscribe([{ stream: s }]);
|
|
467
|
+
expect3(second.subscribed).toBe(0);
|
|
468
|
+
});
|
|
469
|
+
it3("claims a subscribed stream and ack releases the lease", async () => {
|
|
470
|
+
const s = `claim-${uid()}`;
|
|
471
|
+
await store.subscribe([{ stream: s }]);
|
|
472
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
473
|
+
const by = `worker-${uid()}`;
|
|
474
|
+
const leased = await store.claim(100, 0, by, 1e4);
|
|
475
|
+
const mine = leased.find((l) => l.stream === s);
|
|
476
|
+
expect3(mine).toBeDefined();
|
|
477
|
+
expect3(mine.by).toBe(by);
|
|
478
|
+
await store.ack([{ ...mine, at: mine.at + 1 }]);
|
|
479
|
+
});
|
|
480
|
+
it3("does not double-claim a held lease", async () => {
|
|
481
|
+
const s = `claim-held-${uid()}`;
|
|
482
|
+
const other = `claim-other-${uid()}`;
|
|
483
|
+
await store.subscribe([{ stream: s }]);
|
|
484
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
485
|
+
const leasedA = await store.claim(100, 0, `wA-${uid()}`, 1e5);
|
|
486
|
+
const targetA = leasedA.find((l) => l.stream === s);
|
|
487
|
+
expect3(targetA).toBeDefined();
|
|
488
|
+
await store.subscribe([{ stream: other }]);
|
|
489
|
+
await store.commit(
|
|
490
|
+
other,
|
|
491
|
+
[inc(2)],
|
|
492
|
+
makeMeta({ stream: other })
|
|
493
|
+
);
|
|
494
|
+
const leasedB = await store.claim(100, 0, `wB-${uid()}`, 1e5);
|
|
495
|
+
expect3(leasedB.length).toBeGreaterThan(0);
|
|
496
|
+
expect3(leasedB.find((l) => l.stream === s)).toBeUndefined();
|
|
497
|
+
expect3(leasedB.find((l) => l.stream === other)).toBeDefined();
|
|
498
|
+
});
|
|
499
|
+
it3("supports dual frontiers (lagging + leading)", async () => {
|
|
500
|
+
const s = `claim-dual-${uid()}`;
|
|
501
|
+
await store.subscribe([{ stream: s }]);
|
|
502
|
+
await store.commit(
|
|
503
|
+
s,
|
|
504
|
+
[inc(1), inc(2)],
|
|
505
|
+
makeMeta({ stream: s })
|
|
506
|
+
);
|
|
507
|
+
const first = await store.claim(100, 0, `w-${uid()}`, 1);
|
|
508
|
+
const mine = first.find((l) => l.stream === s);
|
|
509
|
+
expect3(mine).toBeDefined();
|
|
510
|
+
await store.ack([{ ...mine, at: mine.at + 1 }]);
|
|
511
|
+
const second = await store.claim(0, 100, `w-${uid()}`, 1);
|
|
512
|
+
expect3(second.find((l) => l.stream === s)).toBeDefined();
|
|
513
|
+
});
|
|
514
|
+
it3("dedupes when both frontiers would return the same stream", async () => {
|
|
515
|
+
const s = `claim-dedup-${uid()}`;
|
|
516
|
+
await store.subscribe([{ stream: s }]);
|
|
517
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
518
|
+
const claimed = await store.claim(100, 100, `w-${uid()}`, 1e5);
|
|
519
|
+
const matches = claimed.filter((l) => l.stream === s);
|
|
520
|
+
expect3(matches).toHaveLength(1);
|
|
521
|
+
});
|
|
522
|
+
it3("silently ignores ack from the wrong holder", async () => {
|
|
523
|
+
const s = `ack-wrong-${uid()}`;
|
|
524
|
+
const sibling = `ack-sibling-${uid()}`;
|
|
525
|
+
await store.subscribe([{ stream: s }, { stream: sibling }]);
|
|
526
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
527
|
+
await store.commit(
|
|
528
|
+
sibling,
|
|
529
|
+
[inc(2)],
|
|
530
|
+
makeMeta({ stream: sibling })
|
|
531
|
+
);
|
|
532
|
+
const leased = await store.claim(100, 0, `right-${uid()}`, 1e5);
|
|
533
|
+
const mine = leased.find((l) => l.stream === s);
|
|
534
|
+
const siblingLease = leased.find((l) => l.stream === sibling);
|
|
535
|
+
expect3(mine).toBeDefined();
|
|
536
|
+
expect3(siblingLease).toBeDefined();
|
|
537
|
+
const acked = await store.ack([
|
|
538
|
+
{ ...mine, by: "imposter" },
|
|
539
|
+
siblingLease
|
|
540
|
+
]);
|
|
541
|
+
expect3(acked.length).toBeGreaterThan(0);
|
|
542
|
+
expect3(acked.find((l) => l.stream === s)).toBeUndefined();
|
|
543
|
+
});
|
|
544
|
+
it3("ack with a stale (lower) watermark does not throw", async () => {
|
|
545
|
+
const s = `ack-stale-${uid()}`;
|
|
546
|
+
await store.subscribe([{ stream: s }]);
|
|
547
|
+
const by = `w-${uid()}`;
|
|
548
|
+
const leased = await store.claim(100, 0, by, 1e5);
|
|
549
|
+
const mine = leased.find((l) => l.stream === s);
|
|
550
|
+
expect3(mine).toBeDefined();
|
|
551
|
+
await expect3(
|
|
552
|
+
store.ack([{ ...mine, at: -5 }])
|
|
553
|
+
).resolves.toBeDefined();
|
|
554
|
+
});
|
|
555
|
+
it3("claim with no subscribed streams returns an empty array", async () => {
|
|
556
|
+
const fresh = await options.factory();
|
|
557
|
+
try {
|
|
558
|
+
await fresh.drop();
|
|
559
|
+
await fresh.seed();
|
|
560
|
+
const claimed = await fresh.claim(1, 1, `w-${uid()}`, 1e3);
|
|
561
|
+
expect3(claimed).toEqual([]);
|
|
562
|
+
} finally {
|
|
563
|
+
await fresh.dispose();
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
describe3("block", () => {
|
|
568
|
+
it3("hides blocked streams from claim", async () => {
|
|
569
|
+
const s = `block-${uid()}`;
|
|
570
|
+
await store.subscribe([{ stream: s }]);
|
|
571
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
572
|
+
const leased = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
573
|
+
const mine = leased.find((l) => l.stream === s);
|
|
574
|
+
expect3(mine).toBeDefined();
|
|
575
|
+
const others = leased.filter((l) => l.stream !== s);
|
|
576
|
+
await store.ack(others);
|
|
577
|
+
const blocked = await store.block([
|
|
578
|
+
{ ...mine, error: "boom" }
|
|
579
|
+
]);
|
|
580
|
+
expect3(blocked).toHaveLength(1);
|
|
581
|
+
expect3(blocked[0].error).toBe("boom");
|
|
582
|
+
const again = await store.claim(100, 100, `w2-${uid()}`, 1e5);
|
|
583
|
+
expect3(again.find((l) => l.stream === s)).toBeUndefined();
|
|
584
|
+
});
|
|
585
|
+
it3("rejects block calls from a different holder", async () => {
|
|
586
|
+
const s = `block-wrong-${uid()}`;
|
|
587
|
+
await store.subscribe([{ stream: s }]);
|
|
588
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
589
|
+
const leased = await store.claim(100, 0, `right-${uid()}`, 1e5);
|
|
590
|
+
const mine = leased.find((l) => l.stream === s);
|
|
591
|
+
expect3(mine).toBeDefined();
|
|
592
|
+
const others = leased.filter((l) => l.stream !== s);
|
|
593
|
+
await store.ack(others);
|
|
594
|
+
const blocked = await store.block([
|
|
595
|
+
{ ...mine, by: "imposter", error: "no" }
|
|
596
|
+
]);
|
|
597
|
+
expect3(blocked).toHaveLength(0);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
describe3("reset", () => {
|
|
601
|
+
it3("rewinds a stream watermark to -1", async () => {
|
|
602
|
+
const s = `reset-${uid()}`;
|
|
603
|
+
await store.subscribe([{ stream: s }]);
|
|
604
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
605
|
+
const leased = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
606
|
+
const mine = leased.find((l) => l.stream === s);
|
|
607
|
+
expect3(mine).toBeDefined();
|
|
608
|
+
await store.ack([{ ...mine, at: 99 }]);
|
|
609
|
+
expect3(await store.reset([s])).toBe(1);
|
|
610
|
+
const after = await store.claim(100, 0, `w2-${uid()}`, 1e5);
|
|
611
|
+
const back = after.find((l) => l.stream === s);
|
|
612
|
+
expect3(back).toBeDefined();
|
|
613
|
+
expect3(back.at).toBe(-1);
|
|
614
|
+
});
|
|
615
|
+
it3("clears blocked status when resetting", async () => {
|
|
616
|
+
const s = `reset-blk-${uid()}`;
|
|
617
|
+
await store.subscribe([{ stream: s }]);
|
|
618
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
619
|
+
const leased = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
620
|
+
const mine = leased.find((l) => l.stream === s);
|
|
621
|
+
const others = leased.filter((l) => l.stream !== s);
|
|
622
|
+
await store.ack(others);
|
|
623
|
+
await store.block([{ ...mine, error: "boom" }]);
|
|
624
|
+
expect3(await store.reset([s])).toBe(1);
|
|
625
|
+
const after = await store.claim(100, 0, `w2-${uid()}`, 1e5);
|
|
626
|
+
expect3(after.find((l) => l.stream === s)).toBeDefined();
|
|
627
|
+
});
|
|
628
|
+
it3("returns 0 for unknown streams and empty input", async () => {
|
|
629
|
+
expect3(await store.reset([`missing-${uid()}`])).toBe(0);
|
|
630
|
+
expect3(await store.reset([])).toBe(0);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
describe3("prioritize", () => {
|
|
634
|
+
it3("sets priority directly, overriding subscribe's max() rule", async () => {
|
|
635
|
+
const tag = uid();
|
|
636
|
+
const s1 = `pri-${tag}-a`;
|
|
637
|
+
const s2 = `pri-${tag}-b`;
|
|
638
|
+
await store.subscribe([
|
|
639
|
+
{ stream: s1, priority: 5 },
|
|
640
|
+
{ stream: s2, priority: 5 }
|
|
641
|
+
]);
|
|
642
|
+
const updated = await store.prioritize(
|
|
643
|
+
{ stream: s1, stream_exact: true },
|
|
644
|
+
3
|
|
645
|
+
);
|
|
646
|
+
expect3(updated).toBe(1);
|
|
647
|
+
const got1 = {};
|
|
648
|
+
const got2 = {};
|
|
649
|
+
await store.query_streams(
|
|
650
|
+
(p) => {
|
|
651
|
+
if (p.stream === s1) got1.priority = p.priority;
|
|
652
|
+
if (p.stream === s2) got2.priority = p.priority;
|
|
653
|
+
},
|
|
654
|
+
{ stream: `pri-${tag}-.*`, limit: 100 }
|
|
655
|
+
);
|
|
656
|
+
expect3(got1.priority).toBe(3);
|
|
657
|
+
expect3(got2.priority).toBe(5);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
describe3("truncate", () => {
|
|
661
|
+
it3("seeds a tombstone when no snapshot is provided", async () => {
|
|
662
|
+
const s = `trunc-tomb-${uid()}`;
|
|
663
|
+
await store.commit(
|
|
664
|
+
s,
|
|
665
|
+
[inc(1), inc(2)],
|
|
666
|
+
makeMeta({ stream: s })
|
|
667
|
+
);
|
|
668
|
+
const result = await store.truncate([{ stream: s }]);
|
|
669
|
+
expect3(result.get(s)?.deleted).toBe(2);
|
|
670
|
+
const remaining = [];
|
|
671
|
+
await store.query(
|
|
672
|
+
(e) => {
|
|
673
|
+
remaining.push(e);
|
|
674
|
+
},
|
|
675
|
+
{ stream: s, stream_exact: true }
|
|
676
|
+
);
|
|
677
|
+
expect3(remaining).toHaveLength(1);
|
|
678
|
+
expect3(remaining[0].name).toBe(
|
|
679
|
+
"__tombstone__"
|
|
680
|
+
);
|
|
681
|
+
});
|
|
682
|
+
it3("seeds a snapshot when one is provided", async () => {
|
|
683
|
+
const s = `trunc-snap-${uid()}`;
|
|
684
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
685
|
+
const result = await store.truncate([
|
|
686
|
+
{ stream: s, snapshot: { count: 7 } }
|
|
687
|
+
]);
|
|
688
|
+
expect3(result.get(s)?.deleted).toBe(1);
|
|
689
|
+
const remaining = [];
|
|
690
|
+
await store.query(
|
|
691
|
+
(e) => {
|
|
692
|
+
remaining.push(e);
|
|
693
|
+
},
|
|
694
|
+
{ stream: s, stream_exact: true, with_snaps: true }
|
|
695
|
+
);
|
|
696
|
+
expect3(remaining).toHaveLength(1);
|
|
697
|
+
expect3(remaining[0].name).toBe(
|
|
698
|
+
"__snapshot__"
|
|
699
|
+
);
|
|
700
|
+
expect3(remaining[0].data).toEqual({ count: 7 });
|
|
701
|
+
});
|
|
702
|
+
it3("returns an empty map for empty input", async () => {
|
|
703
|
+
const result = await store.truncate([]);
|
|
704
|
+
expect3(result.size).toBe(0);
|
|
705
|
+
});
|
|
706
|
+
it3("returns 0 deleted for streams that don't exist", async () => {
|
|
707
|
+
const s = `trunc-missing-${uid()}`;
|
|
708
|
+
const result = await store.truncate([{ stream: s }]);
|
|
709
|
+
expect3(result.get(s)?.deleted).toBe(0);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
describe3("query_streams", () => {
|
|
713
|
+
it3("returns positions filtered by stream regex, exact, source, and source_exact", async () => {
|
|
714
|
+
const tag = uid();
|
|
715
|
+
const proj1 = `qs-${tag}-projection-tickets`;
|
|
716
|
+
const proj2 = `qs-${tag}-projection-users`;
|
|
717
|
+
const dyn1 = `qs-${tag}-stats-1`;
|
|
718
|
+
const dyn2 = `qs-${tag}-stats-2`;
|
|
719
|
+
const src1 = `qs-${tag}-src-1`;
|
|
720
|
+
const src2 = `qs-${tag}-src-2`;
|
|
721
|
+
await store.subscribe([
|
|
722
|
+
{ stream: proj1 },
|
|
723
|
+
{ stream: proj2 },
|
|
724
|
+
{ stream: dyn1, source: src1 },
|
|
725
|
+
{ stream: dyn2, source: src2 }
|
|
726
|
+
]);
|
|
727
|
+
const all = [];
|
|
728
|
+
const allResult = await store.query_streams(
|
|
729
|
+
(p) => all.push({ stream: p.stream, source: p.source }),
|
|
730
|
+
{ stream: `qs-${tag}-.*` }
|
|
731
|
+
);
|
|
732
|
+
expect3(allResult.count).toBe(4);
|
|
733
|
+
expect3(allResult.maxEventId).toBeGreaterThanOrEqual(-1);
|
|
734
|
+
expect3(all.map((p) => p.stream).sort()).toEqual(
|
|
735
|
+
[proj1, proj2, dyn1, dyn2].sort()
|
|
736
|
+
);
|
|
737
|
+
const projections = [];
|
|
738
|
+
await store.query_streams((p) => projections.push(p.stream), {
|
|
739
|
+
stream: `qs-${tag}-projection-.*`
|
|
740
|
+
});
|
|
741
|
+
expect3(projections.sort()).toEqual([proj1, proj2].sort());
|
|
742
|
+
const exact = [];
|
|
743
|
+
await store.query_streams((p) => exact.push(p.stream), {
|
|
744
|
+
stream: dyn1,
|
|
745
|
+
stream_exact: true
|
|
746
|
+
});
|
|
747
|
+
expect3(exact).toEqual([dyn1]);
|
|
748
|
+
const bySource = [];
|
|
749
|
+
await store.query_streams((p) => bySource.push(p.stream), {
|
|
750
|
+
stream: `qs-${tag}-.*`,
|
|
751
|
+
source: `qs-${tag}-src-.*`
|
|
752
|
+
});
|
|
753
|
+
expect3(bySource.sort()).toEqual([dyn1, dyn2].sort());
|
|
754
|
+
const exactSource = [];
|
|
755
|
+
await store.query_streams((p) => exactSource.push(p.stream), {
|
|
756
|
+
stream: `qs-${tag}-.*`,
|
|
757
|
+
source: src2,
|
|
758
|
+
source_exact: true
|
|
759
|
+
});
|
|
760
|
+
expect3(exactSource).toEqual([dyn2]);
|
|
761
|
+
});
|
|
762
|
+
it3("paginates with limit + after (keyset)", async () => {
|
|
763
|
+
const tag = uid();
|
|
764
|
+
const streams = [
|
|
765
|
+
`qp-${tag}-a`,
|
|
766
|
+
`qp-${tag}-b`,
|
|
767
|
+
`qp-${tag}-c`,
|
|
768
|
+
`qp-${tag}-d`
|
|
769
|
+
];
|
|
770
|
+
await store.subscribe(streams.map((stream) => ({ stream })));
|
|
771
|
+
const page1 = [];
|
|
772
|
+
await store.query_streams((p) => page1.push(p.stream), {
|
|
773
|
+
stream: `qp-${tag}-.*`,
|
|
774
|
+
limit: 2
|
|
775
|
+
});
|
|
776
|
+
expect3(page1).toHaveLength(2);
|
|
777
|
+
const page2 = [];
|
|
778
|
+
await store.query_streams((p) => page2.push(p.stream), {
|
|
779
|
+
stream: `qp-${tag}-.*`,
|
|
780
|
+
limit: 2,
|
|
781
|
+
after: page1.at(-1)
|
|
782
|
+
});
|
|
783
|
+
expect3(page2).toHaveLength(2);
|
|
784
|
+
expect3([...page1, ...page2].sort()).toEqual([...streams].sort());
|
|
785
|
+
});
|
|
786
|
+
it3("filters by blocked status", async () => {
|
|
787
|
+
const tag = uid();
|
|
788
|
+
const s = `qb-${tag}`;
|
|
789
|
+
const sibling = `qb-${tag}-other`;
|
|
790
|
+
await store.subscribe([{ stream: s }, { stream: sibling }]);
|
|
791
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
792
|
+
const leased = await store.claim(100, 0, `w-${uid()}`, 1e5);
|
|
793
|
+
const mine = leased.find((l) => l.stream === s);
|
|
794
|
+
const others = leased.filter((l) => l.stream !== s);
|
|
795
|
+
await store.ack(others);
|
|
796
|
+
await store.block([{ ...mine, error: "boom" }]);
|
|
797
|
+
const blocked = [];
|
|
798
|
+
await store.query_streams(
|
|
799
|
+
(p) => blocked.push({ stream: p.stream, error: p.error }),
|
|
800
|
+
{ stream: `qb-${tag}.*`, blocked: true }
|
|
801
|
+
);
|
|
802
|
+
expect3(blocked).toHaveLength(1);
|
|
803
|
+
expect3(blocked[0].error).toBe("boom");
|
|
804
|
+
const unblocked = [];
|
|
805
|
+
await store.query_streams((p) => unblocked.push(p.stream), {
|
|
806
|
+
stream: `qb-${tag}.*`,
|
|
807
|
+
blocked: false
|
|
808
|
+
});
|
|
809
|
+
expect3(unblocked).toEqual([sibling]);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
describe3("query_streams anchor contract", () => {
|
|
813
|
+
it3("plain regex without anchors is a substring match", async () => {
|
|
814
|
+
const tag = uid();
|
|
815
|
+
const inner = `qsr-${tag}-inner`;
|
|
816
|
+
const longer = `qsr-${tag}-inner-extra`;
|
|
817
|
+
const other = `zz-${tag}-other`;
|
|
818
|
+
await store.subscribe([
|
|
819
|
+
{ stream: inner },
|
|
820
|
+
{ stream: longer },
|
|
821
|
+
{ stream: other }
|
|
822
|
+
]);
|
|
823
|
+
const seen = [];
|
|
824
|
+
await store.query_streams((p) => seen.push(p.stream), {
|
|
825
|
+
stream: `qsr-${tag}-inner`
|
|
826
|
+
});
|
|
827
|
+
expect3(seen.sort()).toEqual([inner, longer].sort());
|
|
828
|
+
});
|
|
829
|
+
it3("caller-anchored `^name$` matches only the whole string", async () => {
|
|
830
|
+
const tag = uid();
|
|
831
|
+
const inner = `qsr-${tag}-anchor`;
|
|
832
|
+
const longer = `qsr-${tag}-anchor-extra`;
|
|
833
|
+
await store.subscribe([{ stream: inner }, { stream: longer }]);
|
|
834
|
+
const seen = [];
|
|
835
|
+
await store.query_streams((p) => seen.push(p.stream), {
|
|
836
|
+
stream: `^qsr-${tag}-anchor$`
|
|
837
|
+
});
|
|
838
|
+
expect3(seen).toEqual([inner]);
|
|
839
|
+
});
|
|
840
|
+
it3("caller-anchored `^prefix` matches by prefix", async () => {
|
|
841
|
+
const tag = uid();
|
|
842
|
+
const a = `qsr-${tag}-pfx-a`;
|
|
843
|
+
const b = `qsr-${tag}-pfx-b`;
|
|
844
|
+
const other = `zz-${tag}-pfx-c`;
|
|
845
|
+
await store.subscribe([
|
|
846
|
+
{ stream: a },
|
|
847
|
+
{ stream: b },
|
|
848
|
+
{ stream: other }
|
|
849
|
+
]);
|
|
850
|
+
const seen = [];
|
|
851
|
+
await store.query_streams((p) => seen.push(p.stream), {
|
|
852
|
+
stream: `^qsr-${tag}-pfx-`
|
|
853
|
+
});
|
|
854
|
+
expect3(seen.sort()).toEqual([a, b].sort());
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
describe3("prioritize anchor contract", () => {
|
|
858
|
+
it3("caller-anchored `^name$` filter matches only the whole string", async () => {
|
|
859
|
+
const tag = uid();
|
|
860
|
+
const inner = `pr-${tag}-anchor`;
|
|
861
|
+
const longer = `pr-${tag}-anchor-extra`;
|
|
862
|
+
await store.subscribe([
|
|
863
|
+
{ stream: inner, priority: 0 },
|
|
864
|
+
{ stream: longer, priority: 0 }
|
|
865
|
+
]);
|
|
866
|
+
const updated = await store.prioritize(
|
|
867
|
+
{ stream: `^pr-${tag}-anchor$` },
|
|
868
|
+
7
|
|
869
|
+
);
|
|
870
|
+
expect3(updated).toBe(1);
|
|
871
|
+
const seen = /* @__PURE__ */ new Map();
|
|
872
|
+
await store.query_streams((p) => seen.set(p.stream, p.priority), {
|
|
873
|
+
stream: `pr-${tag}-anchor`
|
|
874
|
+
});
|
|
875
|
+
expect3(seen.get(inner)).toBe(7);
|
|
876
|
+
expect3(seen.get(longer)).toBe(0);
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
describe3("query_streams head", () => {
|
|
880
|
+
it3("maxEventId tracks the highest committed id", async () => {
|
|
881
|
+
const s = `head-${uid()}`;
|
|
882
|
+
await store.subscribe([{ stream: s }]);
|
|
883
|
+
await store.commit(s, [inc(1)], makeMeta({ stream: s }));
|
|
884
|
+
const positions = [];
|
|
885
|
+
const { maxEventId } = await store.query_streams(
|
|
886
|
+
(p) => positions.push(p.stream),
|
|
887
|
+
{ stream: s, stream_exact: true, limit: 1 }
|
|
888
|
+
);
|
|
889
|
+
expect3(maxEventId).toBeGreaterThanOrEqual(0);
|
|
890
|
+
expect3(positions).toEqual([s]);
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
describe3("seedStream helper coverage", () => {
|
|
894
|
+
it3("commits N events with monotonically increasing ids", async () => {
|
|
895
|
+
const s = `seed-${uid()}`;
|
|
896
|
+
const committed = await seedStream(store, s, 3);
|
|
897
|
+
expect3(committed).toHaveLength(3);
|
|
898
|
+
for (let i = 1; i < committed.length; i++) {
|
|
899
|
+
expect3(committed[i].id).toBeGreaterThan(committed[i - 1].id);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
if (caps.notify) {
|
|
904
|
+
describe3("notify (capability)", () => {
|
|
905
|
+
it3("delivers a notification when a different instance commits", async () => {
|
|
906
|
+
const notify = store.notify;
|
|
907
|
+
expect3(notify).toBeDefined();
|
|
908
|
+
const received = [];
|
|
909
|
+
let resolveArrived;
|
|
910
|
+
const arrived = new Promise((res) => {
|
|
911
|
+
resolveArrived = res;
|
|
912
|
+
});
|
|
913
|
+
const disposer = await notify.call(store, (n) => {
|
|
914
|
+
received.push(n);
|
|
915
|
+
resolveArrived();
|
|
916
|
+
});
|
|
917
|
+
const writer = await options.factory();
|
|
918
|
+
try {
|
|
919
|
+
const stream = `notify-${uid()}`;
|
|
920
|
+
await writer.commit(
|
|
921
|
+
stream,
|
|
922
|
+
[inc(1)],
|
|
923
|
+
makeMeta({ stream })
|
|
924
|
+
);
|
|
925
|
+
await arrived;
|
|
926
|
+
expect3(received.length).toBeGreaterThanOrEqual(1);
|
|
927
|
+
expect3(received[0].stream).toBe(stream);
|
|
928
|
+
expect3(received[0].events.length).toBeGreaterThanOrEqual(1);
|
|
929
|
+
} finally {
|
|
930
|
+
await writer.dispose();
|
|
931
|
+
await Promise.resolve(disposer());
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
};
|
|
938
|
+
export {
|
|
939
|
+
COUNTER_EVENT_NAMES,
|
|
940
|
+
CounterSchemas,
|
|
941
|
+
Decremented,
|
|
942
|
+
Incremented,
|
|
943
|
+
Reset,
|
|
944
|
+
actor,
|
|
945
|
+
collect,
|
|
946
|
+
dec,
|
|
947
|
+
inc,
|
|
948
|
+
makeMeta,
|
|
949
|
+
reset,
|
|
950
|
+
runCacheTck,
|
|
951
|
+
runLoggerTck,
|
|
952
|
+
runStoreTck,
|
|
953
|
+
seedStream,
|
|
954
|
+
uid
|
|
955
|
+
};
|
|
956
|
+
//# sourceMappingURL=index.js.map
|