Identity and keys
A node’s identity is a graph node id of the form tag:key. The tag is set on the spec; the key is computed from args every time a node is requested. Same tag and same key produce the same id, which resolves to the same node.
runtime.client.node(ProfileNode, { userId: "u_42" }).nodeId;
// -> "resources/profile:v1:{\"userId\":\"u_42\"}"
The tag
The tag names the kind of node, not an instance. Set it on the spec with Frond.tag(value).
static readonly spec = Frond.resourceSpec<ProfileSpec>({
tag: Frond.tag("resources/profile"),
// ...
});
- Use path-like values:
resources/profile, notresources/profile/u_42. - The tag is part of the id. Renaming it changes the id of every node built from the spec.
- Two specs must not share a tag.
The key
A node declares a key(args) function. The runtime runs it, then canonicalizes the result into the string half of the id.
key: (args) => Frond.Key.structure({ userId: args.userId }),
The function picks the parts of args that define identity and returns a branded key. Use Frond.Key.structure(...) for JSON-shaped structured keys and Frond.Key.singleton() for singleton nodes.
Warning:
keymust be pure and deterministic — no clock, no random, no environment, no Effect services, no async. A non-deterministic key (for example, one that includesDate.now()) produces a new id on every call, and therefore a new node and a new fetch.
Canonicalization
The runtime does not compare key values directly. It serializes the value with canonicalKey and compares the resulting string.
| Rule | Behavior |
|---|---|
| Object key order | Ignored. Object keys are sorted before stringifying, so { a: 1, b: 2 } and { b: 2, a: 1 } produce the same key. |
| Array order | Significant. [1, 2] and [2, 1] are different keys. Sort first if order is not part of identity. |
| Allowed values | JSON-shaped only: string, finite number, boolean, null, undefined, arrays, and plain objects. |
| Prefix | The canonical string starts with v1:. |
| Length cap | 2048 characters (MAX_CANONICAL_KEY_LENGTH). |
Inputs that cannot be canonicalized throw:
| Input | Error |
|---|---|
A Date, Map, class instance, or function | KeyUnsupportedJsonValueError |
NaN, Infinity, or -Infinity | KeyNonFiniteNumberError |
| A canonical string over 2048 characters | KeyTooLongError |
Include only the fields that distinguish the node; embedding whole payloads can exceed the length cap.
Failure modes
| Mode | Cause | Result | Fix |
|---|---|---|---|
| Over-keying | A field varies when it should not (timestamp, request nonce, object reference recreated each render). | One logical node fragments into many ids, each with its own fetch and cache. | Key on the stable identifier only. |
| Under-keying | A distinguishing field is omitted (paginated list keyed on filter but not page). | Two distinct nodes collapse onto one id; the second read gets the first node’s result. | Put every field that defines a distinct instance in the key. |
See Args and deps for declaring args and the key function, and Dependencies for how one node’s id becomes another’s edge.
Next: Lifecycle — the states a node moves through once it has an id.