File I/O

The Crous Node.js SDK provides convenient functions for reading and writing serialized data to files and streams, on top of the in-memorydumps / loads API.

Writing to Files

dump — Write by File Path

The simplest way to persist data. Pass the data and a file path — Crous handles the rest.

write_file.js
const crous = require('crous');

const config = {
    server: { host: '0.0.0.0', port: 8080 },
    database: { url: 'postgres://localhost/mydb' },
    features: ['auth', 'logging', 'rate-limiting'],
    maxRetries: 3,
    debug: false
};

// Write to file — synchronous
crous.dump(config, 'config.crous');
console.log('✓ Config saved');

dump — Write to Stream

Pass a writable stream instead of a file path for integration with Node.js stream pipelines.

write_stream.js
const crous = require('crous');
const fs = require('fs');

const data = { message: 'Hello from a stream!' };

const stream = fs.createWriteStream('output.crous');
crous.dump(data, stream);
stream.end();

// Works with any Writable stream
const { PassThrough } = require('stream');
const passthrough = new PassThrough();
const chunks = [];
passthrough.on('data', (chunk) => chunks.push(chunk));
passthrough.on('end', () => {
    const buffer = Buffer.concat(chunks);
    console.log(`Captured ${buffer.length} bytes`);
});
crous.dump(data, passthrough);
passthrough.end();

Reading from Files

load — Read by File Path

read_file.js
const crous = require('crous');

// Read from file — synchronous
const config = crous.load('config.crous');
console.log(config.server.host); // '0.0.0.0'
console.log(config.features);    // ['auth', 'logging', 'rate-limiting']

load — Read from Buffer

When you already have the file contents in memory (e.g., from an HTTP response or database), use loads directly.

read_buffer.js
const crous = require('crous');
const fs = require('fs');

// Read file into buffer manually
const buffer = fs.readFileSync('config.crous');
const config = crous.loads(buffer);

// From an HTTP response
const http = require('http');
http.get('http://api.example.com/data.crous', (res) => {
    const chunks = [];
    res.on('data', (chunk) => chunks.push(chunk));
    res.on('end', () => {
        const data = crous.loads(Buffer.concat(chunks));
        console.log(data);
    });
});

Multi-Record Files

You can write multiple records to the same file by concatenating serialized buffers. Each call to dumps produces a self-contained record.

multi_record.js
const crous = require('crous');
const fs = require('fs');

// Write multiple records
const stream = fs.createWriteStream('events.crous');

const events = [
    { type: 'click', x: 100, y: 200, time: Date.now() },
    { type: 'scroll', delta: -50, time: Date.now() },
    { type: 'keypress', key: 'Enter', time: Date.now() },
];

for (const event of events) {
    const buf = crous.dumps(event);
    // Write length prefix + data for framing
    const lenBuf = Buffer.alloc(4);
    lenBuf.writeUInt32BE(buf.length);
    stream.write(lenBuf);
    stream.write(buf);
}
stream.end();

// Read multiple records back
const fileData = fs.readFileSync('events.crous');
let offset = 0;
const decoded = [];

while (offset < fileData.length) {
    const len = fileData.readUInt32BE(offset);
    offset += 4;
    const record = fileData.subarray(offset, offset + len);
    decoded.push(crous.loads(record));
    offset += len;
}

console.log(`Read ${decoded.length} events`);
decoded.forEach((e) => console.log(e.type));

Using with CrousEncoder / CrousDecoder

The class-based API also supports file I/O with the same dump /load methods.

encoder_file_io.js
const { CrousEncoder, CrousDecoder } = require('crous');

const encoder = new CrousEncoder({
    default: (obj) => {
        if (obj instanceof Date) {
            return { __date__: obj.toISOString() };
        }
        throw new TypeError('Unserializable');
    }
});

const decoder = new CrousDecoder({
    object_hook: (obj) => {
        if (obj.__date__) return new Date(obj.__date__);
        return obj;
    }
});

// Write
encoder.dump({ created: new Date(), name: 'test' }, 'record.crous');

// Read
const record = decoder.load('record.crous');
console.log(record.created instanceof Date); // true

Error Handling for File I/O

error_handling.js
const crous = require('crous');

// File not found
try {
    crous.load('nonexistent.crous');
} catch (e) {
    console.log(e.code);    // 'ENOENT'
    console.log(e.message); // No such file or directory
}

// Corrupted file
try {
    const fs = require('fs');
    fs.writeFileSync('bad.crous', 'not valid crous data');
    crous.load('bad.crous');
} catch (e) {
    console.log(e.name);    // 'CrousDecodeError'
    console.log(e.message); // decode error details
}

// Permission denied
try {
    crous.dump({ data: 1 }, '/root/protected.crous');
} catch (e) {
    console.log(e.code);    // 'EACCES'
}

Synchronous I/O

File operations in the Crous Node.js SDK are currently synchronous. For high-throughput applications, consider running file I/O in a worker thread or using the in-memory dumps / loads with your own async file handling.

Practical Patterns

Configuration Files

config_pattern.js
const crous = require('crous');
const fs = require('fs');
const path = require('path');

function loadConfig(configPath) {
    if (!fs.existsSync(configPath)) {
        // Return defaults
        return {
            port: 3000,
            debug: false,
            allowedOrigins: ['http://localhost:3000']
        };
    }
    return crous.load(configPath);
}

function saveConfig(config, configPath) {
    // Atomic write: write to temp, then rename
    const tmpPath = configPath + '.tmp';
    crous.dump(config, tmpPath);
    fs.renameSync(tmpPath, configPath);
}

const config = loadConfig('app.crous');
config.debug = true;
saveConfig(config, 'app.crous');

Data Caching

cache_pattern.js
const crous = require('crous');
const fs = require('fs');

class CrousCache {
    constructor(cacheDir) {
        this.cacheDir = cacheDir;
        if (!fs.existsSync(cacheDir)) {
            fs.mkdirSync(cacheDir, { recursive: true });
        }
    }

    _keyPath(key) {
        // Simple hash for filename
        const hash = Buffer.from(key).toString('hex').slice(0, 16);
        return `${this.cacheDir}/${hash}.crous`;
    }

    set(key, value, ttlMs = 60_000) {
        const entry = {
            value,
            expires: Date.now() + ttlMs
        };
        crous.dump(entry, this._keyPath(key));
    }

    get(key) {
        const filePath = this._keyPath(key);
        if (!fs.existsSync(filePath)) return null;

        const entry = crous.load(filePath);
        if (Date.now() > entry.expires) {
            fs.unlinkSync(filePath);
            return null;
        }
        return entry.value;
    }
}

const cache = new CrousCache('./cache');
cache.set('users', [{ id: 1, name: 'Alice' }], 30_000);
console.log(cache.get('users')); // [{ id: 1, name: 'Alice' }]