@visualtools/namespace
v2.4.1
Published
Dotted-path namespace utilities for JavaScript — 8-verb point-contract API, path algebra, and batch contracts
Maintainers
Readme
@visualtools/namespace
Zero-dependency dotted-path namespace utilities for JavaScript. Safety-by-default verbs, auto-vivification, and a NotFound sentinel that distinguishes "missing" from "undefined".
Works in Node.js, browsers, and TypeScript.
Install
npm install @visualtools/namespace
# or
yarn add @visualtools/namespaceQuick start
import namespace from "@visualtools/namespace";
// or: const namespace = require("@visualtools/namespace");
const ctx = {};
// Bare call: ensure a path exists as a plain object
namespace(ctx, "app.config");
// ctx is now { app: { config: {} } }
// Create-only write (throws if path already holds something)
namespace.setNotExists(ctx, "app.config.port", 3000);
// Read — returns the value, or the NotFound sentinel if absent
const val = namespace.getIfExists(ctx, "app.config.host");
if (namespace.isNotFound(val)) {
console.log("host not configured yet");
}
// Read — throw if absent (great for fail-fast validation)
const port = namespace.getMustExist(ctx, "app.config.port");
// Read — return a fallback if absent, never write
const host = namespace.getOrDefault(ctx, "app.config.host", "localhost");
// Convergence — write only if absent; return whichever now holds
const cache = namespace.setOrDefault(ctx, "app.cache", new Map());Why namespace?
The verb name IS the contract. Each call encodes a claim about what the rest of the codebase has promised at that point. When the claim is wrong, the error names the exact path and verb — no hunting through stack traces.
NotFound is not undefined. getIfExists() returns a frozen sentinel for absent paths, so you can distinguish "not found" from "found and set to undefined". Most path libraries conflate these.
Safety by default. setNotExists() refuses to overwrite. You must reach for the longer name (setOverwrite) to clobber — the verbosity is the signal that you mean it.
The bare call: namespace(obj, path)
Ensures every segment of path exists as a plain object.
- Absent — vivify as
{} - Plain object — return it
- Anything else (array, string, number, ...) — throw
const ctx = {};
const db = namespace(ctx, "services.database");
// ctx is { services: { database: {} } }
// db === ctx.services.database
// Idempotent — second call returns the same object
namespace(ctx, "services.database") === db; // true
// Collision detection — won't silently coerce non-objects
ctx.services.cache = [1, 2, 3];
namespace(ctx, "services.cache"); // throws: non-object value exists at "cache"Read verbs
Read verbs never write to the object.
getIfExists(object, path)
Returns the value at path, or the NotFound sentinel if any segment is absent.
namespace.getIfExists(obj, "a.b.c"); // value or NotFound
namespace.isNotFound(result); // true if absentgetMustExist(object, path, options?)
Returns the value, or throws. Use for fail-fast validation.
namespace.getMustExist(obj, "required.field");
namespace.getMustExist(obj, "required.field", {
errorMessage: "Config error: field is required"
});getOrDefault(object, path, standIn)
Returns the value if present, otherwise standIn. Never writes.
const timeout = namespace.getOrDefault(config, "http.timeout", 5000);getOrDefault.syncFunc(object, path, fn)
Like getOrDefault, but calls fn() only when absent. Never writes.
const config = namespace.getOrDefault.syncFunc(ctx, "app.config", () => loadConfig());getOrDefault.asyncFunc(object, path, fn)
Like getOrDefault, but calls async fn() only when absent. Always returns a promise. Never writes.
const config = await namespace.getOrDefault.asyncFunc(ctx, "app.config", () => fetchConfig());getMustEmpty(object, path)
Throws if a value is present at path. Use as a guard before writing to a slot you know is new.
namespace.getMustEmpty(obj, "slot.that.should.be.new");
namespace.setNotExists(obj, "slot.that.should.be.new", value);Write verbs
setNotExists(object, path, value)
Create-only. Writes value, throws if path already holds something. Auto-vivifies missing intermediates.
namespace.setNotExists(obj, "users.alice.role", "admin");
namespace.setNotExists(obj, "users.alice.role", "user"); // throws: cannot overwritesetMustExist(object, path, value)
Update-only. Writes value, throws if path is absent. Does NOT auto-vivify — the whole hierarchy must already exist.
namespace.setMustExist(obj, "users.alice.role", "user"); // update existingsetOrDefault(object, path, value)
Convergence: writes value only if absent; returns whichever now holds. Auto-vivifies intermediates.
// Many routes may initialize this — first one wins
const db = namespace.setOrDefault(ctx, "connections.db", createPool());setOrDefault.syncFunc(object, path, fn)
Like setOrDefault, but calls fn() only when absent. Writes the result.
const pool = namespace.setOrDefault.syncFunc(ctx, "db.pool", () => createPool());setOrDefault.asyncFunc(object, path, fn)
Like setOrDefault, but calls async fn() only when absent. Awaits, writes, returns a promise.
const pool = await namespace.setOrDefault.asyncFunc(ctx, "db.pool", () => connectAsync());setOverwrite(object, path, value, options?)
Unconditional write. Clobbers any existing value. Auto-vivifies intermediates.
namespace.setOverwrite(obj, "status.updated", Date.now());
// By default, throws if an intermediate is a non-object.
// Pass { overwriteStructure: true } to clobber structure too.
namespace.setOverwrite(obj, "a.b.c", 1, { overwriteStructure: true });Test verbs
exists(object, path)
Returns true if the path holds any value — including 0, false, "", null.
if (namespace.exists(config, "feature.enabled")) { ... }isNotFound(value)
Returns true if value is the NotFound sentinel.
const result = namespace.getIfExists(obj, "maybe.missing");
if (namespace.isNotFound(result)) { ... }Remove verbs
rm(object, path)
Removes the value at path. No-op if absent. Returns the removed value, or NotFound if the path was absent.
const old = namespace.rm(obj, "temp.token");
if (!namespace.isNotFound(old)) {
console.log("removed:", old);
}rmMustExist(object, path)
Removes the value at path. Throws if absent. Returns the removed value.
const token = namespace.rmMustExist(ctx, "pending.token");Path algebra: namespace.path
Pure string operations — no tree argument.
namespace.path.join("users", userId, "entries");
// "users.alice.entries"
namespace.path.join("a.b", ["c", "d"]);
// "a.b.c.d"
namespace.path.joinSlash("api", "v2", "users");
// "api/v2/users"
namespace.path.split("a.b.c");
// ["a", "b", "c"]
namespace.path.isRootOf("users.alice", "users.alice.entries");
// true
namespace.path.tween("a.b.c");
// "a.children.b.children.c"
namespace.path.tween("a.b.c", "items");
// "a.items.b.items.c"Batch operations: namespace.batch
Multi-path contracts in one call.
// Destructure from tree — throws if any path absent
const { db, port } = namespace.batch.destructureMustExist(config, {
db: "connections.database.url",
port: "server.port",
});
// Assert multiple paths exist — keyed by dotted path
const vals = namespace.batch.allMustExist(config, [
"auth.secret",
"auth.issuer",
]);
// Extract: assert exists, delete from tree, return value
const token = namespace.batch.extractMustExist(ctx, "pending.token");Immutability: namespace.freeze
Object.freeze is one level deep. These walk the whole reachable tree.
// Freeze in place — same reference back, everything below it frozen
const config = namespace.freeze.deep(loadConfig());
config.server.port = 9090; // TypeError in strict mode
// Freeze one point in a tree that stays otherwise open
namespace.freeze.at(state, "constants"); // subtree
namespace.freeze.at(state, "config.port"); // primitive leaf → property locked
// Frozen copy — the original stays mutable
const snapshot = namespace.freeze.clone(state);
state.a.b = 2; // snapshot.a.b unchanged
// Mutable copy back out
const draft = namespace.freeze.thaw(snapshot);
namespace.freeze.isDeep(snapshot); // trueTraversal rules, shared by deep / at / isDeep:
- Cycle-safe — a node already visited is not revisited
- Getters are never invoked — accessor properties are skipped, only data properties are descended
- Symbol and non-enumerable keys included —
Reflect.ownKeys - Functions left alone unless
{ freezeFunctions: true } - Map/Set contents are not frozen — freezing the container does not stop
.set()/.add(); JS offers no way to make those immutable in place
freeze.clone / freeze.thaw copy plain objects and arrays; Date, Map, Set, and class instances are carried across by reference.
Configuration
// Append object JSON to error messages (truncated at 200 chars)
namespace.configure({ errorContext: true });
namespace.getMustExist({}, "missing");
// Error: namespace.getMustExist: property not found at "missing"
// object: {}Design philosophy
- The verb name is the contract —
setNotExistsmeans create-only;setOverwritemeans you intend to clobber - NotFound is not undefined — the sentinel distinguishes absence from a stored
undefined - Fail fast —
MustExistverbs throw with the exact path, not a silentundefined - Safety by default — reaching for the destructive verb requires the longer name
- Functions applied to data, never resident in it — the tree is always plain
{} - Zero dependencies
See METALAND/ for the full applied philosophy.
License
MIT
