@pepys/testing 0.1.0 → 0.1.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 +3 -0
- package/package.json +4 -3
- package/src/fact-log/apply-fact-log.ts +58 -0
- package/src/index.ts +1 -0
- package/src/test-store/create-test-store.ts +20 -10
- package/src/test-store/store-hooks.ts +16 -0
package/README.md
CHANGED
|
@@ -28,6 +28,9 @@ Set the compiler options from the [kit's README](../kit/README.md#configuration)
|
|
|
28
28
|
## Features
|
|
29
29
|
|
|
30
30
|
- `createTestStore`: apply facts or write rows, then run a query, read rows and read links.
|
|
31
|
+
- `applyFactLog`: apply the facts that `createIngest` of `@pepys/ingest` stored in a memory fact log
|
|
32
|
+
to a test store, in place of the host. A second call applies only the records the log stored after
|
|
33
|
+
the first.
|
|
31
34
|
- `applyInBothOrders`: apply facts in two orders and compare the stores.
|
|
32
35
|
- The memory adapter of the store reader, for a test of a core query.
|
|
33
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pepys/testing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Test a pepys plug-in on Bun: an in-memory store that runs its consumers and queries",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@pepys/
|
|
23
|
-
"@pepys/
|
|
22
|
+
"@pepys/ingest": "0.1.1",
|
|
23
|
+
"@pepys/kit": "0.1.1",
|
|
24
|
+
"@pepys/kysely-turso": "0.1.1",
|
|
24
25
|
"@tursodatabase/database": "0.8.3",
|
|
25
26
|
"kysely": "0.29.8",
|
|
26
27
|
"tiny-invariant": "1.3.3",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { MemoryFactLog } from '@pepys/ingest';
|
|
2
|
+
import { toStreamName, toStreamOffset } from '@pepys/kit';
|
|
3
|
+
import type { CheckedDeployment, FactEnvelope, StreamName } from '@pepys/kit';
|
|
4
|
+
import invariant from 'tiny-invariant';
|
|
5
|
+
import { storeHooks } from '../test-store/store-hooks.ts';
|
|
6
|
+
import type { TestStore } from '../test-store/types.ts';
|
|
7
|
+
|
|
8
|
+
// The host's part for a test: the records of every stream a consumer reads go to the store as one
|
|
9
|
+
// batch, whole streams in the order the consumers name them. A refusal throws an ApplyError and
|
|
10
|
+
// moves no offset, so the next call reads the same records again.
|
|
11
|
+
export async function applyFactLog(
|
|
12
|
+
store: TestStore,
|
|
13
|
+
log: MemoryFactLog,
|
|
14
|
+
deployment: CheckedDeployment,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const hooks = storeHooks.get(store);
|
|
17
|
+
|
|
18
|
+
if (hooks === undefined) {
|
|
19
|
+
throw new Error('the store is not the object that createTestStore returned');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const streams = new Set(deployment.consumers.flatMap((consumer) => consumer.input.streams));
|
|
23
|
+
|
|
24
|
+
const records: { stream: StreamName; envelope: FactEnvelope<unknown> }[] = [];
|
|
25
|
+
|
|
26
|
+
const tails = new Map<StreamName, number>();
|
|
27
|
+
|
|
28
|
+
for (const declared of streams) {
|
|
29
|
+
const stream = toStreamName(declared);
|
|
30
|
+
const from = hooks.nextOffsets.get(stream) ?? 0;
|
|
31
|
+
|
|
32
|
+
const tail = await log.readTail(stream);
|
|
33
|
+
|
|
34
|
+
if (!tail.ok) {
|
|
35
|
+
throw new Error(`the fact log has no stream ${declared}, which a consumer reads`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (tail.value === from) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const read = await log.read(stream, toStreamOffset(from), tail.value - from);
|
|
43
|
+
|
|
44
|
+
invariant(read.ok, 'a stream with a tail can be read');
|
|
45
|
+
|
|
46
|
+
for (const stored of read.value) {
|
|
47
|
+
records.push({ stream, envelope: stored.fact });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
tails.set(stream, tail.value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await hooks.applyEnvelopes(records);
|
|
54
|
+
|
|
55
|
+
for (const [stream, tail] of tails) {
|
|
56
|
+
hooks.nextOffsets.set(stream, tail);
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { applyFactLog } from './fact-log/apply-fact-log.ts';
|
|
1
2
|
export { applyInBothOrders } from './orders/apply-in-both-orders.ts';
|
|
2
3
|
export type { OrderReport, StoreSnapshot } from './orders/apply-in-both-orders.ts';
|
|
3
4
|
export type { MemoryStoreReader } from './memory-store-reader/create-memory-store-reader.ts';
|
|
@@ -11,6 +11,8 @@ import { ApplyError } from '../utils/apply-error.ts';
|
|
|
11
11
|
import { parseAppliedFacts } from './parse-applied-facts.ts';
|
|
12
12
|
import { runConsumers } from './run-consumers.ts';
|
|
13
13
|
import type { AppliedFactIDs } from './run-consumers.ts';
|
|
14
|
+
import { storeHooks } from './store-hooks.ts';
|
|
15
|
+
import type { StoreHooks } from './store-hooks.ts';
|
|
14
16
|
import type { TestStore } from './types.ts';
|
|
15
17
|
|
|
16
18
|
interface TestStoreOptions {
|
|
@@ -48,7 +50,19 @@ export async function createTestStore(options: TestStoreOptions): Promise<TestSt
|
|
|
48
50
|
return table;
|
|
49
51
|
};
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
const applyEnvelopes: StoreHooks['applyEnvelopes'] = async (records) => {
|
|
54
|
+
const next = buildFactIndex(index, records);
|
|
55
|
+
|
|
56
|
+
applied = await db
|
|
57
|
+
.transaction()
|
|
58
|
+
.execute((transaction) =>
|
|
59
|
+
runConsumers(transaction, deployment.consumers, { ...next, applied }),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
index = next.index;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const testStore: TestStore = {
|
|
52
66
|
writeRows: async (_consumer, table, rows) => {
|
|
53
67
|
for (const row of rows) {
|
|
54
68
|
await writeRow(db, table, getTable(table), row);
|
|
@@ -61,15 +75,7 @@ export async function createTestStore(options: TestStoreOptions): Promise<TestSt
|
|
|
61
75
|
throw new ApplyError(envelopes.failure);
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
applied = await db
|
|
67
|
-
.transaction()
|
|
68
|
-
.execute((transaction) =>
|
|
69
|
-
runConsumers(transaction, deployment.consumers, { ...next, applied }),
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
index = next.index;
|
|
78
|
+
await applyEnvelopes(envelopes.value);
|
|
73
79
|
},
|
|
74
80
|
query: async (query, input) => {
|
|
75
81
|
const parsed = query.input.parse(input);
|
|
@@ -91,4 +97,8 @@ export async function createTestStore(options: TestStoreOptions): Promise<TestSt
|
|
|
91
97
|
reader,
|
|
92
98
|
[Symbol.asyncDispose]: () => db.destroy(),
|
|
93
99
|
};
|
|
100
|
+
|
|
101
|
+
storeHooks.set(testStore, { applyEnvelopes, nextOffsets: new Map() });
|
|
102
|
+
|
|
103
|
+
return testStore;
|
|
94
104
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FactEnvelope, StreamName } from '@pepys/kit';
|
|
2
|
+
import type { TestStore } from './types.ts';
|
|
3
|
+
|
|
4
|
+
export interface StoreHooks {
|
|
5
|
+
// Runs the consumers on the records as one batch in one transaction. A refusal throws an
|
|
6
|
+
// ApplyError, and the batch then writes nothing.
|
|
7
|
+
applyEnvelopes: (
|
|
8
|
+
records: readonly { stream: string; envelope: FactEnvelope<unknown> }[],
|
|
9
|
+
) => Promise<void>;
|
|
10
|
+
|
|
11
|
+
// the offset of the next record to apply from a fact log, by stream
|
|
12
|
+
nextOffsets: Map<StreamName, number>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// What a test store gives the package's own functions and keeps out of its public type.
|
|
16
|
+
export const storeHooks = new WeakMap<TestStore, StoreHooks>();
|