Skip to content

[2e] Read the whole text in the host with a form-in-text index #439

Closed

Description

@imnasnainaec

Part of Phase 2 (#425) of the data layer plan.

Whole-text read in BookService: reads every book of the source text, keeps a form-in-text index, and reports progress and failed books through getTextReadStatus; addressed by source project, and from 3k by session. Not called by the WebView yet.

Done when: on the #407 datasets the index finds the same catalog forms not in the text as main's whole-text read, a book that fails to read is reported and turns off "not in the text" marking, and a Scripture edit updates that book's entries.

Depends on: 2a (#435), 2d (#438).

From the plan

API

Call Kind Request Response Replaces today Issue
getTextReadStatus query source project (2e to 4c), or session (from 3k) Books read, books in the text, books that failed to read The read status TextReadingStatus shows 2e, 3k

Cross-book answers

Answer Today Planned Freshness Issue
Forms in the text The concordance's whole-text read in the WebView, run when a catalog filter or the concordance needs it 2e's form-in-text index, read at session open; getCatalogRows filters by it At session open, and per book on a Scripture edit 2e, 5a
Text read status TextReadingStatus over the WebView's read getTextReadStatus Refetched when named 2e, 5a

Performance checkpoint

After What to check Baseline (large-loadable)
2e Whole-text read time and host heap with every book read; decides whether the host reads every book at session open or, as on main, only on demand Read every book 22.9 / 23.5 s

Risks

Risk Effect Mitigation
Reading every book at session open costs too much Slow session open, or a large host heap The read runs in the background and the catalog shows its progress; the checkpoint after 2e decides, and the fallback is reading on demand, as on main, with the host re-anchoring each book as getTokenizedBook serves it

Decisions

Question Decision From
Whole-text read Carried: the host reads every book of the source text at session open, re-anchors each linked book as it arrives and builds a form-in-text index, so Stale covers the whole draft once the read finishes. The checkpoint after 2e decides whether that cost is affordable; if not, the read stays on demand, as on main —

Considered and rejected

  • Moving the text-only answers into Phase 2 (the concordance's occurrences and the form-in-text check, joined with the WebView's analysis), so 4c would remove the text bridge. It adds WebView wiring to Phase 2 and splits the concordance across two issues. It's the fallback if the 2e checkpoint or 4a's latency gate fails because the text is read twice from 4a to 5d.

Activity

  1. alex-rawlings-yyc commented on Oct 16, 2026

    @alex-rawlings-yyc
    Contributor

    BookService keeps only each project's most recently requested book (2a, #460). Reading the whole text through getTokenizedBook pushes the open book out of that cache and stops watching its text. That's intended for 2a, but 2e will need its own watches rather than relying on the cache's.

  2. self-assigned this
    on Oct 19, 2026
  3. imnasnainaec commented on Oct 19, 2026

    @imnasnainaec
    ContributorAuthor

    2d (#468) watches each project's open book (the last one outlined) separately from BookService's one-book cache, and reports its edits through bookService.onOpenBookTextChanged. That leaves for 2e:

    • The open book has two USJ subscriptions while it's also the held book.
    • The whole-text read evicts the open book, so the next page fetch re-reads and re-tokenizes it. A host-side read pays that too.
    • Closing waits on the open watch's first delivery as well as the held book's (the stop() stall from Fetch and tokenize books in a host BookService (#435) #460). Only deactivate awaits it.

    Keeping the open book in the cache beside the latest book fixes all three: one subscription per book, no eviction. The read still needs its own watches to track edits to every book it keeps. onOpenBookTextChanged is small enough to fold into them.

    A patch against #468's head (65f5ee6) that keeps the open book in the cache, with its tests:

    Patch
    diff --git a/src/__tests__/services/bookService.test.ts b/src/__tests__/services/bookService.test.ts
    index 67533a24..c481a55c 100644
    --- a/src/__tests__/services/bookService.test.ts
    +++ b/src/__tests__/services/bookService.test.ts
    @@ -539,12 +539,17 @@ describe('onOpenBookTextChanged', () => {
       const unsubscribers: Array<() => boolean> = [];
     
       /** A listener registered for the test, removed once it ends. */
    -  function listen() {
    -    const listener = jest.fn();
    +  function listen(onCall?: () => void) {
    +    const listener = jest.fn(onCall);
         unsubscribers.push(onOpenBookTextChanged(listener));
         return listener;
       }
     
    +  /** The subscriptions made to `bookId`, oldest first. */
    +  function subscriptionsTo(project: ReturnType<typeof serveProject>, bookId: string) {
    +    return project.subscriptions.filter((s) => s.bookId === bookId);
    +  }
    +
       beforeEach(() => {
         segmentStandIns();
       });
    @@ -553,6 +558,69 @@ describe('onOpenBookTextChanged', () => {
         unsubscribers.splice(0).forEach((unsubscribe) => unsubscribe());
       });
     
    +  it('subscribes once to a book that is outlined and read whole', async () => {
    +    const project = serveProject((bookId) => usj(bookId));
    +
    +    await getBookOutline('src', 'GEN');
    +    await getTokenizedBook('src', 'GEN');
    +    await getSegments('src', 'GEN', 0, 1);
    +
    +    expect(project.subscriptions).toHaveLength(1);
    +  });
    +
    +  it('subscribes once to a book however often it is outlined', async () => {
    +    const project = serveProject((bookId) => usj(bookId));
    +
    +    await getBookOutline('src', 'GEN');
    +    await getBookOutline('src', 'GEN');
    +
    +    expect(project.subscriptions).toHaveLength(1);
    +  });
    +
    +  it('keeps the outlined book while other books are read whole', async () => {
    +    const project = serveProject((bookId) => usj(bookId));
    +    await getBookOutline('src', 'GEN');
    +    await getTokenizedBook('src', 'EXO');
    +    await getTokenizedBook('src', 'LEV');
    +    await settle();
    +    jest.mocked(tokenizeBook).mockClear();
    +
    +    await getTokenizedBook('src', 'GEN');
    +    await getBookOutline('src', 'GEN');
    +
    +    expect(tokenizeBook).not.toHaveBeenCalled();
    +    expect(subscriptionsTo(project, 'GEN')).toHaveLength(1);
    +    expect(subscriptionsTo(project, 'GEN')[0].unsubscribe).not.toHaveBeenCalled();
    +  });
    +
    +  it('serves pages of the outlined book after another book was read whole', async () => {
    +    const project = serveProject((bookId) => usj(bookId));
    +    await getBookOutline('src', 'GEN');
    +    await getTokenizedBook('src', 'EXO');
    +    jest.mocked(tokenizeBook).mockClear();
    +
    +    const page = parsePage(await getSegments('src', 'GEN', 0, 2));
    +
    +    expect(page.segments.map((seg) => seg.id)).toEqual(['GEN 1:1', 'GEN 1:3']);
    +    expect(tokenizeBook).not.toHaveBeenCalled();
    +    expect(subscriptionsTo(project, 'GEN')).toHaveLength(1);
    +  });
    +
    +  it('lets go of a book read whole once another replaces it, but not the outlined book', async () => {
    +    const project = serveProject((bookId) => usj(bookId));
    +    await getBookOutline('src', 'GEN');
    +    await getTokenizedBook('src', 'EXO');
    +
    +    await getTokenizedBook('src', 'LEV');
    +    await settle();
    +
    +    expect(project.subscriptions.map((s) => [s.bookId, s.unsubscribe.mock.calls.length])).toEqual([
    +      ['GEN', 0],
    +      ['EXO', 1],
    +      ['LEV', 0],
    +    ]);
    +  });
    +
       it('reports an edit to the outlined book with its project and book', async () => {
         const project = serveProject((bookId) => usj(bookId));
         const listener = listen();
    @@ -574,35 +642,31 @@ describe('onOpenBookTextChanged', () => {
         listeners.forEach((listener) => expect(listener).toHaveBeenCalledTimes(1));
       });
     
    -  it('reads the edited text when the book is next requested', async () => {
    -    let version = 'v1';
    -    const project = serveProject((bookId) => usj(bookId, version));
    +  it('watches the book again once an edit to it is reported', async () => {
    +    const project = serveProject((bookId) => usj(bookId));
         await getBookOutline('src', 'GEN');
    -    version = 'v2';
    +    const [first] = project.subscriptions;
     
    -    project.subscriptions[0].deliver(usj('GEN', version));
    +    first.deliver(usj('GEN', 'v2'));
         await settle();
     
    -    expect(parseBook(await getTokenizedBook('src', 'GEN')).textVersion).toBe('v2@en');
    +    expect(subscriptionsTo(project, 'GEN')).toHaveLength(2);
    +    expect(first.unsubscribe).toHaveBeenCalled();
       });
     
    -  it('reads the edited text when the held book heard of the edit first', async () => {
    +  it('reads the edited text when the book is next requested', async () => {
         let version = 'v1';
         const project = serveProject((bookId) => usj(bookId, version));
    -    const listener = listen();
         await getBookOutline('src', 'GEN');
         version = 'v2';
     
    -    project.subscriptions[1].deliver(usj('GEN', version));
         project.subscriptions[0].deliver(usj('GEN', version));
         await settle();
     
    -    expect(listener).toHaveBeenCalledTimes(1);
    -    expect(listener).toHaveBeenCalledWith('src', 'GEN');
         expect(parseBook(await getTokenizedBook('src', 'GEN')).textVersion).toBe('v2@en');
       });
     
    -  it('still reports an edit after other books have been read in between', async () => {
    +  it('reads the edited text after other books were read in between', async () => {
         let version = 'v1';
         const project = serveProject((bookId) => usj(bookId, version));
         const listener = listen();
    @@ -614,92 +678,56 @@ describe('onOpenBookTextChanged', () => {
         project.subscriptions[0].deliver(usj('GEN', version));
         await settle();
     
    -    expect(listener).toHaveBeenCalledTimes(1);
         expect(listener).toHaveBeenCalledWith('src', 'GEN');
         expect(parseBook(await getTokenizedBook('src', 'GEN')).textVersion).toBe('v2@en');
    +    expect(parseOutline(await getBookOutline('src', 'GEN')).textVersion).toBe('v2@en');
       });
     
    -  it('keeps the book held for another when the outlined book is edited', async () => {
    +  it('reports later edits through the book watched again', async () => {
         const project = serveProject((bookId) => usj(bookId));
    +    const listener = listen();
         await getBookOutline('src', 'GEN');
    -    await getTokenizedBook('src', 'EXO');
    -    const held = project.subscriptions.at(-1);
    -
         project.subscriptions[0].deliver(usj('GEN', 'v2'));
         await settle();
     
    -    expect(held?.unsubscribe).not.toHaveBeenCalled();
    -  });
    +    subscriptionsTo(project, 'GEN')[1].deliver(usj('GEN', 'v3'));
     
    -  it('watches the book again on the next outline once its watch fails to read it', async () => {
    -    const project = serveProject();
    -    const listener = listen();
    -    const outline = getBookOutline('src', 'GEN');
    -    await settle();
    -    const [failed, held] = project.subscriptions;
    -    failed.deliver(platformError('busy'));
    -    held.deliver(usj('GEN'));
    -    await outline;
    -    await settle();
    -
    -    await getBookOutline('src', 'GEN');
    -    await settle();
    -    const watch = project.subscriptions[2];
    -    watch.deliver(usj('GEN'));
    -    watch.deliver(usj('GEN', 'v2'));
    -
    -    expect(failed.unsubscribe).toHaveBeenCalled();
    -    expect(listener).toHaveBeenCalledTimes(1);
    -    expect(listener).toHaveBeenCalledWith('src', 'GEN');
    +    expect(listener).toHaveBeenCalledTimes(2);
       });
     
    -  it('keeps the newer watch when a replaced watch fails to read its book', async () => {
    -    const project = serveProject();
    -    const listener = listen();
    -    getBookOutline('src', 'GEN').catch(() => undefined);
    -    await settle();
    -    const replaced = project.subscriptions[0];
    -    getBookOutline('src', 'EXO').catch(() => undefined);
    -    await settle();
    -    const watch = project.subscriptions.find((s) => s.bookId === 'EXO');
    +  it('keeps the book held for another when the outlined book is edited', async () => {
    +    const project = serveProject((bookId) => usj(bookId));
    +    await getBookOutline('src', 'GEN');
    +    await getTokenizedBook('src', 'EXO');
    +    const [exo] = subscriptionsTo(project, 'EXO');
     
    -    replaced.deliver(platformError('busy'));
    +    project.subscriptions[0].deliver(usj('GEN', 'v2'));
         await settle();
    -    watch?.deliver(usj('EXO'));
    -    watch?.deliver(usj('EXO', 'v2'));
     
    -    expect(listener).toHaveBeenCalledWith('src', 'EXO');
    +    expect(exo.unsubscribe).not.toHaveBeenCalled();
       });
     
    -  it('reports nothing for a book only read whole', async () => {
    +  it('reports nothing for an edit to a book only read whole', async () => {
         const project = serveProject((bookId) => usj(bookId));
         const listener = listen();
         await getTokenizedBook('src', 'GEN');
         await getSegments('src', 'GEN', 0, 1);
     
    -    project.subscriptions.forEach((subscription) => subscription.deliver(usj('GEN', 'v2')));
    +    project.subscriptions[0].deliver(usj('GEN', 'v2'));
     
         expect(listener).not.toHaveBeenCalled();
       });
     
    -  it('watches an outlined book once however often it is outlined', async () => {
    +  it('stops watching the previous outlined book once another is outlined', async () => {
         const project = serveProject((bookId) => usj(bookId));
    -
    -    await getBookOutline('src', 'GEN');
         await getBookOutline('src', 'GEN');
    +    await getTokenizedBook('src', 'EXO');
    +    const [gen] = project.subscriptions;
     
    -    expect(project.subscriptions).toHaveLength(2);
    -  });
    -
    -  it('stops watching the previous book once another is outlined', async () => {
    -    const project = serveProject((bookId) => usj(bookId));
    -    await getBookOutline('src', 'GEN');
    -    const [first] = project.subscriptions;
    -
    -    await getBookOutline('src', 'EXO');
    +    await getBookOutline('src', 'LEV');
         await settle();
     
    -    expect(first.unsubscribe).toHaveBeenCalled();
    +    expect(gen.unsubscribe).toHaveBeenCalled();
       });
     
       it('reports edits to the newly outlined book', async () => {
    @@ -707,36 +735,37 @@ describe('onOpenBookTextChanged', () => {
         const listener = listen();
         await getBookOutline('src', 'GEN');
         await getBookOutline('src', 'EXO');
    -    const exo = project.subscriptions.find((s) => s.bookId === 'EXO');
     
    -    exo?.deliver(usj('EXO', 'v2'));
    +    subscriptionsTo(project, 'EXO')[0].deliver(usj('EXO', 'v2'));
     
         expect(listener).toHaveBeenCalledTimes(1);
         expect(listener).toHaveBeenCalledWith('src', 'EXO');
       });
     
    -  it("stops watching the project's outlined book once it closes", async () => {
    +  it("stops watching the project's books once it closes", async () => {
         const project = serveProject((bookId) => usj(bookId));
         await getBookOutline('src', 'GEN');
    +    await getTokenizedBook('src', 'EXO');
     
         await closeProject('src');
     
         expect(project.subscriptions.every((s) => s.unsubscribe.mock.calls.length === 1)).toBe(true);
       });
     
    -  it('closes a project that has an outlined book but nothing held', async () => {
    +  it('stops watching an outlined book that is also the latest once the project closes', async () => {
         const project = serveProject((bookId) => usj(bookId));
         await getBookOutline('src', 'GEN');
    -    project.subscriptions[1].deliver(usj('GEN', 'v2'));
    -    await settle();
     
    -    await expect(closeProject('src')).resolves.toBeUndefined();
    +    await closeProject('src');
    +
    +    expect(project.subscriptions[0].unsubscribe).toHaveBeenCalledTimes(1);
       });
     
       it('stops watching every outlined book once all projects close', async () => {
         const project = serveProject((bookId) => usj(bookId));
         await getBookOutline('src', 'GEN');
    -    await getBookOutline('other', 'EXO');
    +    await getTokenizedBook('src', 'EXO');
    +    await getBookOutline('other', 'LEV');
     
         await closeAllProjects();
     
    diff --git a/src/services/bookService.ts b/src/services/bookService.ts
    index f053047e..1e482ea9 100644
    --- a/src/services/bookService.ts
    +++ b/src/services/bookService.ts
    @@ -30,16 +30,16 @@ interface HeldBook {
     }
     
     /** Each source project's most recently requested book. */
    -const heldBooks = new Map<string, HeldBook>();
    -
    -/** Tells a request whether its project closed while it was under way. */
    -const closeCounts = new Map<string, number>();
    +const latestBooks = new Map<string, HeldBook>();
     
     /**
    - * Each source project's open book, the one whose outline was last asked for, watched apart from
    - * {@link heldBooks} since reading other books replaces the held one.
    + * Each source project's open book, the one whose outline was last asked for. It stays kept while
    + * other books are read, and its edits are reported to {@link onOpenBookTextChanged}'s listeners.
      */
    -const openBooks = new Map<string, { bookId: string; watch: BookTextWatch }>();
    +const openBooks = new Map<string, HeldBook>();
    +
    +/** Tells a request whether its project closed while it was under way. */
    +const closeCounts = new Map<string, number>();
     
     /** Receives the source project and book of each edit to an open book's text. */
     type OpenBookTextListener = (sourceProjectId: string, bookId: string) => void;
    @@ -99,14 +99,30 @@ function watchBookText(
       };
     }
     
    -/** Stops watching `watch`'s book, and forgets it unless another book has replaced it. */
    +/** Stops watching `watch`'s book, and forgets it wherever the source project keeps it. */
     function releaseBook(sourceProjectId: string, watch: BookTextWatch): Promise<void> {
    -  if (heldBooks.get(sourceProjectId)?.watch === watch) heldBooks.delete(sourceProjectId);
    +  if (latestBooks.get(sourceProjectId)?.watch === watch) latestBooks.delete(sourceProjectId);
    +  if (openBooks.get(sourceProjectId)?.watch === watch) openBooks.delete(sourceProjectId);
       return watch.stop();
     }
     
    +/** Stops watching `held`'s book once the source project keeps it neither as latest nor as open. */
    +function dropUnlessKept(sourceProjectId: string, held: HeldBook | undefined): void {
    +  if (!held) return;
    +  if (latestBooks.get(sourceProjectId) === held || openBooks.get(sourceProjectId) === held) return;
    +  held.watch.stop();
    +}
    +
     function holdBook(sourceProjectId: string, bookId: string, writingSystem: string): HeldBook {
    -  const watch = watchBookText(sourceProjectId, bookId, () => releaseBook(sourceProjectId, watch));
    +  const watch = watchBookText(sourceProjectId, bookId, () => {
    +    const isOpen = openBooks.get(sourceProjectId)?.watch === watch;
    +    releaseBook(sourceProjectId, watch);
    +    if (!isOpen) return;
    +    // Read again at once, so the book stays watched and a refetch the listeners start reads the
    +    // edit.
    +    openBooks.set(sourceProjectId, holdBook(sourceProjectId, bookId, writingSystem));
    +    openBookTextListeners.forEach((listener) => listener(sourceProjectId, bookId));
    +  });
       const book = perfTrack('getTokenizedBook:usj', watch.text).then(
         (usj) =>
           usj &&
    @@ -121,10 +137,15 @@ function holdBook(sourceProjectId: string, bookId: string, writingSystem: string
     }
     
     /**
    - * Finds the source project's book tokenized under the project's language tag. Each project's latest
    - * book is kept, and served again until its text changes or {@link closeProject} lets it go.
    + * Finds the source project's book tokenized under the project's language tag, and keeps it as the
    + * project's latest book, and as its open book too when `isOpen`. A kept book is served again until
    + * its text changes, another book takes its place, or {@link closeProject} lets it go.
      */
    -async function heldBook(sourceProjectId: string, bookId: string): Promise<HeldBook> {
    +async function heldBook(
    +  sourceProjectId: string,
    +  bookId: string,
    +  isOpen = false,
    +): Promise<HeldBook> {
       const closeCount = closeCounts.get(sourceProjectId);
       const writingSystem = await getWritingSystem(sourceProjectId);
       if (closeCounts.get(sourceProjectId) !== closeCount) {
    @@ -133,12 +154,17 @@ async function heldBook(sourceProjectId: string, bookId: string): Promise<HeldBo
         releaseBook(sourceProjectId, book.watch);
         return book;
       }
    -  const held = heldBooks.get(sourceProjectId);
    -  if (held?.bookId === bookId && held.writingSystem === writingSystem) return held;
    -  if (held) releaseBook(sourceProjectId, held.watch);
    -  const book = holdBook(sourceProjectId, bookId, writingSystem);
    -  heldBooks.set(sourceProjectId, book);
    -  return book;
    +  const latest = latestBooks.get(sourceProjectId);
    +  const open = openBooks.get(sourceProjectId);
    +  const held =
    +    [latest, open].find(
    +      (kept) => kept?.bookId === bookId && kept.writingSystem === writingSystem,
    +    ) ?? holdBook(sourceProjectId, bookId, writingSystem);
    +  latestBooks.set(sourceProjectId, held);
    +  if (isOpen) openBooks.set(sourceProjectId, held);
    +  dropUnlessKept(sourceProjectId, latest);
    +  dropUnlessKept(sourceProjectId, open);
    +  return held;
     }
     
     /**
    @@ -205,34 +231,6 @@ export function onOpenBookTextChanged(listener: OpenBookTextListener): () => boo
       return () => openBookTextListeners.delete(listener);
     }
     
    -function watchOpenBook(sourceProjectId: string, bookId: string): void {
    -  const open = openBooks.get(sourceProjectId);
    -  if (open?.bookId === bookId) return;
    -  open?.watch.stop();
    -  const watch = watchBookText(sourceProjectId, bookId, () => {
    -    // Either watch may hear of the edit first, so the held text goes before any listener refetches.
    -    const held = heldBooks.get(sourceProjectId);
    -    if (held?.bookId === bookId) releaseBook(sourceProjectId, held.watch);
    -    openBookTextListeners.forEach((listener) => listener(sourceProjectId, bookId));
    -  });
    -  // A read failure reaches whoever requests the text. Forgetting the watch lets the next outline
    -  // watch again; stopping it ends a subscription an error delivery leaves open.
    -  watch.text.catch(() => {
    -    if (openBooks.get(sourceProjectId)?.watch !== watch) return;
    -    openBooks.delete(sourceProjectId);
    -    watch.stop();
    -  });
    -  openBooks.set(sourceProjectId, { bookId, watch });
    -}
    -
    -/** Stops watching the source project's open book. */
    -function closeOpenBook(sourceProjectId: string): Promise<void> {
    -  const open = openBooks.get(sourceProjectId);
    -  if (!open) return Promise.resolve();
    -  openBooks.delete(sourceProjectId);
    -  return open.watch.stop();
    -}
    -
     /**
      * Outlines a book of the source project: its segments, segmented by the boundaries
      * `segmentationJson` names, each without its text. The book becomes the project's open book, whose
    @@ -247,8 +245,7 @@ export async function getBookOutline(
       bookId: string,
       segmentationJson?: string,
     ): Promise<string | undefined> {
    -  watchOpenBook(sourceProjectId, bookId);
    -  const held = await heldBook(sourceProjectId, bookId);
    +  const held = await heldBook(sourceProjectId, bookId, true);
       const book = await held.book;
       if (!book) return undefined;
       const segments = segmentsOf(held, book, segmentationJson, 'getBookOutline');
    @@ -273,8 +270,10 @@ export async function getSegments(
       end: number,
       segmentationJson?: string,
     ): Promise<string | undefined> {
    -  const kept = heldBooks.get(sourceProjectId);
    -  const held = kept?.bookId === bookId ? kept : await heldBook(sourceProjectId, bookId);
    +  const held =
    +    [openBooks.get(sourceProjectId), latestBooks.get(sourceProjectId)].find(
    +      (kept) => kept?.bookId === bookId,
    +    ) ?? (await heldBook(sourceProjectId, bookId));
       const book = await held.book;
       if (!book) return undefined;
       const segments = segmentsOf(held, book, segmentationJson, 'getSegments');
    @@ -287,23 +286,20 @@ export async function getSegments(
       return perfTime('getSegments:serialize', () => JSON.stringify(page));
     }
     
    -/**
    - * Lets go of the book kept for the source project, and of any a request under way would keep, and
    - * stops watching its open book.
    - */
    +/** Lets go of the books kept for the source project. */
    +async function releaseProject(sourceProjectId: string): Promise<void> {
    +  const kept = new Set([latestBooks.get(sourceProjectId), openBooks.get(sourceProjectId)]);
    +  await Promise.all([...kept].map((held) => held && releaseBook(sourceProjectId, held.watch)));
    +}
    +
    +/** Lets go of the books kept for the source project, and of any a request under way would keep. */
     export async function closeProject(sourceProjectId: string): Promise<void> {
       closeCounts.set(sourceProjectId, (closeCounts.get(sourceProjectId) ?? 0) + 1);
    -  const held = heldBooks.get(sourceProjectId);
    -  await Promise.all([
    -    held && releaseBook(sourceProjectId, held.watch),
    -    closeOpenBook(sourceProjectId),
    -  ]);
    +  await releaseProject(sourceProjectId);
     }
     
    -/** Lets go of every book kept, and stops watching every open book. */
    +/** Lets go of every book kept. */
     export async function closeAllProjects(): Promise<void> {
    -  await Promise.all([
    -    ...[...heldBooks].map(([sourceProjectId, { watch }]) => releaseBook(sourceProjectId, watch)),
    -    ...[...openBooks.keys()].map(closeOpenBook),
    -  ]);
    +  const sourceProjectIds = new Set([...latestBooks.keys(), ...openBooks.keys()]);
    +  await Promise.all([...sourceProjectIds].map(releaseProject));
     }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions