Skip to content

[Sprint 16] Clear tsc + patch 0.1.1 - #4

Merged
joaovjo merged 1 commit into
mainfrom
feature/sprint-16
Jul 28, 2026
Merged

joaovjo merged 1 commit into
mainfrom
feature/sprint-16

Conversation

@joaovjo

@joaovjo joaovjo commented Jul 28, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Clear ~93 tsc --noEmit errors under TS7 / Bun 1.3
  • Align S3/Archive/Bun.cron/gzipSync APIs; add missing/prepend + FileVisibility
  • SemVer patch 0.1.1 (OIDC publish.yml existing)

Test plan

  • bunx tsc --noEmit exit 0
  • bun test 102 pass
  • After merge: publish OIDC → @ninots/filesystem@0.1.1

Summary by CodeRabbit

  • New Features
    • Added file existence checks, prepending content, and strongly typed visibility controls.
    • Improved S3 region, ACL, URL, and directory handling.
    • Added automatic scoped directory creation before writes.
    • Added in-memory archive creation, extraction, and listing.
  • Bug Fixes
    • Debounced filesystem watcher notifications to reduce duplicate events.
    • Improved compression handling and archive reliability.
  • Chores
    • Updated the package version to 0.1.1.

Align S3/Archive/cron/gzip APIs; add missing/prepend to contract; patch 0.1.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings July 28, 2026 01:41
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The package version was bumped to 0.1.1. Filesystem contracts gained visibility and write operations, adapters and decorators were updated, S3 handling was refined, archives moved to in-memory Bun APIs, scheduling was centralized, and TypeScript configuration was adjusted.

Changes

Filesystem API and adapters

Layer / File(s) Summary
Filesystem contracts and adapter delegation
src/contracts/filesystem.ts, src/adapters/local-adapter.ts, src/manager.ts
Adds typed file visibility, missing, and prepend operations, with local adapter implementations and manager delegation.
S3 ACL and directory handling
src/adapters/s3-adapter.ts
Adds typed ACLs, defaults the region, updates endpoint construction and upload ACL fallback, and handles S3 directory listing results defensively.

Runtime behavior

Layer / File(s) Summary
Adapter decorator behavior
src/decorators/compressed-adapter.ts, src/decorators/scoped-adapter.ts, src/decorators/watched-adapter.ts
Uses synchronous typed gzip APIs, creates scopes before writes when enabled, and debounces watcher callbacks.
In-memory archive operations
src/utils/archive.ts
Uses Bun.Archive instances for archive creation, extraction, blob generation, and listing.
Centralized filesystem scheduling
src/utils/scheduler.ts
Centralizes cron and interval job registration, lifecycle cleanup, backup execution, and supported cron interval conversion.

Project tooling

Layer / File(s) Summary
Package and compiler configuration
jsr.json, package.json, src/testing/helpers.ts, tsconfig.json
Bumps package versions, imports Bun test primitives, removes Node types, and excludes generated and dependency directories.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: copilot

Poem

A rabbit hops through files so bright,
Compressing clouds of code just right.
Archives bloom without a spare,
Cron jobs dance upon the air.
“Version up!” the bunny sings—
New filesystem wings!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main changes: clearing TypeScript errors and bumping the package to 0.1.1.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@joaovjo
joaovjo merged commit 32bd777 into main Jul 28, 2026
3 of 4 checks passed

@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.

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (1)
src/adapters/s3-adapter.ts (1)

78-95: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Apply defaultAcl in the regular upload path.

S3AdapterConfig.acl is documented as the default ACL, but put() writes with client.file(key).write(contents) and never uses this value. Bun’s S3 write API supports acl, so pass this.defaultAcl into the write options when present, including for append()/prepend() upload paths.

🤖 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/adapters/s3-adapter.ts` around lines 78 - 95, Update the S3Adapter upload
flows in put() and the append()/prepend() paths to pass this.defaultAcl through
Bun’s S3 write options when it is defined, while preserving existing behavior
when no ACL is configured.

Source: MCP tools

🤖 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/adapters/local-adapter.ts`:
- Around line 129-132: Update LocalAdapter.prepend to serialize the get-and-put
sequence per targetPath, reusing the existing per-path write coordination used
by other write operations if available. Ensure concurrent prepends to the same
path execute sequentially so both prefixes are retained, while preserving
independent concurrency for different paths and the current return behavior.

In `@src/adapters/s3-adapter.ts`:
- Line 652: Update the directory-prefix computation in the S3 listing logic
around dirName so nested keys preserve their full directory path relative to the
requested directory, rather than using only parts[0]. Ensure keys like
foo/bar/file.txt emit foo/bar, while retaining the existing undefined guard and
avoiding prefixes outside the requested directory.
- Around line 467-468: Update the URL construction in the S3 adapter method
containing endpoint and bucket interpolation to normalize trailing slashes on
endpoint and URL-encode each segment of key while preserving its slash
separators. Keep the bucket and key path structure intact so special characters
cannot alter the URL path or become query/fragment delimiters.
- Line 627: Update scanDirectory() to paginate S3 list responses: process each
page’s result.contents, then reuse result.nextContinuationToken as
continuationToken while result.isTruncated is true. Preserve the existing
object-processing behavior so files(), allFiles(), and directory discovery
include every page.

In `@src/contracts/filesystem.ts`:
- Around line 155-158: Update S3Adapter.getVisibility() to use the provided
path, check whether the object exists, and retrieve its S3 ACL so it returns the
actual "private" or "public" visibility instead of defaultAcl. Update
S3Adapter.setVisibility() to avoid reporting success while performing no ACL
change; either apply the requested S3 ACL or return the established
unsupported-operation result.

In `@src/decorators/compressed-adapter.ts`:
- Around line 106-110: The string-based FilesystemDisk read corrupts binary data
before decompression or archiving. Add a byte-exact read method to the disk
contract and its adapters, then update CompressedAdapter.get() in
src/decorators/compressed-adapter.ts, archive.createFromDirectory() at
src/utils/archive.ts:61-71, and archive.createBlob() at
src/utils/archive.ts:108-118 to use it when reading compressed files or archive
entries.
- Around line 3-7: Restrict the GzipLevel type in compressed-adapter.ts to the
valid zlib compression levels -1 through 9, removing 10, 11, and 12 so level
options passed to Bun.gzipSync and put() cannot represent unsupported values.

In `@src/decorators/watched-adapter.ts`:
- Around line 130-154: Update the watcher logic around the debounceTimer in the
watch setup to maintain separate debounce timers for each watched path and
filename, so events for different files do not replace one another. When
unwatching a path or all watchers, clear and remove the associated pending
timers before removing callbacks/watchers, preventing delivery to removed
callbacks.

In `@src/utils/scheduler.ts`:
- Around line 215-224: Update the backup method’s catch block to log the caught
failure with the scheduler’s existing logging mechanism, then rethrow it so
backup failures remain observable to scheduled maintenance callers; do not leave
the error silently swallowed.
- Around line 127-167: Update registerJob to reject unsupported cron expressions
when cronToInterval returns no interval instead of returning an inactive
CronJob, and wrap Bun.cron invocation failures for invalid expressions with a
clear error. Also ensure the project either constrains engines.bun and
packageManager to Bun >=1.3.12 or replaces the 2-argument Bun.cron call with a
compatible API path.

---

Outside diff comments:
In `@src/adapters/s3-adapter.ts`:
- Around line 78-95: Update the S3Adapter upload flows in put() and the
append()/prepend() paths to pass this.defaultAcl through Bun’s S3 write options
when it is defined, while preserving existing behavior when no ACL is
configured.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a766af8-a8de-4691-bf36-3f280be4e4a7

📥 Commits

Reviewing files that changed from the base of the PR and between d913b5f and 9b4ccc4.

📒 Files selected for processing (13)
  • jsr.json
  • package.json
  • src/adapters/local-adapter.ts
  • src/adapters/s3-adapter.ts
  • src/contracts/filesystem.ts
  • src/decorators/compressed-adapter.ts
  • src/decorators/scoped-adapter.ts
  • src/decorators/watched-adapter.ts
  • src/manager.ts
  • src/testing/helpers.ts
  • src/utils/archive.ts
  • src/utils/scheduler.ts
  • tsconfig.json

Comment on lines +129 to +132
async prepend(targetPath: string, data: string): Promise<boolean> {
try {
const existing = (await this.get(targetPath)) ?? "";
return await this.put(targetPath, data + existing);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialize prepend writes to prevent lost updates.

get() followed by put() is not atomic. Concurrent prepend() calls can read the same old contents, then the last write silently discards the other prefix. Add per-path serialization shared with other write operations, or use an atomic filesystem primitive.

🤖 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/adapters/local-adapter.ts` around lines 129 - 132, Update
LocalAdapter.prepend to serialize the get-and-put sequence per targetPath,
reusing the existing per-path write coordination used by other write operations
if available. Ensure concurrent prepends to the same path execute sequentially
so both prefixes are retained, while preserving independent concurrency for
different paths and the current return behavior.

Comment on lines +467 to +468
const endpoint = this.endpoint || `https://s3.${this.region}.amazonaws.com`;
return `${endpoint}/${this.bucket}/${key}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching s3-adapter:"
fd -a 's3-adapter\.(ts|js)$' . || true

file="$(fd 's3-adapter\.(ts|js)$' . | head -n 1)"
if [ -n "${file:-}" ]; then
  echo "Selected: $file"
  wc -l "$file"
  echo "Outline:"
  ast-grep outline "$file" || true
  echo "Relevant lines 430-490:"
  sed -n '430,490p' "$file" | nl -ba -v430
fi

echo
echo "S3 key/url related usages:"
rg -n "getObjectUrl|endpoint|bucket|/\\$\\{key\\}|url|sign|presign" "$file" 2>/dev/null || true

echo
echo "Search URL encoding across repo:"
rg -n "encodeURI|encodeURIComponent|URLSearchParams|object keys|objectKey|S3" -S . || true

Repository: nino-ts/filesystem

Length of output: 1017


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="./src/adapters/s3-adapter.ts"
echo "Relevant lines 430-490:"
sed -n '430,490p' "$file"

echo
echo "All occurrences of generated URL expression:"
rg -n "endpoint.*bucket|bucket.*key|getObjectUrl|temporary|presigned|\\$\\{key\\}|\\$\\{endpoint\\}" "$file"

echo
echo "Read-only verifier: extract key interpolation expressions and detect simple URI segment encoding pattern"
python3 - <<'PY'
from pathlib import Path
import re
p = Path('./src/adapters/s3-adapter.ts').read_text()
m = re.search(r'const endpoint = this\.endpoint \|\| `https://s3\.\$\{this\.region\}\.amazonaws\.com`;\s*return `[^`]*(?:\$\{\w+\})+`', p)
print("Found endpoint/url expression:", bool(m))
if m:
    start = max(0, p.rfind('\n', 0, m.start()) + 1)
    line_no = p.count('\n', 0, m.start()) + 1
    print(f"line {line_no}: {m.group(0)}")
PY

Repository: nino-ts/filesystem

Length of output: 2738


URL-encode S3 object keys.

S3 keys may contain spaces, ?, #, and other characters requiring URL encoding; interpolating key directly can address a different object or create a query/fragment instead. Encode each path segment while preserving /, and normalize endpoint slashes.

🤖 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/adapters/s3-adapter.ts` around lines 467 - 468, Update the URL
construction in the S3 adapter method containing endpoint and bucket
interpolation to normalize trailing slashes on endpoint and URL-encode each
segment of key while preserving its slash separators. Keep the bucket and key
path structure intact so special characters cannot alter the URL path or become
query/fragment delimiters.

Source: MCP tools

const results: string[] = [];

for (const obj of result.objects || []) {
for (const obj of result.contents ?? []) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Paginate truncated S3 listings.

result.contents is only the first 1,000-object page. scanDirectory() currently ignores isTruncated/nextContinuationToken, so files(), allFiles(), and directory discovery can silently omit later objects. Loop with continuationToken = response.nextContinuationToken until isTruncated is false.

🤖 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/adapters/s3-adapter.ts` at line 627, Update scanDirectory() to paginate
S3 list responses: process each page’s result.contents, then reuse
result.nextContinuationToken as continuationToken while result.isTruncated is
true. Preserve the existing object-processing behavior so files(), allFiles(),
and directory discovery include every page.

if (parts.length > 1) {
const dirName = parts[0];
if (!results.includes(dirName)) {
if (dirName !== undefined && !results.includes(dirName)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve nested directory paths.

The new undefined guard is safe, but dirName = parts[0] still emits only the first key component. For example, foo/bar/file.txt yields foo, not foo/bar; recursive listings therefore omit nested directories and scoped listings return incorrect paths. Emit directory prefixes relative to the requested directory.

🤖 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/adapters/s3-adapter.ts` at line 652, Update the directory-prefix
computation in the S3 listing logic around dirName so nested keys preserve their
full directory path relative to the requested directory, rather than using only
parts[0]. Ensure keys like foo/bar/file.txt emit foo/bar, while retaining the
existing undefined guard and avoiding prefixes outside the requested directory.

Comment on lines 155 to +158
/**
* Get the visibility of a file (public/private).
*/
getVisibility(path: string): Promise<string | null>;
getVisibility(path: string): Promise<FileVisibility | null>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate filesystem-related files =="
git ls-files | rg 'src/contracts/filesystem\.ts$|filesystem|storage|s3|adapter|storage' | head -200

echo
echo "== outline src/contracts/filesystem.ts =="
if [ -f src/contracts/filesystem.ts ]; then
  ast-grep outline src/contracts/filesystem.ts || true
  wc -l src/contracts/filesystem.ts
fi

echo
echo "== relevant contract section =="
if [ -f src/contracts/filesystem.ts ]; then
  sed -n '120,190p' src/contracts/filesystem.ts
fi

echo
echo "== search getVisibility/setVisibility implementations/usages =="
rg -n "getVisibility|setVisibility|FileVisibility|setPublic|SetPublic|FilesystemDisk" src || true

Repository: nino-ts/filesystem

Length of output: 10735


🏁 Script executed:

# The previous shell was a capability request only; results are not present.

Repository: nino-ts/filesystem

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== S3 adapter visibility methods =="
sed -n '360,450p' src/adapters/s3-adapter.ts

echo
echo "== Memory adapter visibility methods =="
sed -n '260,320p' src/adapters/memory-adapter.ts

echo
echo "== Local adapter visibility methods =="
sed -n '220,255p' src/adapters/local-adapter.ts

echo
echo "== S3 tests visibility-related content =="
rg -n -C 5 "getVisibility|setVisibility|visibility|acl|aclObject|ACL" tests/unit/s3-adapter.test.ts

echo
echo "== package deps relevant =="
if [ -f package.json ]; then jq '.dependencies, .devDependencies | {aws, s3}' package.json; fi

Repository: nino-ts/filesystem

Length of output: 7713


🏁 Script executed:

# The previous shell was a capability request only; results are not present.

Repository: nino-ts/filesystem

Length of output: 156


Make S3 visibility methods honor the visibility contract.

S3Adapter.getVisibility() ignores the requested path and defaults every object to the configured defaultAcl; it should check object existence and read/report the object’s ACL before returning "private" or "public". Also avoid returning success from setVisibility() when it only documents a no-op and does not update S3 ACL 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/contracts/filesystem.ts` around lines 155 - 158, Update
S3Adapter.getVisibility() to use the provided path, check whether the object
exists, and retrieve its S3 ACL so it returns the actual "private" or "public"
visibility instead of defaultAcl. Update S3Adapter.setVisibility() to avoid
reporting success while performing no ACL change; either apply the requested S3
ACL or return the established unsupported-operation result.

Comment on lines +3 to +7
/**
* Bun gzip compression level (zlib / libdeflate).
*/
export type GzipLevel = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
curl -fsSL https://bun.sh/docs/runtime/utils | grep -F 'Must be between `-1` and `9`'
rg -n -C2 'type GzipLevel|gzipSync' src/decorators/compressed-adapter.ts

Repository: nino-ts/filesystem

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant occurrences =="
rg -n -C3 'type GzipLevel|gzipSync|gunzipSync|level.*GzipLevel|GzipLevel' src/decorators/compressed-adapter.ts

echo
echo "== targeted docs search =="
curl -fsSL https://bun.sh/docs/runtime/utils | sed -n '/gzipSync/,+60p' | sed -n '1,120p'

echo
echo "== file section =="
sed -n '1,90p;120,170p' src/decorators/compressed-adapter.ts

Repository: nino-ts/filesystem

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant occurrences =="
rg -n -C3 'type GzipLevel|gzipSync|gunzipSync|level.*GzipLevel|GzipLevel' src/decorators/compressed-adapter.ts

echo
echo "== targeted docs search =="
curl -fsSL https://bun.sh/docs/runtime/utils | sed -n '/gzipSync/,+80p' | sed -n '1,160p'

echo
echo "== file section =="
sed -n '1,100p;120,180p' src/decorators/compressed-adapter.ts

Repository: nino-ts/filesystem

Length of output: 50375


🌐 Web query:

Bun gzipSync level option must be between -1 and 9 Bun.gzipSync docs

💡 Result:

The level option for Bun.gzipSync must be between -1 and 9 [1][2][3]. According to the official Bun documentation, the compression level is defined within the ZlibCompressionOptions interface as follows: - A value of -1 uses the default compression level (currently 6) [1][2][3]. - A value of 0 provides no compression [1][2][3]. - A value of 1 offers the least compression and the fastest speed [1][2][3]. - A value of 9 offers the best compression and the slowest speed [1][2][3]. The valid range for the level option is -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, or 9 [1][3].

Citations:


Restrict GzipLevel to -1..9.

Bun.gzipSync only accepts zlib levels -1 through 9; including 10..12 makes invalid call options available through level?: GzipLevel, and these values can fail put() via the try/catch.

🤖 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/decorators/compressed-adapter.ts` around lines 3 - 7, Restrict the
GzipLevel type in compressed-adapter.ts to the valid zlib compression levels -1
through 9, removing 10, 11, and 12 so level options passed to Bun.gzipSync and
put() cannot represent unsupported values.

Comment on lines 106 to 110
const data = await this.adapter.get(compressedPath);
if (data) {
try {
const decompressed = await Bun.gunzip(data);
const decompressed = Bun.gunzipSync(data);
return new TextDecoder().decode(decompressed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files around candidate names:"
fd -a 'compressed-adapter|archive|filesystem' . | sed 's#^\./##' | head -200

echo
echo "Inspect compressed-adapter outline:"
ast-grep outline src/decorators/compressed-adapter.ts --view expanded || true

echo
echo "Compressed-adapter relevant lines:"
cat -n src/decorators/compressed-adapter.ts | sed -n '1,160p'

echo
echo "Archive outline:"
ast-grep outline src/utils/archive.ts --view expanded || true

echo
echo "Archive relevant lines:"
cat -n src/utils/archive.ts | sed -n '1,150p'

Repository: nino-ts/filesystem

Length of output: 14360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Filesystem contract:"
cat -n src/contracts/filesystem.ts | sed -n '1,260p'

echo
echo "Search for FilesystemDisk get/getBytes/getBytes/bytes implementations and related adapters:"
rg -n "interface FilesystemDisk|class .*FilesystemDisk|implements FilesystemDisk|get\\(|getBytes\\(|bytes\\(|async get\\(|Promise\\.Uint8Array|Uint8Array|string \\| null" src -S

echo
echo "Try detecting static type shape:"
python3 - <<'PY'
from pathlib import Path
for p in Path("src").rglob("*.ts"):
    s=p.read_text()
    if "class " in s or "interface FilesystemDisk" in s:
        print(f"\n--- {p} ---")
        for i,line in enumerate(s.splitlines(),1):
            if "get(" in line or "interface FilesystemDisk" in line or "implements FilesystemDisk" in line or "bytes(" in line or "getBytes(" in line or "Promise<" in line:
                print(f"{i}: {line[:220]}")
PY

echo
echo "Node text binary probe matching compressor flow:"
node - <<'JS'
const zlib = require('zlib');
const data = new Uint8Array([0xCA, 0xFE, 0xBA, 0xBE, 0xFF]);
const compressed = zlib.gzipSync(data);
const text = Buffer.from(compressed).toString('utf8');
const input = Buffer.from(text, 'utf8');
const decompressed = zlib.gunzipSync(input);
console.log(JSON.stringify({
  original: Array.from(data),
  textBytes: Array.from(input),
  decompressed: Array.from(decompressed),
  equalBufferToStringBuffer: Buffer.compare(compressed, input) === 0,
  equalOriginalToDecompressed: Buffer.compare(data, decompressed) === 0,
}));
JS

Repository: nino-ts/filesystem

Length of output: 36635


Add a byte-exact filesystem read path. FilesystemDisk.get(): Promise<string | null> is used for binary round-trips, so compressed bytes survive UTF-8 text encoding/decoding and gunzipSync sees corrupted output. Add a byte API to the contract/adapters and use it at CompressedAdapter.get(), archive.createFromDirectory(), and archive.createBlob() before decompressing or building archive entries.

📍 Affects 2 files
  • src/decorators/compressed-adapter.ts#L106-L110 (this comment)
  • src/utils/archive.ts#L61-L71
  • src/utils/archive.ts#L108-L118
🤖 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/decorators/compressed-adapter.ts` around lines 106 - 110, The
string-based FilesystemDisk read corrupts binary data before decompression or
archiving. Add a byte-exact read method to the disk contract and its adapters,
then update CompressedAdapter.get() in src/decorators/compressed-adapter.ts,
archive.createFromDirectory() at src/utils/archive.ts:61-71, and
archive.createBlob() at src/utils/archive.ts:108-118 to use it when reading
compressed files or archive entries.

Comment on lines +130 to +154
let debounceTimer: ReturnType<typeof setTimeout> | null = null;

const watcher = watch(path, { recursive: this.recursive }, (event, filename) => {
if (!filename) {
return;
}

// Debounce
const callbacks = this.callbacks.get(path);
if (!callbacks) {
return;
}

const eventType = event === "change" ? "change" : "rename";

// Call all callbacks
for (const callback of callbacks) {
try {
callback(eventType, filename);
} catch (_error) {}
if (debounceTimer !== null) {
clearTimeout(debounceTimer);
}

debounceTimer = setTimeout(() => {
for (const callback of callbacks) {
try {
callback(eventType, filename);
} catch (_error) {}
}
}, this.debounce);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Debounce per filename and cancel pending delivery on unwatch.

One timer per watched directory drops earlier events when different files change within the debounce window. It also survives unwatch()/unwatchAll(), so removed callbacks can still run. Track timers by watched path and filename, then clear them when the watcher is removed.

🤖 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/decorators/watched-adapter.ts` around lines 130 - 154, Update the watcher
logic around the debounceTimer in the watch setup to maintain separate debounce
timers for each watched path and filename, so events for different files do not
replace one another. When unwatching a path or all watchers, clear and remove
the associated pending timers before removing callbacks/watchers, preventing
delivery to removed callbacks.

Comment thread src/utils/scheduler.ts
Comment on lines +127 to +167
private registerJob(jobId: string, cronExpression: string, callback: CronJobCallback): CronJob {
let running = false;
let bunJob: import("bun").CronJob | null = null;
let timerId: ReturnType<typeof setInterval> | null = null;

const job: CronJob = {
isRunning: () => running,
start: () => {
if (running) {
return;
}

if (typeof Bun.cron === "function") {
bunJob = Bun.cron(cronExpression, callback);
running = true;
this.jobs.set(jobId, job);
return;
}

const interval = this.cronToInterval(cronExpression);
if (interval) {
timerId = setInterval(callback, interval);
running = true;
this.jobs.set(jobId, job);
}
},
stop: () => {
bunJob?.stop();
bunJob = null;
if (timerId !== null) {
clearInterval(timerId);
timerId = null;
}
running = false;
this.jobs.delete(jobId);
},
};

job.start();
return job;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check declared Bun version/engine constraints across the repo
fd -a 'package.json' -d 1 --exec cat {}
fd -a 'bunfig.toml' --exec cat {}
fd -a '.bun-version' --exec cat {}
rg -n '"bun"' package.json 2>/dev/null

Repository: nino-ts/filesystem

Length of output: 1783


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- files ---\n'
git ls-files | rg '(^|/)src/utils/scheduler\.ts$|scheduler'

printf '\n--- scheduler outline ---\n'
ast-grep outline src/utils/scheduler.ts --view compact || true

printf '\n--- relevant scheduler lines ---\n'
cat -n src/utils/scheduler.ts | sed -n '1,240p'

printf '\n--- Bun cron parse/overload mentions in repo ---\n'
rg -n 'Bun\.cron|cronToInterval|scheduleCleanup|scheduleBackup|schedule\(' src tests || true

printf '\n--- package runtime deps/types ---\n'
node - <<'JS'
const fs=require('fs');
const p=require('./package.json');
console.log(JSON.stringify({engines:p.engines,packageManager:p.packageManager,dependencies:p.dependencies,devDependencies:p.devDependencies}, null, 2));
JS

Repository: nino-ts/filesystem

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- files ---'
git ls-files | rg '(^|/)src/utils/scheduler\.ts$|scheduler' || true

printf '%s\n' ''
printf '%s\n' '--- scheduler outline ---'
ast-grep outline src/utils/scheduler.ts --view compact || true

printf '%s\n' ''
printf '%s\n' '--- relevant scheduler lines ---'
cat -n src/utils/scheduler.ts | sed -n '1,240p' || true

printf '%s\n' ''
printf '%s\n' '--- Bun cron parse/overload mentions in repo ---'
rg -n 'Bun\.cron|cronToInterval|scheduleCleanup|scheduleBackup|schedule\(' src tests || true

printf '%s\n' ''
printf '%s\n' '--- package runtime deps/types ---'
node - <<'JS'
const fs=require('fs');
const p=require('./package.json');
const keys = ['engines','packageManager','dependencies','devDependencies'];
console.log(JSON.stringify(keys.reduce((o,k)=>({...o,[k]:p[k]}),{}), null, 2));
JS

Repository: nino-ts/filesystem

Length of output: 10439


🌐 Web query:

Bun cron API Bun.cron.parse Bun.cron throws TypeError invalid expression Bun.cron overload Bun.cron(path, expression, title) Bun.cron(expression, callback)

💡 Result:

The Bun.cron API allows for in-process cron scheduling and the registration of OS-level cron jobs [1][2][3]. API Overloads Bun.cron uses the following main overloads: 1. In-process scheduling: Bun.cron(schedule, handler) [1][2] - schedule: A string representing a 5-field cron expression or a predefined nickname (e.g., "@hourly") [1][4]. - handler: A callback function executed on each fire [1][2]. It returns a CronJob object synchronously [2][5]. 2. OS-level job registration: Bun.cron(path, schedule, title) [1][2] - path: Path to the script (resolved relative to the caller) [1][2]. - schedule: Cron expression or nickname [2]. - title: A unique alphanumeric identifier (can include hyphens and underscores) [1][2]. - This returns a Promise [2][6][7]. Bun.cron.parse The Bun.cron.parse(expression, relativeDate?) function parses a cron expression and returns the next matching UTC Date (or null if no match exists within approximately 4 years) [2][8][3]. Error Handling Bun.cron throws a TypeError if the provided cron expression is invalid (e.g., syntactically incorrect) or represents a time that will never occur (e.g., "0 0 30 2 *") [2][5]. For OS-level jobs, if a provided cron pattern is incompatible with the host operating system's constraints, the function may reject with an error message [5].

Citations:


🌐 Web query:

Bun cron callback overload added version Bun.cron(Bun.script(...), expression, title) schedule handler 1.3.12 1.3.13

💡 Result:

The in-process callback overload for Bun.cron was introduced in Bun v1.3.12 [1][2][3]. This feature allows you to run a function on a cron schedule directly within your application process, sharing state (such as database pools, caches, and module-level variables) with the rest of your code [1][3]. It complements the existing OS-level Bun.cron(path, schedule, title) API, which handles persistent, external cron jobs via system schedulers like crontab, launchd, or Task Scheduler [1][4]. Key characteristics of the in-process Bun.cron(schedule, handler) overload include: No-overlap guarantee: The next execution is scheduled only after the handler (and any returned Promise) has settled [1][4]. If a task takes longer than its scheduled interval, it will not trigger concurrent runs; instead, it will resume at the next available interval after completion [4][5]. Error handling: Synchronous errors in the handler emit uncaughtException, and rejected Promises emit unhandledRejection [1][6]. The job will reschedule itself after an error, provided the process does not exit [1][7]. Lifecycle Management: The function returns a CronJob object which can be managed using.ref or.unref, and supports the Disposable interface (e.g., using job = Bun.cron(...)) to automatically stop the job when the scope is exited [1][4]. Compatibility: It is --hot safe, meaning in-process cron jobs are automatically cleared before the module graph is re-evaluated, preventing timer leaks during hot reloads [1][6]. Time Zones: While initially scheduled in UTC, subsequent updates (following v1.3.12) added support for interpreting schedules in the system's local time zone or specific IANA time zones via an options argument [8][5]. The API signature is Bun.cron(schedule: string, handler: (this: CronJob) => unknown) [4][9]. As of version 1.3.13, this functionality remains core to the Bun.cron runtime API [1][2].

Citations:


Avoid scheduling unsupported expressions silently and constrain Bun.cron versions.

registerJob can return a valid-looking CronJob that never runs when Bun’s interval fallback hits an unsupported cronToInterval case, and if Bun.cron is present on older Bun engines the 2-argument callback overload may actually be the 3-argument OS-level signature instead, causing cronExpression to be treated as a script path. Raise a clear error for unsupported/pending fallbacks, wrap Bun.cron(...) invalid-expression cases, and tighten engines.bun/packageManager to Bun >=1.3.12 or stop calling the 2-argument overload.

[low_effort和high_reward]

🤖 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/utils/scheduler.ts` around lines 127 - 167, Update registerJob to reject
unsupported cron expressions when cronToInterval returns no interval instead of
returning an inactive CronJob, and wrap Bun.cron invocation failures for invalid
expressions with a clear error. Also ensure the project either constrains
engines.bun and packageManager to Bun >=1.3.12 or replaces the 2-argument
Bun.cron call with a compatible API path.

Comment thread src/utils/scheduler.ts
Comment on lines 215 to 224
private async backup(source: string, destination: string): Promise<void> {
try {
// Import ArchiveUtils dynamically to avoid circular dependency
const { ArchiveUtils } = await import("./archive");

const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupPath = `${destination}_${timestamp}.tar`;

const success = await ArchiveUtils.createFromDirectory(backupPath, source, this.adapter);

if (success) {
} else {
}
await ArchiveUtils.createFromDirectory(backupPath, source, this.adapter);
} catch (_error) {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

backup() silently swallows all failures.

The empty catch (_error) {} means a failed backup (bad source, archive write failure, permissions, etc.) produces no signal whatsoever — no log, no thrown error, no return value to inspect. Combined with cleanup()'s identical pattern (line 209), scheduled maintenance jobs can silently stop working indefinitely. Bun's own cron guidance in the community docs notes that Swallowing errors with an empty catch makes production debugging very difficult. Re-throwing after logging is the right pattern.

🛠️ Suggested minimal fix
-        } catch (_error) {}
+        } catch (error) {
+            console.error(`Backup failed for "${source}" -> "${destination}":`, error);
+        }
📝 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
private async backup(source: string, destination: string): Promise<void> {
try {
// Import ArchiveUtils dynamically to avoid circular dependency
const { ArchiveUtils } = await import("./archive");
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupPath = `${destination}_${timestamp}.tar`;
const success = await ArchiveUtils.createFromDirectory(backupPath, source, this.adapter);
if (success) {
} else {
}
await ArchiveUtils.createFromDirectory(backupPath, source, this.adapter);
} catch (_error) {}
}
private async backup(source: string, destination: string): Promise<void> {
try {
const { ArchiveUtils } = await import("./archive");
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupPath = `${destination}_${timestamp}.tar`;
await ArchiveUtils.createFromDirectory(backupPath, source, this.adapter);
} catch (error) {
console.error(`Backup failed for "${source}" -> "${destination}":`, 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/utils/scheduler.ts` around lines 215 - 224, Update the backup method’s
catch block to log the caught failure with the scheduler’s existing logging
mechanism, then rethrow it so backup failures remain observable to scheduled
maintenance callers; do not leave the error silently swallowed.

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.

2 participants