@remit/search-index-worker 0.0.24 → 0.0.25
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/package.json
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import type { SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
5
|
+
import {
|
|
6
|
+
DrizzleMessageRepository,
|
|
7
|
+
OUTBOX_EVENTS,
|
|
8
|
+
} from "@remit/drizzle-service";
|
|
9
|
+
import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
|
|
10
|
+
import { OutboxRelay } from "@remit/outbox-relay";
|
|
11
|
+
import { SqliteOutboxStore } from "./sqlite-outbox-drain.js";
|
|
12
|
+
|
|
13
|
+
type SqliteHandle = ReturnType<typeof createShippedSqliteDb>["sqlite"];
|
|
14
|
+
|
|
15
|
+
// The outbox invariant (reader#1063): every event kind a producer writes has a
|
|
16
|
+
// consumer that drains it. `message.created` was written once per message and
|
|
17
|
+
// drained by nothing, so a live instance carried 30,906 rows that could never
|
|
18
|
+
// clear. This runs the real producers (the message repo) and the real consumer
|
|
19
|
+
// (the relay over the SQLite store) against the shipped migrations, so a kind
|
|
20
|
+
// with no drain shows up as a row that stays undrained.
|
|
21
|
+
//
|
|
22
|
+
// What holds the invariant is the union `OUTBOX_EVENTS`, not this file: the
|
|
23
|
+
// column's type is what stops a producer in another package inventing a kind,
|
|
24
|
+
// and the compiler is what reports it. This covers the kinds the union declares
|
|
25
|
+
// and the producers in this repo's message repo — a producer elsewhere reaches
|
|
26
|
+
// it only by adding its kind to the union first.
|
|
27
|
+
|
|
28
|
+
const fakeSqs = (sent: string[]) =>
|
|
29
|
+
({
|
|
30
|
+
send: async (cmd: SendMessageCommand) => {
|
|
31
|
+
sent.push(String(cmd.input.MessageBody));
|
|
32
|
+
return {};
|
|
33
|
+
},
|
|
34
|
+
}) as unknown as ConstructorParameters<typeof OutboxRelay>[0]["sqs"];
|
|
35
|
+
|
|
36
|
+
const drainAll = async (sqlite: SqliteHandle): Promise<void> => {
|
|
37
|
+
const relay = new OutboxRelay({
|
|
38
|
+
store: new SqliteOutboxStore(sqlite as unknown as never),
|
|
39
|
+
sqs: fakeSqs([]),
|
|
40
|
+
queueUrl: "q",
|
|
41
|
+
});
|
|
42
|
+
while ((await relay.drainPending()) > 0) {
|
|
43
|
+
// Drain reads a bounded batch; repeat until the table is quiet.
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const undrainedEvents = (sqlite: SqliteHandle): string[] =>
|
|
48
|
+
(
|
|
49
|
+
sqlite
|
|
50
|
+
.prepare(
|
|
51
|
+
"SELECT DISTINCT event FROM outbox WHERE processed_at IS NULL ORDER BY event",
|
|
52
|
+
)
|
|
53
|
+
.all() as Array<{ event: string }>
|
|
54
|
+
).map((row) => row.event);
|
|
55
|
+
|
|
56
|
+
const writtenEvents = (sqlite: SqliteHandle): string[] =>
|
|
57
|
+
(
|
|
58
|
+
sqlite
|
|
59
|
+
.prepare("SELECT DISTINCT event FROM outbox ORDER BY event")
|
|
60
|
+
.all() as Array<{ event: string }>
|
|
61
|
+
).map((row) => row.event);
|
|
62
|
+
|
|
63
|
+
const NOW = 1700000000000;
|
|
64
|
+
const MAILBOX_ID = "00000000-0000-0000-4444-000000000001";
|
|
65
|
+
const DEST_MAILBOX_ID = "00000000-0000-0000-4444-000000000002";
|
|
66
|
+
|
|
67
|
+
const createInput = (messageId: string) => ({
|
|
68
|
+
messageId,
|
|
69
|
+
mailboxId: MAILBOX_ID,
|
|
70
|
+
uid: 1,
|
|
71
|
+
sequenceNumber: 1,
|
|
72
|
+
rfc822Size: 512,
|
|
73
|
+
internalDate: NOW,
|
|
74
|
+
envelopeId: randomUUID(),
|
|
75
|
+
rootBodyPartId: randomUUID(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("outbox drain coverage", () => {
|
|
79
|
+
test("every event the message repo writes is drained", async () => {
|
|
80
|
+
const { db, sqlite, close } = createShippedSqliteDb();
|
|
81
|
+
const repo = new DrizzleMessageRepository(
|
|
82
|
+
db as unknown as ConstructorParameters<
|
|
83
|
+
typeof DrizzleMessageRepository
|
|
84
|
+
>[0],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const bodySyncedId = randomUUID();
|
|
88
|
+
await repo.create(createInput(bodySyncedId));
|
|
89
|
+
await repo.update(bodySyncedId, { bodyStorageKey: "body/1.json" });
|
|
90
|
+
|
|
91
|
+
const movedId = randomUUID();
|
|
92
|
+
await repo.create(createInput(movedId));
|
|
93
|
+
await repo.updateUid(movedId, 99, DEST_MAILBOX_ID);
|
|
94
|
+
|
|
95
|
+
const removedId = randomUUID();
|
|
96
|
+
await repo.create(createInput(removedId));
|
|
97
|
+
await repo.delete(removedId);
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(
|
|
100
|
+
writtenEvents(sqlite),
|
|
101
|
+
[...OUTBOX_EVENTS].sort(),
|
|
102
|
+
"the producers write exactly the declared outbox vocabulary",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await drainAll(sqlite);
|
|
106
|
+
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
undrainedEvents(sqlite),
|
|
109
|
+
[],
|
|
110
|
+
"a kind left undrained here grows the outbox forever",
|
|
111
|
+
);
|
|
112
|
+
close();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("every kind the union declares is drained", async () => {
|
|
116
|
+
const { sqlite, close } = createShippedSqliteDb();
|
|
117
|
+
|
|
118
|
+
for (const event of OUTBOX_EVENTS) {
|
|
119
|
+
sqlite
|
|
120
|
+
.prepare(
|
|
121
|
+
`INSERT INTO outbox (id, message_id, event, payload, created_at)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
123
|
+
)
|
|
124
|
+
.run(
|
|
125
|
+
randomUUID(),
|
|
126
|
+
`m-${event}`,
|
|
127
|
+
event,
|
|
128
|
+
JSON.stringify({ messageId: `m-${event}` }),
|
|
129
|
+
Date.now(),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await drainAll(sqlite);
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(undrainedEvents(sqlite), []);
|
|
136
|
+
close();
|
|
137
|
+
});
|
|
138
|
+
});
|