Skip to content

feat(community): community.export() — embedded (#79)#100

Open
Rinse12 wants to merge 14 commits into
masterfrom
feat/community-export
Open

feat(community): community.export() — embedded (#79)#100
Rinse12 wants to merge 14 commits into
masterfrom
feat/community-export

Conversation

@Rinse12
Copy link
Copy Markdown
Collaborator

@Rinse12 Rinse12 commented May 22, 2026

Closes part of #79.

Summary

Implements the embedded half of community.export(): ask any started LocalCommunity to back itself up to a consistent sqlite snapshot. Records show up on community.exports and an exportschange event fires on every transition. Cancellation is via AbortSignal. pkc.destroy() aborts in-flight exports.

The RPC wire protocol + HTTP download endpoint + 24h orphan sweep are deferred to a follow-up PR (still tracked on #79) so this change stays reviewable.

Esteban's design feedback baked in

Per the comment on #79, the public CommunityExportRecord exposes only:

  • progress: number (0..1) — the only state indicator
  • size, sha256, url — present once progress === 1
  • error?: { code, message } — present on failure (cancellation surfaces as error.code === 'ERR_EXPORT_CANCELLED')
  • identification: exportId, name?, publicKey, includePrivateKey

Dropped from the earlier draft: explicit state enum, startedAt, pagesCopied, totalPages, completedAt. Consumers infer state from progress and error. The spec doc notes these can be added back compatibly if a finer-grained state surface is ever needed.

What's in this PR

  • Spec: src/rpc/EXPORT_COMMUNITY_SPEC.md
  • Types/schemas: ExportCommunityUserOptions, CommunityExportRecord (zod + TS) in src/community/{schema,types}.ts
  • Backup helper: backupCommunityDb() in src/runtime/node/util.ts — better-sqlite3 backup with AbortSignal-aware progress, atomic .partial → final rename, sha256, optional private-key scrub of the signer KeyV record
  • Embedded impl: src/runtime/node/community/local-community/export.ts — per-community serialization queue, KeyV persistence under a new STORAGE_KEYS.EXPORTS, prune-on-load for files that vanished out of band
  • LocalCommunity wiring: community.export(), community.exports, exportschange event
  • RemoteCommunity base: read-only stubs (export() rejects with ERR_COMMUNITY_NOT_LOCAL; exports returns [])
  • pkc.destroy(): aborts active exports and awaits their unwind before tearing down storage
  • Error codes: ERR_COMMUNITY_NOT_LOCAL, ERR_COMMUNITY_NOT_FOUND, ERR_PRIVATE_KEY_EXPORT_NOT_ALLOWED, ERR_EXPORT_PATH_NOT_SUPPORTED_OVER_RPC, ERR_EXPORT_CANCELLED, ERR_EXPORT_BACKUP_FAILED, ERR_DOWNLOAD_EXPORT_ID_NOT_FOUND

Test plan

  • npm run build — clean (node + browser + verify:browser-imports)
  • npx tsc --project test/tsconfig.json --noEmit — clean
  • New suite passes locally (10/10):
    node test/run-test-config.js --pkc-config local-kubo-rpc test/node/community/export.test.ts
  • Regression: test/node/community/create.community.test.ts still green
  • CI runs the new file under local-kubo-rpc and remote-pkc-rpc configs

The new test cases cover: happy path (sha256 + readable sqlite), includePrivateKey: false scrubs signer, includePrivateKey: true preserves it, exportPath override, exportschange emissions, AbortSignal mid-flight cancellation, pre-aborted signal rejecting synchronously without a record, concurrent exports of the same community both completing, ERR_COMMUNITY_NOT_LOCAL for read-only RemoteCommunity, and pkc.destroy() teardown.

Follow-up (still tracked on #79)

  • RPC methods (exportCommunity, exportsSubscribe, cancelExport) + schemas
  • HTTP GET /exports/<exportId> download endpoint on the same port as the WS RPC
  • Server-startup orphan sweep (>24h)
  • test/node-and-browser/rpc/exports.test.ts

Summary by CodeRabbit

  • New Features

    • Community export: start/track exports with progress, persisted export history, HTTP download for finished exports, cancel via AbortSignal, optional private-key inclusion, custom file paths (embedded only), and concurrent exports. RPC clients surface export/start/subscribe/cancel APIs and enforce RPC-specific limits.
  • Bug Fixes

    • Shutdown now aborts and awaits in‑flight exports to reach terminal states; server cleans up expired export files.
  • Tests

    • End-to-end coverage for success, cancellation, concurrency, private-key scrubbing, RPC behaviors, and persistence.
  • Documentation

    • Added comprehensive community export specification.

Review Change Stack

Implements the embedded slice of issue #79: `community.export()` produces a
consistent sqlite snapshot of a LocalCommunity at
`<pkcDataPath>/exports/<exportId>.sqlite`, exposed through `community.exports`
and the `exportschange` event.

Per Esteban's feedback on the design thread, the public CommunityExportRecord
exposes only `progress` (0..1), `error`, and the terminal `size`/`sha256`/`url`
fields. Consumers infer state: complete = progress===1, failed/cancelled =
error!==undefined. Internal pagesCopied/totalPages/startedAt/completedAt and
the explicit state enum are intentionally omitted; they can be added back
compatibly if a finer-grained state surface is ever needed.

What's in this PR:
- Spec doc: src/rpc/EXPORT_COMMUNITY_SPEC.md
- New zod schemas + types: ExportCommunityUserOptions, CommunityExportRecord
- backupCommunityDb helper: better-sqlite3 backup with AbortSignal-aware
  progress callback, atomic .partial→final rename, sha256 hashing, private-key
  scrub of the signer record when includePrivateKey is false
- LocalCommunity: _exports / _activeExports / _exportQueue state, export(),
  exports getter, _cancelExport(), persistence to a new STORAGE_KEYS.EXPORTS
  keyv entry with prune-on-load for files that vanished out of band
- RemoteCommunity base: read-only stubs (export() rejects with
  ERR_COMMUNITY_NOT_LOCAL; exports returns [])
- pkc.destroy() aborts in-flight exports and awaits their unwind before
  tearing down storage, matching the spec
- 10 vitest cases covering happy path, includePrivateKey on/off, exportPath
  override, exportschange emissions, AbortSignal mid-flight, pre-aborted
  signal, concurrent exports of the same community, ERR_COMMUNITY_NOT_LOCAL,
  and pkc.destroy() teardown

Follow-up (tracked on #79): the RPC wire protocol (exportCommunity,
exportsSubscribe, cancelExport), the HTTP /exports/<id> download endpoint,
24h orphan sweep, and tests under test/node-and-browser/rpc/.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

Implements community database export: adds Zod schemas, event/type and error/storage-key declarations, a base RemoteCommunity export API that rejects for non-local communities, Node SQLite backup utilities with cancellation and hashing, LocalCommunity embedded export flow with persistent progress and cancellation, RPC client/server export plumbing and HTTP download endpoint, lifecycle wiring, docs, and comprehensive tests.

Changes

Community Export Implementation

Layer / File(s) Summary
Export data contracts and public interface
src/community/schema.ts, src/community/types.ts, src/errors.ts, src/constants.ts
Zod schemas for export options/records, new ExportCommunityUserOptions and CommunityExportRecord types, exportschange event, new export-related error codes, and STORAGE_KEYS.EXPORTS storage key.
Base and RPC-local community interfaces
src/community/remote-community.ts, src/community/rpc-local-community.ts
RemoteCommunity exposes read-only exports and an export() method that rejects for non-local communities; RpcLocalCommunity implements RPC-backed export cache, subscription handling, abort wiring, and exported getter.
RPC client parsers and methods
src/clients/rpc-client/schema.ts, src/clients/rpc-client/rpc-schema-util.ts, src/clients/rpc-client/types.ts, src/clients/rpc-client/pkc-rpc-client.ts
RPC Zod schemas and loose parsers for export/cancel/exportschange plus PKCRpcClient methods exportCommunity, exportsSubscribe, cancelExport and rpcHttpOrigin derivation.
SQLite backup utility
src/runtime/node/util.ts
backupCommunityDb with better-sqlite3 readonly backup, progress callback, AbortSignal cancellation (BackupAbortError), optional private-key scrubbing, streaming SHA-256 and size, and atomic .partial commit semantics.
LocalCommunity embedded export runtime
src/runtime/node/community/local-community.ts, src/runtime/node/community/local-community/export.ts
LocalCommunity state (_exports, _activeExports, _exportQueue), export helpers: exportCommunityEmbedded, runExportTask, cancelExportEmbedded, deleteExportRecord, loadAndPruneExportsFromKeyv, cloning/snapshot helpers, KeyV persistence, progress throttling, and exportschange emissions.
Lifecycle and teardown integration
src/runtime/node/community/local-community/lifecycle.ts, src/runtime/node/community/local-community/db-state.ts, src/pkc/pkc.ts, src/pkc/pkc-with-rpc-client.ts
Loads persisted exports on community start/DB init; PKC.destroy() aborts and awaits in-flight exports; PKCWithRpcClient attaches exports subscription when creating local communities.
RPC server and HTTP download endpoint
src/rpc/src/index.ts, src/rpc/src/schema.ts
Registers exportCommunity/exportsSubscribe/cancelExport, per-address export LocalCommunity cache, HTTP GET /exports/<exportId> streaming with post-download cleanup, startup export-file sweep, and allowPrivateKeyExport server option.
Specification and tests
src/rpc/EXPORT_COMMUNITY_SPEC.md, test/node/community/export.test.ts, test/node-and-browser/pkc/test.configs.pkc.test.ts
Detailed RPC/embedded spec and comprehensive Vitest suites covering end-to-end export, integrity, private-key scrubbing, exportPath rules, events, cancellation, concurrency, persistence, RPC-only behaviors, and cleanup semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(community): community.export() — embedded' clearly and concisely describes the primary change: implementing the embedded half of a community export feature, allowing LocalCommunity instances to back themselves up to SQLite snapshots.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/community-export
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/community-export

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (1)
src/community/schema.ts (1)

363-388: ⚡ Quick win

Please confirm the new runtime field is reflected in reserved-field handling.

exports is now a runtime-only community field, but this reserved-field list wasn’t updated. Please confirm whether it should be added here to avoid accidental leakage into signed/public payload paths.

As per coding guidelines, "Every time you add a runtime-only field, ask whether it should also be added to the corresponding reserved-field list before you finish the change".

🤖 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/community/schema.ts` around lines 363 - 388, The reserved-field list
CommunityIpfsReservedFields is missing the new runtime-only field "exports", so
add "exports" to the first array passed to remeda.difference so it will be
excluded from signed/public payloads (keep using
remeda.keys.strict(CommunityIpfsSchema.shape) for the schema comparison); also
verify CommunityIpfsSchema does not include "exports" as a persisted field and
update the TODO typing comment if desired to reflect the enum of reserved props.
🤖 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.

Inline comments:
In `@src/community/schema.ts`:
- Around line 344-359: The schema CommunityExportRecordSchema currently leaves
size, sha256 and url optional even though the comment requires they be present
when progress === 1; update CommunityExportRecordSchema to enforce that
invariant by adding a refinement (z.refine or .superRefine) on the object that
checks the progress field and if progress === 1 then ensures size is defined
(and non-null), sha256 matches the hex regex, and url is defined; keep the
existing individual validators (size int/nonnegative, sha256 regex) but make the
overall validator reject objects where progress === 1 and any of size/sha256/url
is missing or invalid.

In `@src/rpc/EXPORT_COMMUNITY_SPEC.md`:
- Around line 109-140: Summary: The document reads as if RPC methods, the HTTP
download endpoint, and the 24h orphan sweep are implemented; they must be marked
as planned/not-yet-implemented. Edit EXPORT_COMMUNITY_SPEC.md to explicitly
prefix or annotate the `exportCommunity`, `exportsSubscribe`, and `cancelExport`
RPC descriptions (and any mentions of the HTTP `/exports/<exportId>` download
endpoint and the "24h orphan sweep") with a clear "Planned / Not yet
implemented" notice; clarify that AbortSignal routing, server behavior on
disconnect, and retention/orphan-sweep semantics are future work and not part of
the embedded-export scope; ensure `community.exports` and
`_findCommunityAddress` descriptions remain but are described as intended design
only. Also apply the same wording change to the later referenced section (the
content around lines noted in the review) so consumers and tests cannot assume
these behaviors are implemented.
- Around line 159-160: Steps 5–6 in EXPORT_COMMUNITY_SPEC.md are out of order:
you must rename the partial file to the final path before computing the SHA256
of the finalized file. Update the doc so that fs.rename(destPath + ".partial",
destPath) is listed before "Compute sha256 of the finalized file" (or explicitly
state that the hash is computed against destPath after the atomic rename),
referencing the filenames destPath and destPath + ".partial" and the fs.rename
operation to make the sequence unambiguous.

In `@src/runtime/node/community/local-community.ts`:
- Around line 388-390: The getter override get exports currently returns a
shallow-cloned array but still exposes live CommunityExportRecord objects from
this._exports; update the exports getter to return deep-cloned records (e.g.,
map over this._exports and clone each record) so callers cannot mutate internal
state, and reuse the same cloning helper used for preparing the exportschange
payloads to ensure consistent cloning logic across the class (reference the
override get exports, this._exports, CommunityExportRecord type, and the
exportschange payload helper).
- Around line 75-82: The import list in local-community.ts currently brings
InternalExportHandle as a value import alongside cancelExportEmbedded,
exportCommunityEmbedded, and loadAndPruneExportsFromKeyv; change it so
InternalExportHandle is imported using a type-only import (import type {
InternalExportHandle } from "./local-community/export.js") while keeping the
other symbols as regular imports to avoid runtime import hazards under TS ESM
emit settings.

In `@src/runtime/node/community/local-community/export.ts`:
- Around line 116-117: The export currently allows options.exportPath to resolve
to the live DB and later overwrites it; after computing destPath (from
options.exportPath or defaultExportPathFor(community, exportId)) add a
protection that resolves and compares destPath against
sourceDbPathFor(community) and throw/reject if they are the same (or if destPath
is a path inside the live DB directory, if applicable) to prevent clobbering the
active database; include a clear error message mentioning exportPath and that
the target equals the live DB, and perform this check before any rename or write
operations and before using includePrivateKey to continue.
- Around line 89-98: Replace the loose catch typing by using catch (err:
unknown) and narrow the error before using .message: when handling the abort
branch preserve the existing BackupAbortError check and opts.signal.aborted, but
derive the message with something like const msg = err instanceof Error ?
err.message : String(err) and use msg in the updateRecord call for the
"ERR_EXPORT_CANCELLED" case; in the non-abort branch log the raw err (or msg)
via log.error and set the updateRecord error message to the same narrowed msg
for "ERR_EXPORT_BACKUP_FAILED"; update references in this block around
updateRecord, BackupAbortError, exportId, community, and log.error accordingly.

In `@src/runtime/node/util.ts`:
- Around line 378-385: After the backup finishes this block ignores the provided
AbortSignal so cancellation during scrub/hash/rename still reports success;
update the code around includePrivateKey, scrubPrivateKeyFromBackup,
statAndHashFile, and fsPromises.rename to honor cancellation by checking
signal.aborted (or throwing an AbortError) before each await and immediately
after each awaited call, or pass the signal into
scrubPrivateKeyFromBackup/statAndHashFile if they accept it, and ensure any
in-progress rename is aborted/rolled back and the function rethrows an abort so
pkc.destroy() sees the cancellation.
- Around line 406-410: The code currently casts JSON.parse(row.value) to {
value: Record<string, any>; expires?: number | null } and then mutates
parsed.value.signer; replace that any-escape with a safe refinement: parse into
unknown, verify parsed is an object with a value property that is an object, and
that value.signer (if present) is an object with optional privateKey?: string
and ipfsKey?: Uint8Array (or nullable variants); only after these runtime type
checks mutate signer.privateKey = undefined and signer.ipfsKey = undefined.
Update the variable around JSON.parse(...) (the parsed variable inside
scrubPrivateKeyFromBackup) to use unknown + type guards before touching signer
so typing remains strict and safe.

In `@test/node/community/export.test.ts`:
- Line 77: Several tests use the helper itSkipIfRpc (and describeSkipIfRpc)
without the required explanatory comment; for each occurrence (e.g., the
itSkipIfRpc call shown, plus the ones at the other noted locations) add a short
single-line comment immediately above the call stating why RPC cannot run that
test (for example: "// RPC cannot access local filesystem to validate sha256" or
a similarly specific rationale). Make sure the comment is directly above the
itSkipIfRpc/describeSkipIfRpc invocation and references the reason
(network/FS/permission/DB constraints) so it satisfies the coding guideline.
- Line 115: The test currently casts signer as `any` when parsing row!.value;
replace that with a proper signer type by either importing the existing
Signer/CommunitySigner interface used by production fixtures or by defining a
local narrow interface (e.g., { id: string; name?: string; publicKey?: string }
matching the fixture shape) and change the parse to JSON.parse(row!.value) as {
value: { signer: Signer } } (or your local interface), then update subsequent
assertions to use the typed signer fields instead of relying on `any`.

---

Nitpick comments:
In `@src/community/schema.ts`:
- Around line 363-388: The reserved-field list CommunityIpfsReservedFields is
missing the new runtime-only field "exports", so add "exports" to the first
array passed to remeda.difference so it will be excluded from signed/public
payloads (keep using remeda.keys.strict(CommunityIpfsSchema.shape) for the
schema comparison); also verify CommunityIpfsSchema does not include "exports"
as a persisted field and update the TODO typing comment if desired to reflect
the enum of reserved props.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9904ea84-f4e2-4d29-bc2b-c40a7929b48d

📥 Commits

Reviewing files that changed from the base of the PR and between 540f275 and f3f504d.

📒 Files selected for processing (12)
  • src/community/remote-community.ts
  • src/community/schema.ts
  • src/community/types.ts
  • src/constants.ts
  • src/errors.ts
  • src/pkc/pkc.ts
  • src/rpc/EXPORT_COMMUNITY_SPEC.md
  • src/runtime/node/community/local-community.ts
  • src/runtime/node/community/local-community/export.ts
  • src/runtime/node/community/local-community/lifecycle.ts
  • src/runtime/node/util.ts
  • test/node/community/export.test.ts

Comment thread src/community/schema.ts Outdated
Comment thread src/rpc/EXPORT_COMMUNITY_SPEC.md Outdated
Comment thread src/rpc/EXPORT_COMMUNITY_SPEC.md Outdated
Comment thread src/runtime/node/community/local-community.ts
Comment thread src/runtime/node/community/local-community.ts
Comment thread src/runtime/node/community/local-community/export.ts
Comment thread src/runtime/node/util.ts
Comment on lines +378 to +385
if (!includePrivateKey) {
await scrubPrivateKeyFromBackup(partialPath);
}

const { size, sha256 } = await statAndHashFile(partialPath);

await fsPromises.rename(partialPath, destPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor cancellation during scrub/hash/rename.

Once the SQLite backup finishes, this code no longer checks signal.aborted, so a cancel that arrives during scrubbing or hashing still completes as a successful export. That breaks the AbortSignal contract and makes pkc.destroy() race with long post-processing on large DBs.

Suggested fix
         if (!includePrivateKey) {
             await scrubPrivateKeyFromBackup(partialPath);
+            if (signal?.aborted) throw new BackupAbortError();
         }
 
         const { size, sha256 } = await statAndHashFile(partialPath);
+        if (signal?.aborted) throw new BackupAbortError();
 
         await fsPromises.rename(partialPath, destPath);
+        if (signal?.aborted) throw new BackupAbortError();
 
         return { size, sha256 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!includePrivateKey) {
await scrubPrivateKeyFromBackup(partialPath);
}
const { size, sha256 } = await statAndHashFile(partialPath);
await fsPromises.rename(partialPath, destPath);
if (!includePrivateKey) {
await scrubPrivateKeyFromBackup(partialPath);
if (signal?.aborted) throw new BackupAbortError();
}
const { size, sha256 } = await statAndHashFile(partialPath);
if (signal?.aborted) throw new BackupAbortError();
await fsPromises.rename(partialPath, destPath);
if (signal?.aborted) throw new BackupAbortError();
🤖 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/runtime/node/util.ts` around lines 378 - 385, After the backup finishes
this block ignores the provided AbortSignal so cancellation during
scrub/hash/rename still reports success; update the code around
includePrivateKey, scrubPrivateKeyFromBackup, statAndHashFile, and
fsPromises.rename to honor cancellation by checking signal.aborted (or throwing
an AbortError) before each await and immediately after each awaited call, or
pass the signal into scrubPrivateKeyFromBackup/statAndHashFile if they accept
it, and ensure any in-progress rename is aborted/rolled back and the function
rethrows an abort so pkc.destroy() sees the cancellation.

Comment thread src/runtime/node/util.ts Outdated
Comment thread test/node/community/export.test.ts Outdated
await pkc.destroy();
});

itSkipIfRpc("happy path: file is on disk, sha256 matches, sqlite is readable", async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add explicit rationale comments above each itSkipIfRpc usage.

Each skipped RPC test needs a short “why RPC cannot run this” comment directly above it. Right now that requirement isn’t met.

As per coding guidelines, "When using describeSkipIfRpc, itSkipIfRpc, or otherwise skipping tests for RPC, you MUST add a comment above explaining why the test cannot run under RPC".

Also applies to: 106-106, 126-126, 142-142, 155-155, 172-172, 188-188, 196-196, 209-209, 230-230

🤖 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 `@test/node/community/export.test.ts` at line 77, Several tests use the helper
itSkipIfRpc (and describeSkipIfRpc) without the required explanatory comment;
for each occurrence (e.g., the itSkipIfRpc call shown, plus the ones at the
other noted locations) add a short single-line comment immediately above the
call stating why RPC cannot run that test (for example: "// RPC cannot access
local filesystem to validate sha256" or a similarly specific rationale). Make
sure the comment is directly above the itSkipIfRpc/describeSkipIfRpc invocation
and references the reason (network/FS/permission/DB constraints) so it satisfies
the coding guideline.

Comment thread test/node/community/export.test.ts Outdated
…dCommunityOrDb

A fresh LocalCommunity instance now sees prior exports in community.exports as
soon as pkc.createCommunity({ address }) returns, mirroring how internal state
is loaded. Two branches:

- Sibling started instance in the same PKC: snapshot _exports from it.
- DB load path: call _loadExportsFromKeyv() before destroying the connection.

Test covers both: a same-PKC sibling and a brand-new PKC instance pointed at
the same dataPath both see the previously-completed export's progress, url,
and sha256.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/runtime/node/community/local-community/db-state.ts (1)

151-154: ⚡ Quick win

Deep-clone export records for true snapshot semantics.

This currently snapshots only the array, not the record objects. If a record is mutated in place later, state can still leak across instances despite the non-propagation intent.

Suggested change
-        community._exports = [...startedCommunity._exports];
+        community._exports = startedCommunity._exports.map((record) => ({
+            ...record,
+            ...(record.error ? { error: { ...record.error } } : {})
+        }));
🤖 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/runtime/node/community/local-community/db-state.ts` around lines 151 -
154, The current snapshot copies only the array reference for community._exports
from startedCommunity._exports which still allows in-place mutations of export
record objects to leak across instances; replace the shallow copy with a deep
clone of each export record so community._exports =
startedCommunity._exports.map(record => deep-clone(record)); use a reliable
cloning approach available in the runtime (e.g., structuredClone if supported or
a JSON deep-clone fallback) and ensure this change is applied where
community._exports is assigned so each record object is a true snapshot.
🤖 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/runtime/node/community/local-community/db-state.ts`:
- Around line 151-154: The current snapshot copies only the array reference for
community._exports from startedCommunity._exports which still allows in-place
mutations of export record objects to leak across instances; replace the shallow
copy with a deep clone of each export record so community._exports =
startedCommunity._exports.map(record => deep-clone(record)); use a reliable
cloning approach available in the runtime (e.g., structuredClone if supported or
a JSON deep-clone fallback) and ensure this change is applied where
community._exports is assigned so each record object is a true snapshot.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 524720cb-d780-4d75-b8fa-b796eefc1395

📥 Commits

Reviewing files that changed from the base of the PR and between f3f504d and 8929681.

📒 Files selected for processing (2)
  • src/runtime/node/community/local-community/db-state.ts
  • test/node/community/export.test.ts

Rinse12 added 12 commits May 24, 2026 07:11
- Reject exportPath that resolves to the live community DB
  (new ERR_EXPORT_PATH_TARGETS_LIVE_DB) to prevent clobbering it
- Honor AbortSignal during post-backup scrub/hash/rename so
  pkc.destroy() cannot race with long post-processing
- Enforce size/sha256/url presence when progress === 1 via
  CommunityExportRecordSchema.superRefine
- Add 'exports' to CommunityIpfsReservedFields
- Deep-clone records returned via community.exports and the
  exportschange payload (shared helper cloneExportRecord)
- Replace Record<string, any> in scrubPrivateKeyFromBackup with
  a typed runtime guard; document scrub scope (community signer
  only — pseudonymityAliases.aliasPrivateKey intentionally kept)
- Narrow catch (err: unknown) in runExportTask; switch
  InternalExportHandle to import type
- Add per-call RPC-skip rationales and remove `as any` from the
  signer parse in export.test.ts
- Update EXPORT_COMMUNITY_SPEC.md: status table, mark deferred
  RPC/HTTP/sweep sections as Planned, reorder backup steps so
  rename precedes hash, document new error code and snapshot
  semantics
…lers

Registers the three wire methods on PKCWsServer with their zod schemas and
parsers. Server-side records carry file:// URLs; the wire format converts
them to a relative /exports/<exportId> URL so the RPC client can absolutize
against the WS server's HTTP origin.

The exportsSubscribe handler emits the current exports snapshot as the
initial notification and pipes the LocalCommunity exportschange event into
push notifications thereafter. Tearing down the subscription does not
cancel any in-flight exports — exports outlive client connections.

Adds an allowPrivateKeyExport flag (default true) so public-RPC operators
can refuse includePrivateKey: true requests.

Spec: src/rpc/EXPORT_COMMUNITY_SPEC.md
Issue: #79
PKCWsServer now always operates on an explicit http.Server (creating one
when only port is provided) so the WebSocket upgrade and the HTTP download
route can live on the same port. A successful download deletes the file
and prunes the record via the new LocalCommunity._deleteExport helper,
matching the spec's post-download cleanup semantics. Client aborts and
stream errors leave the record intact so the consumer can retry.

Adds the deleteExportRecord helper alongside the existing cancel/load
helpers in local-community/export.ts so the deletion path stays adjacent
to the other lifecycle transitions.

Issue: #79
PKCRpcClient gains exportCommunity / exportsSubscribe / cancelExport
methods plus an rpcHttpOrigin getter that derives http[s]://host from the
WS URL so the client can absolutize the relative `/exports/<exportId>`
URLs that the server emits.

RpcLocalCommunity now overrides export() and the exports getter. The
subscription is attached eagerly during pkc.createCommunity (before it
resolves), so consumers see prior exports — including ones still running
from an earlier client session — synchronously after createCommunity.
This is what lets a fresh client survive its predecessor's disconnect:
the server keeps the export going, and the new client's initial
exportschange notification reveals the in-flight record.

AbortSignal cancellation routes through cancelExport on the wire, with
the listener detached automatically once the record reaches a terminal
state.

Issue: #79
Deletes any *.sqlite files under <pkcDataPath>/exports/ whose mtime is
over 24h old. Records pointing to the deleted files are pruned lazily
the next time the community loads via loadAndPruneExportsFromKeyv.

Flips the spec status table for the RPC surfaces shipped in this PR.

Issue: #79
Wraps the export tests with getAvailablePKCConfigsToTestAgainst so the same
8 portable cases run under local-kubo-rpc and remote-pkc-rpc. The new
materializeExport helper hides the file:// vs http:// URL difference by
downloading HTTP-served exports to a local temp path before the sha256
and sqlite-open assertions.

Two tests stay embedded-only by design:
- exportPath option (RPC variant lives in the RPC-only suite below as a
  sync rejection check)
- pkc.destroy() cancels in-flight exports (RPC client disconnect must not
  cancel server exports, per spec — disconnect-resume is the RPC variant)

The RPC-only suite adds: exportPath sync rejection, HTTP 404 for unknown
exportId, post-download server-side cleanup (file deleted, record pruned,
second fetch returns 404), and the load-bearing disconnect-resume scenario
(client A starts an export, disconnects mid-flight, client B reconnects
and sees the record un-cancelled and finishing to progress=1).

Issue: #79
…he cache

Factors the 8 portable test bodies into a defineExportTests(getCtx) helper,
then calls it from three setups: embedded started, RPC started, RPC
non-started (new). The non-started suite is what keeps PKCWsServer's
_exportCommunityInstances cache honest — without the cache, exportCommunity
and the eager exportsSubscribe would call pkc.createCommunity separately
and land on different LocalCommunity objects (pkc.ts:749 builds a new one
each call), so the export's exportschange would never reach the
subscription's listener. Multiple polling assertions would then time out,
giving us a loud regression signal if the cache breaks.

Bootstrap is necessary because publishing a post requires the community
to be running; we start once via a throwaway client, publish, stop, then
disconnect so the LocalCommunity instance is gone from _startedCommunities.

Issue: #79
The old order (started → cache) broke the typical client flow:

1. pkc.createCommunity() — server makes LocalCommunity #1
2. _attachExportsSubscription — community not yet started, falls through to
   pkc.createCommunity → fresh #2, cached, listener on #2
3. community.start() — server's startCommunity → fresh #3 in _startedCommunities
4. community.export() — findStartedCommunity returns #3, export runs on #3,
   exportschange fires on #3 — but the listener is on #2

Symptom: the test server log shows "Export complete for community ... exportId ..."
while the client's waitForCompleteRecord times out at 30s.

Cache-first ordering anchors every export-related handler on the same
LocalCommunity for the address: once the first call resolves, subsequent
exportCommunity / exportsSubscribe / cancelExport calls all reuse it.

Issue: #79
Adds an RPC-only complement to the existing embedded RemoteCommunity
rejection test. Asks the daemon for a fresh signer's address that's
never been a community on the server, so pkc-with-rpc-client.ts returns
an RpcRemoteCommunity (which doesn't override export() and inherits the
base ERR_COMMUNITY_NOT_LOCAL throw). Sanity-checks the returned object
is not an RpcLocalCommunity so a future change to the create-community
flow doesn't quietly turn the test into a no-op.

Issue: #79
The waitForCompleteRecord / waitForRecord helpers polled community.exports
every 50ms, even though the export pipeline already emits exportschange for
every transition. Reuse resolveWhenConditionIsTrue on that event so each
helper attaches a single listener and resolves on the next emission, racing
against the timeoutMs budget. Call sites are updated to the object-parameter
form per AGENTS.md.
updateInstancePropsWithStartedCommunityOrDb used to snapshot _exports
from the in-memory started instance whenever findStartedCommunity or
findCommunityInRegistry(processStartedCommunities, ...) returned a hit.
That makes the new community's exports list depend on whichever
LocalCommunity instance happens to be tracked process-wide, which can
be stale or cross-PKC. Read from the keyv-backed DB instead — it is
the canonical persisted source and is current after every export
record mutation (persistExports is awaited inside updateRecord).

Open the DB read-write rather than read-only: a read-only connection
under SQLite WAL silently misses writes committed by a still-open
writer on the same file, which is exactly the case for the same-PKC
sibling path.
…resh-PKC reload after destroy

test.configs.pkc.test.ts: a new `it` documenting that PKC({
pkcRpcClientsOptions, dataPath }) should retain both options. Today
PKC's constructor drops dataPath when pkcRpcClientsOptions is set, so
the assertion currently fails and pins down the contract.

export.test.ts: a new `itSkipIfRpc` that asserts a fresh PKC on the
same dataPath sees the terminal-error record persisted by an in-flight
export that was cancelled by pkc.destroy(). Plus inline TODOs flagging
that we should also verify exportschange event fields explicitly
rather than only polling community.exports.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/runtime/node/community/local-community/export.ts (1)

121-125: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-check options.signal after wiring the listener.

Line 122 only covers signals that were already aborted before exportCommunityEmbedded() started. If the signal aborts between that check and this listener setup, the event is missed and the export keeps running.

🩹 Minimal fix
     if (options.signal) {
         const userSignal = options.signal;
         const onAbort = () => controller.abort(userSignal.reason);
-        userSignal.addEventListener("abort", onAbort, { once: true });
-        detachUserSignal = () => userSignal.removeEventListener("abort", onAbort);
+        if (userSignal.aborted) onAbort();
+        else {
+            userSignal.addEventListener("abort", onAbort, { once: true });
+            detachUserSignal = () => userSignal.removeEventListener("abort", onAbort);
+        }
     }

Also applies to: 149-154

🤖 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/runtime/node/community/local-community/export.ts` around lines 121 - 125,
The pre-listener abort check in exportCommunityEmbedded is racy: after you
attach the options.signal.addEventListener("abort", ...) you must immediately
re-check options.signal.aborted and throw the same error if set (use the same
logic: const reason = (options.signal as AbortSignal).reason; throw reason ??
new DOMException("The operation was aborted.", "AbortError");). Apply that exact
post-listener re-check in both places where you wired the listener (the block
inside exportCommunityEmbedded and the similar block around lines 149-154) so an
abort that happens between the initial check and listener registration is
handled; keep the existing listener cleanup logic intact.
src/community/schema.ts (1)

327-333: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten ExportCommunityUserOptionsSchema.signal to validate required AbortSignal APIs

ExportCommunityUserOptionsSchema.signal only checks for an aborted property, but both export flows use addEventListener("abort", ...) and removeEventListener(...) on options.signal, so invalid objects (e.g. { aborted: false }) can pass schema validation and fail at runtime.

🩹 Minimal tightening
 export const ExportCommunityUserOptionsSchema = z
     .object({
         includePrivateKey: z.boolean().optional(),
         exportPath: z.string().min(1).optional(),
-        signal: z.custom<AbortSignal>((v) => v === undefined || (typeof v === "object" && v !== null && "aborted" in v)).optional()
+        signal: z
+            .custom<AbortSignal>(
+                (v) =>
+                    v === undefined ||
+                    (typeof v === "object" &&
+                        v !== null &&
+                        "aborted" in v &&
+                        typeof (v as Partial<AbortSignal>).addEventListener === "function" &&
+                        typeof (v as Partial<AbortSignal>).removeEventListener === "function")
+            )
+            .optional()
     })
     .strict();
🤖 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/community/schema.ts` around lines 327 - 333, The schema
ExportCommunityUserOptionsSchema currently validates signal only by presence of
"aborted" which allows plain objects; update the z.custom<AbortSignal> validator
for the signal field to ensure it is either undefined or a real AbortSignal by
checking that the value is an object and has the "aborted" boolean property plus
callable addEventListener and removeEventListener methods (e.g. typeof
v.addEventListener === "function" && typeof v.removeEventListener ===
"function"), so callers using addEventListener("abort", ...) and
removeEventListener(...) won't fail at runtime.
🤖 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.

Inline comments:
In `@src/rpc/EXPORT_COMMUNITY_SPEC.md`:
- Around line 85-87: Several places in the EXPORT_COMMUNITY_SPEC (references:
the "RPC methods", "HTTP download", "retention behavior" sections and the
validation/error examples that mention includePrivateKey and exportPath)
disagree about whether features are "shipped" or "planned"; pick the canonical
status (e.g., mark these surfaces as shipped) and make all occurrences
consistent: update the status phrases and any parenthetical notes like "Planned
— depends on RPC server/client" to the chosen status, remove or revise
conflicting examples in the validation/error sections (including checks that
call out exportPath/exportPath-resolves-to-live-db and includePrivateKey
disallowed scenarios) so they align with the chosen model, and ensure any
downstream sections that describe behavior (RPC, HTTP download, retention, and
validation) use the same terminology and error expectations throughout.

In `@src/rpc/src/index.ts`:
- Around line 177-187: The request listener currently runs for every request and
causes _handleExportsHttpRequest to send 404s for non-/exports routes; restrict
handling to only export paths (or bail without writing response). Update the
request handler attached to this._httpServer so it first checks the request path
(e.g., req.url or parsed pathname) and only calls
this._handleExportsHttpRequest(req, res) when the path matches the export route
(e.g., startsWith "/exports/" or exact "/exports"); otherwise return immediately
(do not set status or end the response). You can alternatively add the same
early-return guard at the top of _handleExportsHttpRequest to avoid touching
other code paths.
- Around line 1662-1673: The handler that resolves /exports/<id> only searches
_exportLoadedCommunities() so exports persisted in KeyV but whose LocalCommunity
isn't started are invisible after restart; update _findCommunityOwningExport
(and the HTTP export handler logic that calls it) to also query the persistent
export records store (the KeyV-backed export persistence used for completed
CommunityExportRecord entries) when a record isn't found in
_exportLoadedCommunities(), and if found either hydrate/instantiate the
corresponding LocalCommunity (using the same initialization used by
_exportCommunityInstances) or return the persisted CommunityExportRecord
directly so the URL remains downloadable after a daemon restart.

In `@test/node-and-browser/pkc/test.configs.pkc.test.ts`:
- Line 5: The failing test imports the Node-only symbol getDefaultDataPath from
the runtime/node module, which makes the file depend on the Node build and
breaks browser runs; either move the test case that uses getDefaultDataPath out
of the shared test/node-and-browser suite into the node-only suite (e.g.,
test/node) or remove the static runtime/node import and instead gate a dynamic
Node-only import/require inside the test body (e.g., if (typeof process !==
"undefined" && process.versions?.node) { const { getDefaultDataPath } = await
import(".../runtime/node/util.js"); ... }) so the module is only loaded when
running in Node. Ensure references to getDefaultDataPath are updated
accordingly.

---

Outside diff comments:
In `@src/community/schema.ts`:
- Around line 327-333: The schema ExportCommunityUserOptionsSchema currently
validates signal only by presence of "aborted" which allows plain objects;
update the z.custom<AbortSignal> validator for the signal field to ensure it is
either undefined or a real AbortSignal by checking that the value is an object
and has the "aborted" boolean property plus callable addEventListener and
removeEventListener methods (e.g. typeof v.addEventListener === "function" &&
typeof v.removeEventListener === "function"), so callers using
addEventListener("abort", ...) and removeEventListener(...) won't fail at
runtime.

In `@src/runtime/node/community/local-community/export.ts`:
- Around line 121-125: The pre-listener abort check in exportCommunityEmbedded
is racy: after you attach the options.signal.addEventListener("abort", ...) you
must immediately re-check options.signal.aborted and throw the same error if set
(use the same logic: const reason = (options.signal as AbortSignal).reason;
throw reason ?? new DOMException("The operation was aborted.", "AbortError");).
Apply that exact post-listener re-check in both places where you wired the
listener (the block inside exportCommunityEmbedded and the similar block around
lines 149-154) so an abort that happens between the initial check and listener
registration is handled; keep the existing listener cleanup logic intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d48d558-fbea-4467-b6c0-1dd3e3213ced

📥 Commits

Reviewing files that changed from the base of the PR and between 8929681 and bd9bbc1.

📒 Files selected for processing (17)
  • src/clients/rpc-client/pkc-rpc-client.ts
  • src/clients/rpc-client/rpc-schema-util.ts
  • src/clients/rpc-client/schema.ts
  • src/clients/rpc-client/types.ts
  • src/community/rpc-local-community.ts
  • src/community/schema.ts
  • src/errors.ts
  • src/pkc/pkc-with-rpc-client.ts
  • src/rpc/EXPORT_COMMUNITY_SPEC.md
  • src/rpc/src/index.ts
  • src/rpc/src/schema.ts
  • src/runtime/node/community/local-community.ts
  • src/runtime/node/community/local-community/db-state.ts
  • src/runtime/node/community/local-community/export.ts
  • src/runtime/node/util.ts
  • test/node-and-browser/pkc/test.configs.pkc.test.ts
  • test/node/community/export.test.ts

Comment on lines +85 to +87
- `includePrivateKey: true` but RPC server policy disallows it. *(Planned — depends on RPC server.)*
- `exportPath` provided but caller is using an RPC client. *(Planned — depends on RPC client.)*
- `exportPath` resolves to the live community database file (would clobber it).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unify shipped-vs-planned status statements across this spec.

Line 15–Line 20 says RPC methods, HTTP download, and retention behavior are shipped, but Line 128–Line 130 and later sections still describe those same surfaces as planned/deferred. The same conflict appears in validation/error sections (for example Line 85–Line 87 and Line 241–Line 243). Please align these sections to one consistent status model to avoid contract drift.

Also applies to: 112-113, 128-130, 200-205, 216-249

🤖 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/rpc/EXPORT_COMMUNITY_SPEC.md` around lines 85 - 87, Several places in the
EXPORT_COMMUNITY_SPEC (references: the "RPC methods", "HTTP download",
"retention behavior" sections and the validation/error examples that mention
includePrivateKey and exportPath) disagree about whether features are "shipped"
or "planned"; pick the canonical status (e.g., mark these surfaces as shipped)
and make all occurrences consistent: update the status phrases and any
parenthetical notes like "Planned — depends on RPC server/client" to the chosen
status, remove or revise conflicting examples in the validation/error sections
(including checks that call out exportPath/exportPath-resolves-to-live-db and
includePrivateKey disallowed scenarios) so they align with the chosen model, and
ensure any downstream sections that describe behavior (RPC, HTTP download,
retention, and validation) use the same terminology and error expectations
throughout.

Comment thread src/rpc/src/index.ts
Comment on lines +177 to +187
this._httpServer.on("request", (req, res) => {
// rpc-websockets does not register request listeners — any HTTP request reaching
// this http.Server is ours to handle. Currently the only route is GET /exports/<id>.
this._handleExportsHttpRequest(req, res).catch((e) => {
log.error("Unhandled error in /exports HTTP handler", e);
if (!res.headersSent) {
res.statusCode = 500;
res.end();
}
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't 404 unrelated routes on caller-provided servers.

When server is supplied, this listener still runs for every HTTP request, and _handleExportsHttpRequest() sends a 404 for any non-export path. On a shared HTTP(S) server that can hijack unrelated routes or double-write the response.

🩹 Minimal fix
-        this._httpServer.on("request", (req, res) => {
+        this._httpServer.on("request", (req, res) => {
+            const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
+            if (server && !pathname.startsWith("/exports/")) return;
             // rpc-websockets does not register request listeners — any HTTP request reaching
             // this http.Server is ours to handle. Currently the only route is GET /exports/<id>.
             this._handleExportsHttpRequest(req, res).catch((e) => {

Also applies to: 1709-1714

🤖 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/rpc/src/index.ts` around lines 177 - 187, The request listener currently
runs for every request and causes _handleExportsHttpRequest to send 404s for
non-/exports routes; restrict handling to only export paths (or bail without
writing response). Update the request handler attached to this._httpServer so it
first checks the request path (e.g., req.url or parsed pathname) and only calls
this._handleExportsHttpRequest(req, res) when the path matches the export route
(e.g., startsWith "/exports/" or exact "/exports"); otherwise return immediately
(do not set status or end the response). You can alternatively add the same
early-return guard at the top of _handleExportsHttpRequest to avoid touching
other code paths.

Comment thread src/rpc/src/index.ts
Comment on lines +1662 to +1673
private *_exportLoadedCommunities(): IterableIterator<LocalCommunity> {
for (const value of Object.values(this._startedCommunities)) if (value instanceof LocalCommunity) yield value;
for (const value of this._exportCommunityInstances.values()) yield value;
}

private _findCommunityOwningExport(exportId: string): { community: LocalCommunity; record: CommunityExportRecord } | undefined {
for (const community of this._exportLoadedCommunities()) {
const record = community._exports.find((r) => r.exportId === exportId);
if (record) return { community, record };
}
return undefined;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Persisted export URLs become undownloadable after a restart.

The HTTP handler only looks through _exportLoadedCommunities(), which is just started communities plus _exportCommunityInstances. Completed export records survive in KeyV, but after a daemon restart none of those communities are loaded yet, so a previously valid /exports/<id> URL returns 404 until some other export RPC call happens to hydrate that community first.

Also applies to: 1722-1728

🤖 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/rpc/src/index.ts` around lines 1662 - 1673, The handler that resolves
/exports/<id> only searches _exportLoadedCommunities() so exports persisted in
KeyV but whose LocalCommunity isn't started are invisible after restart; update
_findCommunityOwningExport (and the HTTP export handler logic that calls it) to
also query the persistent export records store (the KeyV-backed export
persistence used for completed CommunityExportRecord entries) when a record
isn't found in _exportLoadedCommunities(), and if found either
hydrate/instantiate the corresponding LocalCommunity (using the same
initialization used by _exportCommunityInstances) or return the persisted
CommunityExportRecord directly so the URL remains downloadable after a daemon
restart.

import PKCFactory from "../../../dist/node/index.js";
import { getAvailablePKCConfigsToTestAgainst, isRpcFlagOn, isRunningInBrowser, mockPKC } from "../../../dist/node/test/test-util.js";
import { itIfRpc } from "../../helpers/conditional-tests.js";
import { getDefaultDataPath } from "../../../dist/node/runtime/node/util.js";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move this case out of test/node-and-browser.

getDefaultDataPath comes from a node-only runtime module, so this file now hard-depends on the node build before the test body runs. That makes the new case unsafe for the browser leg of this suite. Either move it under test/node/ or avoid the runtime/node import here.

Also applies to: 151-170

🤖 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 `@test/node-and-browser/pkc/test.configs.pkc.test.ts` at line 5, The failing
test imports the Node-only symbol getDefaultDataPath from the runtime/node
module, which makes the file depend on the Node build and breaks browser runs;
either move the test case that uses getDefaultDataPath out of the shared
test/node-and-browser suite into the node-only suite (e.g., test/node) or remove
the static runtime/node import and instead gate a dynamic Node-only
import/require inside the test body (e.g., if (typeof process !== "undefined" &&
process.versions?.node) { const { getDefaultDataPath } = await
import(".../runtime/node/util.js"); ... }) so the module is only loaded when
running in Node. Ensure references to getDefaultDataPath are updated
accordingly.

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