mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(search): key the in-flight search by actor and record
Two tabs searching two different records (one's own and a shared one) cancelled each other, because a new search aborted any earlier one by the same actor. The slot is now held per actor and record.
This commit is contained in:
1 parent
24f06884b3
commit
88aec7edb6
3 files changed
+39
-9
No files matched your search
@@ -90,7 +90,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
}
|
||||
|
||||
const slot = beginSearch(actor.id);
|
||||
const slot = beginSearch(actor.id, user.id);
|
||||
try {
|
||||
const [modules, domainVisible, timezone] = await Promise.all([
|
||||
resolveModuleMap(user.id),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { SUPERSEDED, beginSearch, searchesInFlight } from "../in-flight";
|
||||
|
||||
describe("beginSearch", () => {
|
||||
it("a newer search of the same record stops the one before it", () => {
|
||||
const first = beginSearch("actor", "record-a");
|
||||
const second = beginSearch("actor", "record-a");
|
||||
expect(first.signal.aborted).toBe(true);
|
||||
expect(first.signal.reason).toBe(SUPERSEDED);
|
||||
expect(second.signal.aborted).toBe(false);
|
||||
first.end();
|
||||
second.end();
|
||||
expect(searchesInFlight()).toBe(0);
|
||||
});
|
||||
|
||||
it("searches of two different records by one actor run side by side", () => {
|
||||
const own = beginSearch("actor", "record-a");
|
||||
const shared = beginSearch("actor", "record-b");
|
||||
expect(own.signal.aborted).toBe(false);
|
||||
expect(shared.signal.aborted).toBe(false);
|
||||
own.end();
|
||||
shared.end();
|
||||
expect(searchesInFlight()).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,14 @@
|
||||
/**
|
||||
* One search in flight per actor.
|
||||
* One search in flight per actor and record.
|
||||
*
|
||||
* The palette searches once per pause in typing, and the client cancels the
|
||||
* previous request when the text changes. A cancelled fetch does not stop the
|
||||
* server, though: without this, every keystroke-pause left a search reading
|
||||
* and decrypting on the server until its own budget ran out, so a fast typist
|
||||
* held several at once. A new search by the same actor aborts the one before
|
||||
* it, which stops at its next budget check.
|
||||
* held several at once. A new search by the same actor in the same record
|
||||
* aborts the one before it, which stops at its next budget check. Searches
|
||||
* of two different records (a tab on one's own record, another on a record
|
||||
* shared with them) are two separate palettes and never cancel each other.
|
||||
*
|
||||
* Per process: on a multi-instance deploy two searches by one actor on two
|
||||
* instances both run, each still bounded by its own time budget and by the
|
||||
@@ -18,20 +20,22 @@ const inFlight = new Map<string, AbortController>();
|
||||
export const SUPERSEDED = "search.superseded";
|
||||
|
||||
export interface SearchSlot {
|
||||
/** Aborted when the same actor starts a newer search. */
|
||||
/** Aborted when the same actor starts a newer search of the same record. */
|
||||
signal: AbortSignal;
|
||||
/** Release the slot once the search answered (or failed). */
|
||||
end: () => void;
|
||||
}
|
||||
|
||||
export function beginSearch(actorId: string): SearchSlot {
|
||||
inFlight.get(actorId)?.abort(SUPERSEDED);
|
||||
export function beginSearch(actorId: string, recordId: string): SearchSlot {
|
||||
// Ids are cuids, so the separator cannot occur inside either part.
|
||||
const key = `${actorId}:${recordId}`;
|
||||
inFlight.get(key)?.abort(SUPERSEDED);
|
||||
const controller = new AbortController();
|
||||
inFlight.set(actorId, controller);
|
||||
inFlight.set(key, controller);
|
||||
return {
|
||||
signal: controller.signal,
|
||||
end: () => {
|
||||
if (inFlight.get(actorId) === controller) inFlight.delete(actorId);
|
||||
if (inFlight.get(key) === controller) inFlight.delete(key);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user