Conversation
Five runnable TypeScript examples covering the core relayfile SDK: - 01: read files (listTree, readFile, queryFiles) - 02: write files (writeFile, bulkWrite, optimistic locking) - 03: webhook ingestion (ingestWebhook, computeCanonicalPath) - 04: realtime events (getEvents polling with cursors) - 05: scoped agent permissions (path-restricted tokens, 403 handling) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| await new Promise<void>((resolve, reject) => { | ||
| const timer = setTimeout(resolve, ms); | ||
|
|
||
| if (!signal) { | ||
| return; | ||
| } | ||
|
|
||
| const onAbort = () => { | ||
| clearTimeout(timer); | ||
| reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError")); | ||
| }; | ||
|
|
||
| signal.addEventListener("abort", onAbort, { once: true }); | ||
| }); |
There was a problem hiding this comment.
🟡 sleep() leaks abort listener on every normal timer resolution
When the setTimeout fires normally and calls resolve, the onAbort listener is never removed from signal. The { once: true } option only auto-removes the listener when the abort event fires, not when the timer resolves. Since runLoop calls sleep on every poll iteration, a new orphaned listener accumulates on the AbortSignal each cycle. Over the lifetime of a long-running WritebackConsumer, this causes unbounded listener growth on the signal.
Comparison with client.ts sleep which properly cleans up
The existing client.ts:807-826 sleep correctly removes the listener on both paths:
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort); // ← cleanup on normal resolve
resolve();
}, delayMs);The new writeback-consumer.ts:125 passes resolve directly to setTimeout, never getting the chance to remove the listener.
| await new Promise<void>((resolve, reject) => { | |
| const timer = setTimeout(resolve, ms); | |
| if (!signal) { | |
| return; | |
| } | |
| const onAbort = () => { | |
| clearTimeout(timer); | |
| reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError")); | |
| }; | |
| signal.addEventListener("abort", onAbort, { once: true }); | |
| }); | |
| await new Promise<void>((resolve, reject) => { | |
| const onDone = () => { | |
| signal?.removeEventListener("abort", onAbort); | |
| resolve(); | |
| }; | |
| const timer = setTimeout(onDone, ms); | |
| if (!signal) { | |
| return; | |
| } | |
| const onAbort = () => { | |
| clearTimeout(timer); | |
| reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError")); | |
| }; | |
| signal.addEventListener("abort", onAbort, { once: true }); | |
| }); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| curl -fsS -X PUT "$RELAYFILE/v1/workspaces/$WS/fs/file?path=$file_path" \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "X-Correlation-Id: $correlation_id" \ | ||
| -H "Content-Type: application/octet-stream" \ | ||
| -d "$content" >/dev/null |
There was a problem hiding this comment.
🔴 seed.sh sends raw content instead of required JSON body and omits mandatory If-Match header
The rf_put function in seed.sh sends raw file content with Content-Type: application/octet-stream and no If-Match header. However, the Go server's handleWriteFile (internal/httpapi/server.go:1449) requires both: an If-Match header (returns 412 without it, line 1458) and a JSON body parsed via decodeJSONBody (returns 400 for non-JSON, line 1482). Since the script uses set -e and curl -fsS, the first rf_put call will exit non-zero, aborting the entire seed process. The Docker quickstart will report "Ready" without any files seeded, or more likely the seed container will crash.
Expected curl format matching the API contract
The SDK's writeFile sends If-Match: * and a JSON body {"contentType": ..., "content": ...}. The seed script should do the same:
curl -fsS -X PUT "$RELAYFILE/v1/workspaces/$WS/fs/file?path=$file_path" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Correlation-Id: $correlation_id" \
-H "If-Match: *" \
-H "Content-Type: application/json" \
-d "{\"contentType\":\"text/plain\",\"content\":\"$content\"}" >/dev/nullPrompt for agents
In docker/seed.sh, the rf_put function (lines 8-18) needs two fixes:
1. Add an If-Match header with value * (for create-or-overwrite) to the curl command.
2. Change the Content-Type from application/octet-stream to application/json, and wrap the content in a JSON body matching the server's expected format: {"contentType": "<mime>", "content": "<content>"}.
Note that the content being passed includes multi-line strings and JSON strings with special characters, so the JSON body construction must properly escape those. Consider using a tool like jq to safely build the JSON body, or carefully escape the content.
The rf_put function signature may also need an additional parameter for the content type (e.g. text/markdown vs application/json) so each call site can specify the correct MIME type.
Affected call sites are on lines 28-30 (welcome.md, text/markdown), line 32 (metadata.json, application/json), and line 33 (agents.json, application/json).
Was this helpful? React with 👍 or 👎 to provide feedback.
git clone → cd docker → docker compose up → working relayfile + relayauth stack with seed data.