@aweftjs/debug
Reads a running document or commit back as text.
Nothing in the stack imports this package, and nothing may: it is registered as an integrator, so the boundary check fails on any package that reaches for it. You import it yourself, in a test, in a script, or in a running server while working out what happened. It is not stripped from a release build, because the moment you most want it is the one you did not plan for.
Every function here returns a string. That is deliberate. The reader is usually looking at test output or a terminal, and a rich object printed into either is worse than a plain line.
Quickstart
import { createObject, observer } from '@aweftjs/core';
import { explain, trace } from '@aweftjs/debug';
const doc = createObject({ title: 'plan', size: 1 });
// What is in here?
console.log(explain(doc));
// object Hh8kQ2... (title: 'plan', size: 1)
// What is happening to it?
const t = trace(doc);
doc.title = 'plan b';
console.log(t.text());
// commit (deltas: 1)
// replace title (value: 'plan b')
t.stop();The four readers
explain(subject, document?) takes whatever you are holding and works out what it is: an observable, a commit, or either kind of refusal. It never throws. A value it cannot place comes back saying so, because a debug call that fails leaves you worse off than before you made it.
documentOf(document) reads a whole document as a tree, with each observable under the slot that holds it rather than in the flat id-keyed map a snapshot gives you. An observable reached twice prints once and then as a back-reference, so an alias cycle terminates. One the document no longer holds reads as detached rather than as the tree it used to sit in.
commitOf(commit, document?) reads a commit as the list of what it changed. Pass the document and every id becomes the path that reaches it; leave it out and each delta names its observable by id text.
You will see two spellings for an array slot, and the difference is real rather than sloppy. documentOf prints [0], because it is looking at the whole array and can count. A delta prints [at 018143c4ab], because a delta carries one position key and the index that key sits at depends on every other position in that array, which the delta does not carry. When a commit is read against its document the path resolves and you get the index.
trace(document, limit?) watches a document and keeps what it did. It is an ordinary watcher, so it hears commits after they land and it stops when you call stop(). It keeps the last 200 commits by default; a trace left running on a busy document is a memory leak with a friendly name, so the limit is not optional behaviour you have to remember to add. Each commit is rendered as it lands, so a trace does not change its mind about a commit when the tree above it moves later.
Why console.log is not enough
An observable is a proxy over an internal node. console.log(row) prints that node: raw id bytes, a slots Map whose values print as [Object], listeners, watchers, reach, and circular parent and root back-references. It is many lines, and none of them is your data. JSON.stringify(row) is worse: it walks the whole document from wherever you happened to be standing. explain asks the document what it holds and prints that.
Reading a refusal
Two different errors reach you, and they read differently because they come from different places. explain handles both.
A refusal from the library carries a stable reason, a detail saying what was seen, and a fix saying what to do:
try {
apply(doc, { deltas: [] });
} catch (e) {
console.log(explain(e));
// refusal empty-commit (message: ..., fix: Drop the commit instead of applying it, ...)
}Branch on reason, never on the message. The message is written for a person and is allowed to change; the reason is part of the format's contract.
A RefusedError from a guarded document is different, and it is the one you meet most. It comes from a rule your own application wrote, through intercept or through @aweftjs/schema, so it carries a list of refusals rather than one reason, and none of them carries a fix: the library did not write them and has nothing to suggest.
try {
doc.total = -1; // a rule the application registered refuses this
} catch (e) {
console.log(explain(e));
// refused (refusals: 1)
// refusal negative-total (at: 'total', message: 'a total is never negative')
}The remedy for one of these is whatever your rule meant, so put it in the rule's own message. Whether a Refusal should carry a fix field of its own is an open question.
What this package will not do for you
It does not tell you why a watcher did not fire. It shows you what the document holds and what commits landed, and the answer is usually in those two, but working it out is still yours.
It does not attach to anything on its own. There is no global, no auto-install, and no side effect from importing it.
It covers @aweftjs/core, and only that. Documents, commits and refusals. It does not read a live scope, a mounted node, a link or a store row: those live inside packages that hand out no way to see them, and reaching in would have meant a new permanent export in each. Design 103 is why that seam is not here. explain on one of those objects tells you it cannot place it rather than guessing at it.
API
Every export of @aweftjs/debug, its signature as the compiler resolves it, and its block comment.
@aweftjs/debug
Described
interface Described { readonly kind: string; readonly id?: string; readonly facts: readonly Fact[]; readonly children?: readonly Described[]; }A described thing. kind names what it is in the stack's own words.
Fact
type Fact = readonly [ label: string, value: unknown ];One labelled fact about a thing: the label a reader sees, and the value behind it.
Trace
interface Trace { text(): string; count(): number; stop(): void; }A running trace. stop is the function that ends it, as every listener in this stack.
commitOf
commitOf: (commit: Commit, document?: unknown) => DescribedA commit as the list of what it changed.
Params
commit: the commit, as a watcher receives it or asinverse()returns itdocument: optional, the document it landed in. With it, every id becomes the path that reaches it; without it, each delta names its observable by id text
Returns the described commit, ready for render.
Example
observer(doc).watch((change) => console.log(render(commitOf(change, doc))));documentOf
documentOf: (document: unknown) => DescribedA whole document as a tree, ids resolved to the slots that hold them.
Params
document: any observable in the document; the whole document is read from its root
Returns the described tree, ready for render. An observable the document no longer holds comes back as detached rather than as the tree it used to sit in.
Throws not-observable when the value is not one of this stack's observables.
Example
console.log(render(documentOf(doc)));explain
explain: (subject: unknown, document?: unknown) => stringWhatever you are holding, as text.
Params
subject: an observable, a commit, or either kind of refusal this stack throwsdocument: optional, the document a commit landed in, so its ids become paths
Returns lines a person or an agent reads. Never throws: something it cannot describe comes back saying so, because a debug call that fails leaves its reader worse off than before.
Example
try { doc.count = 'text'; } catch (e) { console.log(explain(e)); }render
render: (d: Described) => stringA described thing as text.
Params
d: what a describe hook or one of this package's readers produced
Returns one line per node, children indented two spaces under their parent, no trailing newline.
Example
console.log(render(documentOf(doc)));trace
trace: (document: unknown, limit?: number) => TraceWatch a document and keep what it did, as text.
Params
document: any observable in the document to watchlimit: how many commits to keep, oldest dropped first. Defaults to 200, because a trace left running on a busy document is a memory leak with a friendly name
Returns the trace. Registering it returns its stop function on the object, as every listener in this stack does. Each commit is rendered as it lands, so what a trace says about a commit does not change when the document moves underneath it later.
Throws not-observable when the value is not one of this stack's observables.
Example
const t = trace(doc);
doc.title = 'changed';
console.log(t.text());
t.stop();Refusals
errors.txt lists no refusal for this package.
Recipes
The programs in the stack's gate that use this package, each a job someone would have.
recipes/optimistic-write: A write that applies locally before the server sees it, is refused, and is rolled backrecipes/debug: Finding a bug in a document you did not write