Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Limitations

What this binding does not do, and why each one is a decision rather than a gap.

No initSync

Validation happens inside the load, so the load runs on a worker thread and calls back into the event loop. A synchronous init() would be the loop waiting for itself.

Use top-level await in an ESM module, or await config.init() in your main. A configuration that must exist before anything else — a Nest provider, a Next.js module — has an async door already: useFactory is async, and a server component is.

No configuration engine in the browser

No filesystem, no watcher, no store, and a bundler cannot polyfill any of them. What ships to a client is a snapshot the server chose to send. Web Frameworks draws the line and examples/12-react/README.md writes it out.

The eight Rust stores are a second package

etcd, Consul, Vault, NATS, Redis, S3, Firestore and git each carry a client — gRPC, an AWS SDK, three HTTP stacks — and putting them in every npm install dynamic-config-node is not a default anybody asked for. Same reasoning as the second wheel in Python.

A store this package does not ship is still a function away: Remote Stores.

A remote fetch reaches the engine synchronously

setRemote takes a function that answers { text, format }, not a promise: it is called from a worker thread through the loop, and awaiting from there is not possible. What setRemoteAsync changes is when the awaiting happens — refreshRemote() awaits the fetch on the calling loop and hands the engine the document that came back — not that the engine learned to await. A store the engine reaches on its own schedule would still have to be synchronous, which is why fetching stays explicit.

A changes() loop sleeps through a refusal — by design

A refused reload wakes events() and onReloadFailed natively (since engine 0.7.1, which grew the second wake channel this section used to ask for). What it still does not wake is changes(): that stream yields documents, a refusal installs none, and a service loop resized around undefined would be worse than one that slept. The split is the contract — changes() for the values, events() for the diagnosis — and the engine book's Change Notification page holds it for all three languages.

Encrypted files are not exposed

Decryption needs a Decryptor, which is a Rust trait. Decrypt with the CLI and point this at the result. The Python binding draws the same line for the same reason.

save and JSON Schema export are not exposed

The Rust crate can write a configuration back and export a JSON Schema from a type. Neither has an obvious Node shape — a schema here is a function, so there is nothing to export from — and both are one CLI invocation away.

What a validator may not be

Asynchronous. A validator is called inside the load, on a worker thread, and a promise cannot be awaited there. Every schema library's synchronous door — Zod's parse, Ajv's compiled validator — is what this takes. If a check genuinely needs I/O, it is not validation: do it after init() and refuse to start.

A class instance, or anything else JSON cannot carry. What a validator returns crosses back into Rust to be stored, so it is serialised: the document current() hands back is a plain object with the same data and none of the identity. A class loses its prototype — instanceof is false, methods and getters are gone. A Date is worse than a string: it serialises to {}, because it has no fields.

class Database { constructor(host) { this.host = host } get shouty() { … } }

validate: (document) => new Database(document.host)

config.current() instanceof Database   // false
config.current().shouty                // undefined

Return plain data — objects, arrays, strings, numbers, booleans, null — and keep the behaviour outside the configuration, where a reload does not have to rebuild it. Zod is fine as long as the schema is: z.date() and z.map() produce values with the same problem, and z.coerce.string() or an ISO string in the document is the shape that survives. A wrapper the program wants is one line at the read: new Database(config.current()).