Skip to content

Refactor sdpub access and shorten queue write locks#67

Merged
Moskize91 merged 6 commits into
mainfrom
refactor/sdpub-read-write-boundaries
Jun 18, 2026
Merged

Refactor sdpub access and shorten queue write locks#67
Moskize91 merged 6 commits into
mainfrom
refactor/sdpub-read-write-boundaries

Conversation

@Moskize91

Copy link
Copy Markdown
Contributor

Summary

  • split high-level sdpub access into read/readDocument/write paths and move read-only archive commands off write workspaces
  • make chapter/archive read helpers accept ReadonlyDocument and prevent read-only TOC queries from writing repairs
  • stage queue graph/summary generation in job workspaces so archive write access is held only during final commits
  • add coverage for visible queue concurrency through the queue list data source

Validation

  • pnpm verify
  • pnpm test:run
  • installed local CLI with pnpm run cli:install-local
  • verified spinedigest queue list showed 10 running jobs with request.concurrent=10

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 719cf7a4-9050-40a0-88eb-90a1fe23dd56

📥 Commits

Reviewing files that changed from the base of the PR and between 3a599ac and 90649e2.

📒 Files selected for processing (2)
  • package.json
  • test/cli/queue.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/cli/queue.test.ts

Summary by CodeRabbit

  • Refactor
    • Updated archive and chapter operations to clearly separate read-only actions from mutating actions, while keeping output formatting and CLI commands consistent.
    • Improved the build/worker pipeline to use staged artifact reads/writes, with better worker-slot/job claiming behavior.
    • Chapter tree generation now preserves structural TOC wrapper nodes when serial IDs are absent.
  • Bug Fixes
    • Failed archive writes are now kept safely without being flushed back.
  • Chores
    • Version bumped to 0.2.1.

Walkthrough

The PR renames SdpubCoordinator.openSession/openEditableSession to withReadWorkspace/withWriteWorkspace, then replaces SpineDigestFile.openSession/openEditableSession with three explicit methods: read, readDocument, and write. All read-only facade functions in archive-view.ts and chapter.ts are narrowed from Document to ReadonlyDocument. A new chapter-build.ts module introduces artifact-based graph and summary workflows (buildChapterGraphArtifact, commitChapterGraphArtifact, snapshotChapterSummaryInput, buildChapterSummaryArtifactFromSnapshot, commitChapterSummaryArtifact) that operate on filesystem-backed DirectoryDocument snapshots. The build queue worker is refactored to per-slot concurrency. All CLIs are rewired to the new APIs, and tests are updated throughout.

Possibly Related PRs

  • oomol-lab/spinedigest#7: Introduced the workspace-to-document layer that SpineDigestFile.readDocument/write now directly routes through via SdpubCoordinator.
  • oomol-lab/spinedigest#54: Introduced openEditableSession and CLI chapter/stage editing flows that this PR replaces with the new write API and artifact-based build pipeline.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The pull request title does not follow the required format of (): . It uses an informal narrative style instead of the conventional commit format. Update the title to follow the format: e.g., 'refactor(sdpub): split access paths and optimize queue locks' or similar conventional commit format.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is comprehensive and clearly related to the changeset, covering the refactoring of sdpub access patterns, ReadonlyDocument updates, queue optimization, and test coverage additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch refactor/sdpub-read-write-boundaries

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/cli/archive.ts (1)

308-313: 💤 Low value

Return type discards operation result.

The helper declares Promise<void> but wraps SpineDigestFile.readDocument<T>() which returns Promise<T>. While all current callers return void, this signature prevents future callers from returning values without modifying the helper.

♻️ Suggested fix to propagate return type
-async function readArchiveDocument<T>(
-  path: string,
-  operation: (document: ReadonlyDocument) => Promise<T> | T,
-): Promise<void> {
-  await new SpineDigestFile(path).readDocument(operation);
+async function readArchiveDocument<T>(
+  path: string,
+  operation: (document: ReadonlyDocument) => Promise<T> | T,
+): Promise<T> {
+  return await new SpineDigestFile(path).readDocument(operation);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/archive.ts` around lines 308 - 313, The readArchiveDocument function
declares a return type of Promise<void> but wraps SpineDigestFile.readDocument
which returns Promise<T>, causing the operation result to be discarded. Change
the return type annotation from Promise<void> to Promise<T> and add a return
statement to propagate the result from the SpineDigestFile.readDocument call so
that future callers can retrieve the operation result without modifying the
helper function.
src/facade/build-queue.ts (2)

504-506: 💤 Low value

Redundant heartbeat and recovery calls from every slot.

Each of the N concurrent slots calls heartbeatBuildWorker and recoverStaleBuildJobs on every iteration. With concurrency: 10 and a 500ms delay between iterations, this results in up to ~20 heartbeat and recovery operations per second instead of ~2.

Consider tracking the last heartbeat/recovery time at the worker level and only executing these when a threshold has passed, or designate a single slot for housekeeping duties.

💡 Sketch for throttled housekeeping
// At worker level (outside runSlot)
let lastHousekeepingAt = 0;
const HOUSEKEEPING_INTERVAL_MS = 1000;

// Inside runSlot, replace direct calls with:
const now = Date.now();
if (now - lastHousekeepingAt >= HOUSEKEEPING_INTERVAL_MS) {
  lastHousekeepingAt = now;
  await heartbeatBuildWorker(ownerId, state);
  await recoverStaleBuildJobs(state);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/facade/build-queue.ts` around lines 504 - 506, The heartbeatBuildWorker
and recoverStaleBuildJobs functions are being called on every iteration of the
while(!stopping) loop in each concurrent slot, causing excessive overhead.
Implement throttling by maintaining a lastHousekeepingAt timestamp at the worker
level (outside the runSlot loop) and wrapping the calls to heartbeatBuildWorker
and recoverStaleBuildJobs in a condition that checks if the current time minus
lastHousekeepingAt exceeds a threshold (e.g., 1000ms), only executing these
housekeeping operations when sufficient time has passed and updating
lastHousekeepingAt after execution.

781-791: 💤 Low value

Consider verifying claim ownership after UPDATE for defensive robustness.

The transaction relies on Database-level serialization (#runSerialized) to ensure the SELECT and UPDATE execute atomically across concurrent slots. This is currently safe because acquireBuildWorkerLease ensures only one worker process, and all slots share the same Database instance.

However, if the architecture later supports multiple worker processes or database connections, the UPDATE could match 0 rows (job already claimed), yet requireBuildJobById would still return the job—now owned by another worker.

A defensive improvement would verify ownership in the final SELECT:

💡 Defensive ownership verification
-    return await requireBuildJobById(state, job.jobId);
+    const claimed = await state.queryOne(
+      "SELECT * FROM build_jobs WHERE job_id = ? AND owner_id = ?",
+      [job.jobId, ownerId],
+      mapBuildJob,
+    );
+
+    return claimed;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/facade/build-queue.ts` around lines 781 - 791, The UPDATE statement in
the build job claiming logic may return 0 affected rows if another worker
already claimed the job, but the subsequent call to requireBuildJobById would
still return the job object with a different owner. Add defensive verification
by checking the number of rows affected by the UPDATE query (the state.run call
that sets state to 'running' and updates owner_id). If the UPDATE affects 0
rows, throw an error to indicate the job was already claimed, rather than
returning a job owned by another worker. This ensures the code remains safe if
the architecture evolves to support multiple worker processes or database
connections.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/cli/archive.ts`:
- Around line 308-313: The readArchiveDocument function declares a return type
of Promise<void> but wraps SpineDigestFile.readDocument which returns
Promise<T>, causing the operation result to be discarded. Change the return type
annotation from Promise<void> to Promise<T> and add a return statement to
propagate the result from the SpineDigestFile.readDocument call so that future
callers can retrieve the operation result without modifying the helper function.

In `@src/facade/build-queue.ts`:
- Around line 504-506: The heartbeatBuildWorker and recoverStaleBuildJobs
functions are being called on every iteration of the while(!stopping) loop in
each concurrent slot, causing excessive overhead. Implement throttling by
maintaining a lastHousekeepingAt timestamp at the worker level (outside the
runSlot loop) and wrapping the calls to heartbeatBuildWorker and
recoverStaleBuildJobs in a condition that checks if the current time minus
lastHousekeepingAt exceeds a threshold (e.g., 1000ms), only executing these
housekeeping operations when sufficient time has passed and updating
lastHousekeepingAt after execution.
- Around line 781-791: The UPDATE statement in the build job claiming logic may
return 0 affected rows if another worker already claimed the job, but the
subsequent call to requireBuildJobById would still return the job object with a
different owner. Add defensive verification by checking the number of rows
affected by the UPDATE query (the state.run call that sets state to 'running'
and updates owner_id). If the UPDATE affects 0 rows, throw an error to indicate
the job was already claimed, rather than returning a job owned by another
worker. This ensures the code remains safe if the architecture evolves to
support multiple worker processes or database connections.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfb50173-d0c0-4c9e-a822-b0b881a372bf

📥 Commits

Reviewing files that changed from the base of the PR and between 4a01b5f and 3a599ac.

📒 Files selected for processing (21)
  • src/cli/archive-chapter.ts
  • src/cli/archive-maintenance.ts
  • src/cli/archive.ts
  • src/cli/queue.ts
  • src/facade/app.ts
  • src/facade/archive-view.ts
  • src/facade/build-queue.ts
  • src/facade/chapter-build.ts
  • src/facade/chapter.ts
  • src/facade/index.ts
  • src/facade/sdpub-coordinator.ts
  • src/facade/spine-digest-file.ts
  • src/serial.ts
  • test/cli/archive-chapter.test.ts
  • test/cli/archive-maintenance.test.ts
  • test/cli/archive.test.ts
  • test/cli/queue.test.ts
  • test/facade/build-queue.test.ts
  • test/facade/chapter-graph.test.ts
  • test/facade/chapter.test.ts
  • test/facade/spine-digest-file.test.ts

@Moskize91 Moskize91 merged commit 7f98339 into main Jun 18, 2026
3 checks passed
@Moskize91 Moskize91 deleted the refactor/sdpub-read-write-boundaries branch June 18, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant