TS Deep Signals
Deep structural reactivity for plain objects / arrays / Sets built on top of alien-signals.
Hooks for Svelte (5 and 4), Vue, and React.
Core idea: wrap a data tree in a Proxy that lazily creates per-property signals the first time you read them. Deep mutations emit batched patch objects (in a JSON-patch inspired style) that you can track with watch().
Features
- Lazy: signals & child proxies created only when touched.
- Deep: nested objects, arrays, Sets proxied.
- Patch stream: microtask‑batched granular mutations (paths + op) for syncing external stores / framework adapters.
- Getter => computed: property getters become derived (readonly) signals automatically.
- Sets:
add/delete/clear/...methods emit patches; object entries get synthetic stable ids. - Configurable synthetic IDs: custom property generator - the synthetic ID is used in the paths of patches to identify objects in sets. By default attached as
@idproperty. - Read-only properties: protect specific properties from modification.
- Shallow escape hatch: wrap sub-objects with
shallow(obj)to track only reference replacement.
Install
pnpm add @ng-org/alien-deepsignals
# or
npm i @ng-org/alien-deepsignals
Quick start
import { deepSignal } from "@ng-org/alien-deepsignals";
const state = deepSignal({
count: 0,
user: { name: "Ada" },
items: [{ id: "i1", qty: 1 }],
settings: new Set(["dark"]),
});
state.count++; // mutate normally
state.user.name = "Grace"; // nested write
state.items.push({ id: "i2", qty: 2 });
state.settings.add("beta");
Frontend Hooks
We provide hooks for Svelte 4, Svelte 5, Vue, and React so that you can use deepSignal objects in your frontend framework. Modifying the object within those components works as usual, just that the component will rerender automatically when the object changed (by a modification in the component or a modification from elsewhere).
Note that you can pass existing deepSignal objects to useDeepSignal (that you are using elsewhere too, for example as shared state) as well as plain JavaScript objects (which are then wrapped).
You can (and are often advised to) use deepSignals as a shared state (and sub objects thereof) across components.
React
import { useDeepSignal } from "@ng-org/alien-deepsignals/react";
import { DeepSignal } from "@ng-org/alien-deepsignals";
import UserComponent from "./User.tsx";
import type { User } from "./types.ts";
function UserManager() {
const users: DeepSignal<User[]> = useDeepSignal([{ username: "Bob" }]);
return users.map((user) => <UserComponent key={user.id} user={user} />);
}
In child component User.tsx:
function UserComponent({ user }: { user: DeepSignal<User> }) {
// Modifications here will trigger a re-render in the parent component
// which updates this component.
// For performance reasons, you are advised to call `useDeepSignal`
// close to where its return value is used.
return <input type="text" value={user.name} />;
}
Vue
In component UserManager.vue
<script setup lang="ts">
import { useDeepSignal } from "@ng-org/alien-deepsignals/vue";
import { DeepSignal } from "@ng-org/alien-deepsignals";
import UserComponent from "./User.vue";
import type { User } from "./types.ts";
const users: DeepSignal<User[]> = useDeepSignal([{ username: "Bob", id: 1 }]);
</script>
<template>
<UserComponent v-for="user in users" :key="user.id" :user="user" />
</template>
In a child component, User.vue
<script setup lang="ts">
const props = defineProps<{
user: DeepSignal<User>;
}>();
// The component only rerenders when user.name changes.
// It behaves the same as an object wrapped with `reactive()`
const user = props.user;
</script>
<template>
<input type="text" v-model:value="user.name" />
</template>
Svelte 4
import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte4";
// `users` is a store of type `{username: string}[]`
const users = useDeepSignal([{ username: "Bob" }]);
Svelte 5
import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte";
// `users` is a rune of type `{username: string}[]`
const users = useDeepSignal([{ username: "Bob" }]);
Other Frameworks
Integrating new frontend frameworks is fairly easy. Get in touch if you are interested.
Reference
Interfaces
DeepPatchBatch
Defined in: sdk/js/alien-deepsignals/src/types.ts:36
Batched patch payload tagged with a monotonically increasing version.
Properties
patches
patches:
DeepPatch[]
Defined in: sdk/js/alien-deepsignals/src/types.ts:38
version
version:
number
Defined in: sdk/js/alien-deepsignals/src/types.ts:37
DeepSignalOptions
Defined in: sdk/js/alien-deepsignals/src/types.ts:55
Internal
Options to pass to deepSignal
Properties
onObjectAttached?
optionalonObjectAttached:OnObjectAttachedFn
Defined in: sdk/js/alien-deepsignals/src/types.ts:62
An optional function that is called when an object, array, or set is attached to a (nested) signal object, set, or array. The raw object may be modified or replaced by the function. For sets, a synthetic id may be specified that is used for creating the patches in the watch callback. Will also be called on the root object.
readOnlyProps?
optionalreadOnlyProps:string[]
Defined in: sdk/js/alien-deepsignals/src/types.ts:75
Optional: Properties that are made read-only in objects.
Can only be attached by onObjectAttached callback or must already be set
on the new object before attaching it.
replaceProxiesInBranchOnChange?
optionalreplaceProxiesInBranchOnChange:boolean
Defined in: sdk/js/alien-deepsignals/src/types.ts:81
If set to true, all proxies in the branch to a modified nested property are replaced.
This has no effect except for equality checks (===). This is necessary for react to notice the change.
Default
false;
subscriberFactories?
optionalsubscriberFactories:Set<ExternalSubscriberFactory<any>>
Defined in: sdk/js/alien-deepsignals/src/types.ts:86
syntheticIdPropertyName?
optionalsyntheticIdPropertyName:string
Defined in: sdk/js/alien-deepsignals/src/types.ts:69
The property name which should be used as an object identifier in sets.
You will see it when patches are generated with a path to an object in a set.
The syntheticId will be a patch element then.
Objects with existing properties matching syntheticIdPropertyName keep their values (not overwritten).
WatchOptions
Defined in: sdk/js/alien-deepsignals/src/watch.ts:28
Properties
immediate?
optionalimmediate:boolean
Defined in: sdk/js/alien-deepsignals/src/watch.ts:30
True, if the callback should be run immediately after watch was called.
Default
false;
once?
optionalonce:boolean
Defined in: sdk/js/alien-deepsignals/src/watch.ts:32
True, if the watcher should be unsubscribed after the first event.
Default
false;
triggerInstantly?
optionaltriggerInstantly:boolean
Defined in: sdk/js/alien-deepsignals/src/watch.ts:39
If true, triggers watch callback instantly after changes to the signal object. Otherwise, changes are batched and the watch callback is triggered in a microtask. This is useful for frontends like React where modifications on the changed input in a separate (microtask) will cause the cursor in input elements to reset.
WatchPatchEvent
Defined in: sdk/js/alien-deepsignals/src/watch.ts:42
Type Parameters
T
T
Properties
newValue
newValue:
DeepSignal<T>
Defined in: sdk/js/alien-deepsignals/src/watch.ts:48
The current value of the signal
patches
patches:
DeepPatch[]
Defined in: sdk/js/alien-deepsignals/src/watch.ts:44
The changes made
version?
optionalversion:number
Defined in: sdk/js/alien-deepsignals/src/watch.ts:46
The version if triggerInstantly is not true.
Type Aliases
ComputedSignal
ComputedSignal<
T> =ReturnType<typeofcomputed>
Defined in: sdk/js/alien-deepsignals/src/types.ts:178
Type Parameters
T
T = any
DeepPatch
DeepPatch =
object& {op:"add";type?:"set";value?:any; } | {op:"remove";type?:"set";value:any; }
Defined in: sdk/js/alien-deepsignals/src/types.ts:17
Deep mutation emitted from a deepSignal root.
Type Declaration
path
path: (
string|number|symbol)[]
DeepSignal
DeepSignal<
T> =TextendsFunction?T:Textendsstring|number|boolean?T:TextendsDeepSignalObjectProps<any> |DeepSignalObjectProps<any>[] ?T:Textends infer I[] ?DeepSignal<I>[] :TextendsSet<infer S> ?DeepSignalSet<S> :Textendsobject?DeepSignalObject<T> :T
Defined in: sdk/js/alien-deepsignals/src/types.ts:260
The object returned by the deepSignal function.
It is decorated with utility functions for sets, see DeepSignalSetProps
and a __raw__ prop to get the underlying non-reactive object.
Type Parameters
T
T
DeepSignalObject
DeepSignalObject<
T> ={ [K in keyof T]: DeepSignal<T[K]> }
Defined in: sdk/js/alien-deepsignals/src/types.ts:274
Type Parameters
T
T extends object
DeepSignalSet
DeepSignalSet<
T> =DeepSignalSet_<T> &DeepSignalSetProps<T> &DeepSignalObjectProps<T>
Defined in: sdk/js/alien-deepsignals/src/types.ts:225
Type alias for DeepSignal<Set<T>> and reactive Set wrapper that accepts raw or proxied entries.
Additionally it is decorated with DeepSignalSetProps
and iterator utilities like .map(), .filter(), .some(), …
Note that you can assign plain Sets to properties with type DeepSignalSet,
however Typescript will give you a warning. That is a limitation of TypeScript’s capability.
Internally, the object will be converted to a DeepSignalSet.
You can instruct TypeScript to ignore this with parent.children = new Set() as DeepSignal<Set<any>>.
Type Parameters
T
T
DeepSignalSetProps
DeepSignalSetProps<
T> =object
Defined in: sdk/js/alien-deepsignals/src/types.ts:190
Utility functions for sets.
Type Parameters
T
T
Methods
first()
first():
Textendsobject?DeepSignal<T> :T|undefined
Defined in: sdk/js/alien-deepsignals/src/types.ts:192
Get the element that was first inserted into the set.
Returns
T extends object ? DeepSignal<T> : T | undefined
getBy()
getBy(
graphIri,subjectIri):DeepSignal<T> |undefined
Defined in: sdk/js/alien-deepsignals/src/types.ts:212
Retrieve an object from the Set by its @graph and @id.
Parameters
graphIri
string
The @graph NURI of the object.
subjectIri
string
The @subject IRI of the object.
Returns
DeepSignal<T> | undefined
The proxied entry if found, undefined otherwise.
ExternalSubscriberFactory()
ExternalSubscriberFactory<
T> = () =>object
Defined in: sdk/js/alien-deepsignals/src/types.ts:89
Type Parameters
T
T = any
Returns
object
onGet()
onGet: () =>
void
Returns
void
onSet()
onSet: (
newVal) =>void
Parameters
newVal
T
Returns
void
MaybeSignal
MaybeSignal<
T> =T|ReturnType<typeofalienSignal>
Defined in: sdk/js/alien-deepsignals/src/types.ts:281
Union allowing a plain value or a writable signal wrapping that value.
Type Parameters
T
T = any
MaybeSignalOrComputed
MaybeSignalOrComputed<
T> =MaybeSignal<T> | () =>T
Defined in: sdk/js/alien-deepsignals/src/types.ts:283
Union allowing value, writable signal, computed signal or plain getter function.
Type Parameters
T
T = any
OnObjectAttachedFn()
OnObjectAttachedFn = (
props) =>void|undefined| {replaceWith?:any;syntheticId?:string|number; }
Defined in: sdk/js/alien-deepsignals/src/types.ts:125
The onObjectAttached function is called when an object is added to the deep signal tree.
Parameters
props
path
(string | number | symbol)[]
The path of the newly added object. In case of sets, the synthetic id is not part of the path.
rawObject
Record<string, any> | Set<any> | any[]
The newly added, non-proxied raw object. You may modify it.
rawParent
Set<any> | Record<string, any> | any[]
The parent object, set, or array that rawObject is being added to.
Returns
void
undefined
{ replaceWith?: any; syntheticId?: string | number; }
replaceWith?
optionalreplaceWith:any
By returning a replaceObject, you can intercept what is added to the raw object. The original object won’t be used. The value may be a signal in which case, the underlying raw object is attached.
If left undefined / unset, the original object is attached (which you may still modify).
syntheticId?
optionalsyntheticId:string|number
A custom identifier for the object (used in Set entry paths and optionally as a property).
Example
let counter = 0;
const state = deepSignal(
{ items: new Set() },
{
onObjectAttached: ({ path, inSet, object }) => {
return {
syntheticId: inSet ? `urn:item:${++counter}` : undefined,
replaceWith: {
createdAt: new Date().toISOString(),
name: object.name,
bar: object.bar,
},
};
},
},
);
state.items.add({ name: "Item 1", ignoredProp: "won't be added" });
// Attaches `{ name: "Item 1", createdAt: <current date>}`
state.foo = { bar: 42 };
// Attaches `{bar: 42, createdAt: <current date>}`
ReadOnlyArray
ReadOnlyArray<
T> =Omit<T[],ModifyingArrayFns> &object
Defined in: sdk/js/alien-deepsignals/src/types.ts:291
An array that does not allow modifications. You can modify it’s values but adding, moving, or removing elements is not allowed.
You can generate a ReadOnlyArray using readOnlyArray.
Type Declaration
length
length:
number
Gets the length of the array. This is a number one higher than the highest index in the array.
Type Parameters
T
T
RegisterCleanup()
RegisterCleanup = (
cleanupFn) =>void
Defined in: sdk/js/alien-deepsignals/src/watch.ts:26
Parameters
cleanupFn
() => void
Returns
void
SignalLike
SignalLike<
T> =WritableSignal<T> |ComputedSignal<T>
Defined in: sdk/js/alien-deepsignals/src/types.ts:179
Type Parameters
T
T = any
UnwrapDeepSignal
UnwrapDeepSignal<
T> =TextendsDeepSignal<infer S> ?S:T
Defined in: sdk/js/alien-deepsignals/src/types.ts:278
Type Parameters
T
T
WatchPatchCallback()
WatchPatchCallback<
T> = (event) =>void
Defined in: sdk/js/alien-deepsignals/src/watch.ts:51
Type Parameters
T
T
Parameters
event
Returns
void
Variables
alienSignal()
constalienSignal: {<T>(): {():T|undefined; (value):void; }; <T>(initialValue): {():T; (value):void; }; } =alienSignal_
Defined in: sdk/js/alien-deepsignals/src/core.ts:100
Re-export of alien-signals signal function which creates a basic signal.
Call Signature
<
T>(): {():T|undefined; (value):void; }
Type Parameters
T
T
Returns
():
T|undefined
Returns
T | undefined
(
value):void
Parameters
value
T | undefined
Returns
void
Call Signature
<
T>(initialValue): {():T; (value):void; }
Type Parameters
T
T
Parameters
initialValue
T
Returns
():
T
Returns
T
(
value):void
Parameters
value
T
Returns
void
computed()
constcomputed: <T>(getter) => () =>T=alienComputed
Defined in: sdk/js/alien-deepsignals/src/core.ts:95
Re-export of alien-signals computed function.
Use the computed() function to create lazy derived signals that automatically
track their dependencies and recompute only when needed.
Key features:
- Lazy evaluation: The computation runs only when you actually read the computed value.
If you never access
fullName(), the concatenation never happens—no wasted CPU cycles. - Automatic caching: Once computed, the result is cached until a dependency changes. Multiple reads return the cached value without re-running the getter.
- Fine-grained reactivity: Only recomputes when its tracked dependencies change. Unrelated state mutations don’t trigger unnecessary recalculation.
- Composable: Computed signals can depend on other computed signals, forming efficient dependency chains.
Type Parameters
T
T
Parameters
getter
(previousValue?) => T
Returns
():
T
Returns
T
Example
import { computed } from "@ng-org/alien-deepsignals";
const state = deepSignal({
firstName: "Ada",
lastName: "Lovelace",
items: [1, 2, 3],
});
// Create a computed signal that derives from reactive state
const fullName = computed(() => `${state.firstName} ${state.lastName}`);
console.log(fullName()); // "Ada Lovelace" - computes on first access
state.firstName = "Grace";
console.log(fullName()); // "Grace Lovelace" - recomputes automatically
// Expensive computation only runs when accessed and dependencies change
const expensiveResult = computed(() => {
console.log("Computing...");
return state.items.reduce((sum, n) => sum + n * n, 0);
});
// No computation happens yet!
state.items.push(4);
// Still no computation...
console.log(expensiveResult()); // "Computing..." + result
console.log(expensiveResult()); // Cached, no log
state.items.push(5);
console.log(expensiveResult()); // "Computing..." again (dependency changed)
effect()
consteffect: (fn) => () =>void=alienEffect
Defined in: sdk/js/alien-deepsignals/src/core.ts:108
Re-export of alien-signals effect function.
Callback reruns on every signal modification that is used within its callback.
Parameters
fn
() => void
Returns
():
void
Returns
void
effectScope()
consteffectScope: (fn) => () =>void=alienEffectScope
Defined in: sdk/js/alien-deepsignals/src/core.ts:113
Re-export of alien-signals effectScope function.
Parameters
fn
() => void
Returns
():
void
Returns
void
RAW_KEY
constRAW_KEY:"__raw__"
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:88
sealAllProps
constsealAllProps: typeofsealAllProps
Defined in: sdk/js/alien-deepsignals/src/sealedSignal.ts:98
Property symbol which indicates that all properties of a seal are read-only.
Functions
addWithId()
addWithId<
T>(set,entry,id):T
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1409
Convenience helper to add an entry to a proxied Set with a pre-defined synthetic ID. If the entry is already present, updates the id.
Type Parameters
T
T extends object | any[] | Set<any>
Parameters
set
Set<T> | DeepSignalSet<T>
entry
T
id
string | number | symbol
Returns
T
batch()
batch<
T>(fn):T
Defined in: sdk/js/alien-deepsignals/src/core.ts:35
Execute multiple signal writes in a single batched update frame. All downstream computed/effect re-evaluations are deferred until the function exits.
IMPORTANT: The callback must be synchronous. If it returns a Promise the batch will still end immediately after scheduling, possibly causing mid-async flushes.
Type Parameters
T
T
Parameters
fn
() => T
Returns
T
Example
batch(() => {
count(count() + 1);
other(other() + 2);
}); // effects observing both run only once
createSeal()
createSeal<
T>(value,disallowedProps):DeepSignal<T>
Defined in: sdk/js/alien-deepsignals/src/sealedSignal.ts:55
Function to create a seal on a deep signal.
The properties listed in propertyNames are not modifiable by the returned object.
This includes the property name at any level. If an object is sealed, all its properties
are as well.
If all props should be sealed, set disallowedProps to [sealAllProps]
Type Parameters
T
T
Parameters
value
DeepSignal<T>
The deep signal object to seal properties of.
disallowedProps
(string | symbol | (string | symbol)[])[]
The property names to seal. Nested properties are sealed as well. If an array, it’s the path to a property.
Returns
DeepSignal<T>
Example
const myReactiveObject = deepSignal({
modifiableRootProp: "value",
sealedRootProp: "I'm sealed.",
sealedEverywhere: "I'm sealed everywhere",
sealedSet: new Set([1,2,3]),
child: {
modifiableChildProp: "I'm modifiable",
sealedEverywhere: "I'm sealed everywhere",
sealedChildProp: "I'm sealed."
childChild: {childChildProp: "I'm sealed because childChild is."}
}});
const sealedObj = createSeal([
"sealedEverywhere",
["sealedRootProp"],
["child", "sealedChildProp"],
["child", "childChild"],
]);
// Allowed:
sealedObj.modifiableRootProp = "updated root prop";
sealedObj.child.modifiableChildProp = "updated child prop";
// Returns `true`.
sealedObj.sealedSet.has(2);
// Throws "Cannot set value on sealed property".
sealedObj.sealedEverywhere = "illegal modification";
// Throws "Cannot set value on sealed property".
sealedObj.child.childChild.childChildProp = "illegal modification";
// Throws "Sets of sealed deep signal objects only expose non-mutating functions".
sealedObj.sealedSet.add(4);
deepSignal()
deepSignal<
T>(input,options?):DeepSignal<T>
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1294
MAIN ENTRY POINT to create a deep reactive proxy for objects, arrays or Sets.
If input is a deepSignal already and options are provided,
the added subscriberFactories are joined with the existing ones
and replaceProxiesInBranchOnChange is or-ed with the current value.
Type Parameters
T
T extends object
Parameters
input
T
An object that you want to use as reactive, deepSignal object. If the input object is a DeepSignal object already, returns the same object.
options?
Additional configuration options.
Returns
DeepSignal<T>
A DeepSignal object.
You can use the returned DeepSignal as you would use your input object.
You do not have to to wrap your type definitions in DeepSignal<>.
Nevertheless, it can be instructive for TypeScript to show you the
additional utilities that DeepSignal objects expose. Also, it might keep
you aware that modifications you make to those objects are persisted and
can update the frontend.
The utilities that DeepSignal objects include are:
- For sets:
- iterator helper methods (e.g.
map(),filter(),reduce(),any(), …) first()to get one element from the set — useful if you know that there is only one.getBy(graphNuri: string, subjectIri: string), to find objects by their graph NURI and subject IRI.- NOTE: When assigning a set to
DeepSignal<Set>, TypeScript will warn you. You can safely ignore this by writing (parent.children = new Set() as DeepSignal<Set<any>>). Internally, the set is automatically converted but this is not expressible in TypeScript.
- iterator helper methods (e.g.
- For all objects: RAW_KEY which gives you the underlying non-proxied object without tracking value access and without triggering updates upon modifications. Tracking value access is used in the frontend so it knows on what changes to refresh.
Throws
if provided with unsupported input types.
getRaw()
getRaw<
T>(value):any
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1437
Get the original, raw value of a deep signal.
Type Parameters
T
T extends object
Parameters
value
T | DeepSignal<T>
Returns
any
isDeepSignal()
isDeepSignal(
value): value is DeepSignal<object | any[] | Set<any>>
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1259
Runtime guard that checks whether a value is a deepSignal proxy.
Parameters
value
unknown
Returns
value is DeepSignal<object | any[] | Set<any>>
readOnlyArray()
readOnlyArray<
T>(array):ReadOnlyArray<T>
Defined in: sdk/js/alien-deepsignals/src/readOnlyArray.ts:45
Create a non-modifiable array from a normal array. Everything is allowed except for adding, moving or removing elements.
The returned proxy does not expose mutating functions like push() and will throw
if you try to get them.
NOTE: It does not prevent modifications to its children.
Type Parameters
T
T
Parameters
array
DeepSignal<T[]>
Returns
shallow()
shallow<
T>(obj):T
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1400
Mark an object so deepSignal skips proxying it (shallow boundary).
Type Parameters
T
T extends object
Parameters
obj
T
Returns
T
Example
import { shallow } from "alien-deepsignals";
state.config = shallow({ huge: { blob: true } });
subscribeDeepMutations()
subscribeDeepMutations(
root,cb,triggerInstantly?): () =>void
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1352
Low-level function, you should probably use watch instead.
Register a deep mutation subscriber for the provided root or proxy.
Parameters
root
symbol | object
cb
DeepPatchSubscriber | DeepPatchJITSubscriber
triggerInstantly?
boolean = false
Returns
():
void
Returns
void
watch()
watch<
T>(source,callback,options?):object
Defined in: sdk/js/alien-deepsignals/src/watch.ts:94
Watch for changes to a deepSignal.
Whenever a change is made, callback is called with the patches describing the change and the new value.
If you set triggerInstantly, the callback is called on every property change.
If not, all changes are aggregated and callback is called in a microtask when
the current task finishes, e.g. await is called (meaning it supports batching).
When objects are added to Sets, their synthetic ID (usually @id) becomes part of the patch path. This allows patches to uniquely identify which Set entry is being mutated.
When you do not need need the patches but only want to be called back on object changes that you depend on, you are advised to use effect instead.
Note: If you attach an existing signal object, you won’t see the changes made on the child signal object by watching the root.
All you will see is an initial add patch with the signal object as value. Watch the nested object separately.
Type Parameters
T
T extends unknown
Parameters
source
T
callback
options?
WatchOptions = {}
Returns
object
registerCleanup
registerCleanup:
RegisterCleanup
stopListening()
stopListening: () =>
void
Returns
void
Example
const state = deepSignal(
{ s: new Set() },
{ ...}
);
watch(state, ({ patches }) => {
console.log(JSON.stringify(patches));
});
state.s.add({ data: "test" });
// Will log:
// [
// {"path":["s","did:ng:o:123"],"op":"add"},
// {"path":["s","did:ng:o:123","@id"],"op":"add","value":"did:ng:o:123"},
// {"path":["s","did:ng:o:123","data"],"op":"add","value":"test"}
// ]
state.s.getById("did:ng:o:123")!.data = "new value"
// Will log:
// [
// {"path":["s","did:ng:o:123","data"],"op":"add","value":"new value"}
// ]