@remit/drizzle-service 1.0.0 → 1.0.2
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
package/src/repos/message.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
lt,
|
|
19
19
|
or,
|
|
20
20
|
type SQL,
|
|
21
|
+
sql,
|
|
21
22
|
} from "drizzle-orm";
|
|
22
23
|
import type { SqliteBatchItem, WriteBatch } from "../batch.js";
|
|
23
24
|
import { chunk, chunkRows } from "../bound-parameters.js";
|
|
@@ -280,6 +281,32 @@ const placementTerms = (expected: PlacementPredicate): SQL[] => {
|
|
|
280
281
|
return terms;
|
|
281
282
|
};
|
|
282
283
|
|
|
284
|
+
/** The rows `listStrandedPlacements` returns: `moving` or `deleting`, unwritten since `untouchedSince`. */
|
|
285
|
+
export const strandedPlacementCondition = (
|
|
286
|
+
mailboxIds: readonly string[],
|
|
287
|
+
untouchedSince: number,
|
|
288
|
+
): SQL =>
|
|
289
|
+
and(
|
|
290
|
+
lt(messageTable.updatedAt, untouchedSince),
|
|
291
|
+
or(
|
|
292
|
+
and(
|
|
293
|
+
eq(messageTable.status, "moving"),
|
|
294
|
+
inList(messageTable.originalMailboxId, mailboxIds),
|
|
295
|
+
isNotNull(messageTable.originalUid),
|
|
296
|
+
),
|
|
297
|
+
and(
|
|
298
|
+
eq(messageTable.status, "deleting"),
|
|
299
|
+
inList(messageTable.mailboxId, mailboxIds),
|
|
300
|
+
),
|
|
301
|
+
and(
|
|
302
|
+
eq(messageTable.status, "moving"),
|
|
303
|
+
eq(messageTable.uid, 0),
|
|
304
|
+
isNull(messageTable.originalUid),
|
|
305
|
+
inList(messageTable.mailboxId, mailboxIds),
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
) ?? sql`0`;
|
|
309
|
+
|
|
283
310
|
export class DrizzleMessageRepository implements IMessageRepository {
|
|
284
311
|
private readonly db: DB;
|
|
285
312
|
private readonly batch: WriteBatch;
|
|
@@ -888,32 +915,10 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
888
915
|
untouchedSince: number,
|
|
889
916
|
): ReturnType<IMessageRepository["listStrandedPlacements"]> {
|
|
890
917
|
if (mailboxIds.length === 0) return [];
|
|
891
|
-
const ids = [...mailboxIds];
|
|
892
918
|
const rows = await this.db
|
|
893
919
|
.select()
|
|
894
920
|
.from(messageTable)
|
|
895
|
-
.where(
|
|
896
|
-
and(
|
|
897
|
-
lt(messageTable.updatedAt, untouchedSince),
|
|
898
|
-
or(
|
|
899
|
-
and(
|
|
900
|
-
eq(messageTable.status, "moving"),
|
|
901
|
-
inList(messageTable.originalMailboxId, ids),
|
|
902
|
-
isNotNull(messageTable.originalUid),
|
|
903
|
-
),
|
|
904
|
-
and(
|
|
905
|
-
eq(messageTable.status, "deleting"),
|
|
906
|
-
inList(messageTable.mailboxId, ids),
|
|
907
|
-
),
|
|
908
|
-
and(
|
|
909
|
-
eq(messageTable.status, "moving"),
|
|
910
|
-
eq(messageTable.uid, 0),
|
|
911
|
-
isNull(messageTable.originalUid),
|
|
912
|
-
inList(messageTable.mailboxId, ids),
|
|
913
|
-
),
|
|
914
|
-
),
|
|
915
|
-
),
|
|
916
|
-
)
|
|
921
|
+
.where(strandedPlacementCondition(mailboxIds, untouchedSince))
|
|
917
922
|
.orderBy(asc(messageTable.updatedAt), asc(messageTable.messageId));
|
|
918
923
|
return rows.map(toMessageItem);
|
|
919
924
|
}
|
|
@@ -161,6 +161,29 @@ describe("the thread-message search index in its own database", () => {
|
|
|
161
161
|
assert.deepEqual(await found("receipt"), [created.threadMessageId]);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
test("rebuilds while a live writer indexes a message the rebuild has yet to reach", async () => {
|
|
165
|
+
const created = await repo.create(input({ subject: "Raced reminder" }));
|
|
166
|
+
const store = syncStore(search.db, BETTER_SQLITE3_LIMITS);
|
|
167
|
+
let raced = false;
|
|
168
|
+
const migrator = new SearchIndex(mail.db, {
|
|
169
|
+
...store,
|
|
170
|
+
batch: async (items) => {
|
|
171
|
+
if (!raced) {
|
|
172
|
+
raced = true;
|
|
173
|
+
await index.sync([created.threadMessageId]);
|
|
174
|
+
}
|
|
175
|
+
return store.batch(items);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await migrator.rebuild();
|
|
180
|
+
await migrator.rebuild();
|
|
181
|
+
|
|
182
|
+
assert.equal(raced, true);
|
|
183
|
+
assert.equal(await index.needsRebuild(), false);
|
|
184
|
+
assert.deepEqual(await found("reminder"), [created.threadMessageId]);
|
|
185
|
+
});
|
|
186
|
+
|
|
164
187
|
test("matches the body text past the snippet, through a rewrite and a rebuild", async () => {
|
|
165
188
|
const created = await repo.create(
|
|
166
189
|
input({ subject: "Long letter", snippet: "Dear reader, a preview" }),
|
|
@@ -267,13 +267,24 @@ export class SearchIndex {
|
|
|
267
267
|
.limit(DOCUMENTS_PER_REBUILD_PAGE);
|
|
268
268
|
const last = rows.at(-1);
|
|
269
269
|
if (!last) return indexed;
|
|
270
|
+
// A live writer may index one of these messages after the delete
|
|
271
|
+
// above, so each page replaces its documents the way sync does.
|
|
270
272
|
await this.batch(
|
|
271
|
-
chunkRows(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
273
|
+
chunkRows(
|
|
274
|
+
threadMessageSearchTable,
|
|
275
|
+
rows,
|
|
276
|
+
this.maxBoundParameters,
|
|
277
|
+
).flatMap((slice) => [
|
|
278
|
+
this.search.delete(threadMessageSearchTable).where(
|
|
279
|
+
inArray(
|
|
280
|
+
threadMessageSearchTable.rowid,
|
|
281
|
+
slice.map((row) => searchRowid(row.threadMessageId)),
|
|
282
|
+
),
|
|
283
|
+
),
|
|
284
|
+
this.search
|
|
285
|
+
.insert(threadMessageSearchTable)
|
|
286
|
+
.values(slice.map(toDocument)),
|
|
287
|
+
]),
|
|
277
288
|
);
|
|
278
289
|
indexed += rows.length;
|
|
279
290
|
after = last.threadMessageId;
|