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 @id property.
  • 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:23

Batched patch payload tagged with a monotonically increasing version.

Properties

patches

patches: DeepPatch[]

Defined in: sdk/js/alien-deepsignals/src/types.ts:25

version

version: number

Defined in: sdk/js/alien-deepsignals/src/types.ts:24


DeepSignalOptions

Defined in: sdk/js/alien-deepsignals/src/types.ts:42

Internal

Options to pass to deepSignal

Properties

propGenerator?

optional propGenerator: DeepSignalPropGenFn

Defined in: sdk/js/alien-deepsignals/src/types.ts:47

An optional function that is called when new objects are attached and that may return additional properties to be attached.

readOnlyProps?

optional readOnlyProps: string[]

Defined in: sdk/js/alien-deepsignals/src/types.ts:60

Optional: Properties that are made read-only in objects. Can only be attached by propGenerator or must already be member of the new object before attaching it.

replaceProxiesInBranchOnChange?

optional replaceProxiesInBranchOnChange: boolean

Defined in: sdk/js/alien-deepsignals/src/types.ts:66

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?

optional subscriberFactories: Set<ExternalSubscriberFactory<any>>

Defined in: sdk/js/alien-deepsignals/src/types.ts:71

syntheticIdPropertyName?

optional syntheticIdPropertyName: string

Defined in: sdk/js/alien-deepsignals/src/types.ts:54

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:27

Properties

immediate?

optional immediate: boolean

Defined in: sdk/js/alien-deepsignals/src/watch.ts:29

True, if the callback should be run immediately after watch was called.

Default
false;
once?

optional once: boolean

Defined in: sdk/js/alien-deepsignals/src/watch.ts:31

True, if the watcher should be unsubscribed after the first event.

Default
false;
triggerInstantly?

optional triggerInstantly: boolean

Defined in: sdk/js/alien-deepsignals/src/watch.ts:38

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:41

Type Parameters

T

T extends object

Properties

newValue

newValue: DeepSignal<T>

Defined in: sdk/js/alien-deepsignals/src/watch.ts:47

The current value of the signal

patches

patches: DeepPatch[]

Defined in: sdk/js/alien-deepsignals/src/watch.ts:43

The changes made

version?

optional version: number

Defined in: sdk/js/alien-deepsignals/src/watch.ts:45

The version if triggerInstantly is not true.

Type Aliases

ComputedSignal

ComputedSignal<T> = ReturnType<typeof computed>

Defined in: sdk/js/alien-deepsignals/src/types.ts:150

Type Parameters

T

T = any


DeepPatch

DeepPatch = object & { op: "add"; type?: "object" | "set"; value?: any; } | { op: "remove"; type?: "set"; value?: any; }

Defined in: sdk/js/alien-deepsignals/src/types.ts:15

Deep mutation emitted from a deepSignal root.

Type Declaration

path

path: (string | number)[]


DeepSignal

DeepSignal<T> = T extends Function ? T : T extends string | number | boolean ? T : T extends DeepSignalObjectProps<any> | DeepSignalObjectProps<any>[] ? T : T extends infer I[] ? DeepSignal<I>[] : T extends Set<infer S> ? DeepSignalSet<S> : T extends object ? DeepSignalObject<T> : T

Defined in: sdk/js/alien-deepsignals/src/types.ts:228

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:242

Type Parameters

T

T extends object


DeepSignalPropGenFn()

DeepSignalPropGenFn = (props) => object

Defined in: sdk/js/alien-deepsignals/src/types.ts:106

Internal

The propGenerator function is called when a new object is added to the deep signal tree.

Parameters

props
inSet

boolean

Whether the object is being added to a Set (true) or not (false)

object

any

The newly added object itself

path

(string | number)[]

The path of the newly added object.

Returns

extraProps?

optional extraProps: Record<string, unknown>

Additional properties to be added to the object (overwriting existing ones).

syntheticId?

optional syntheticId: 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() },
  {
    propGenerator: ({ path, inSet, object }) => ({
      syntheticId: inSet
        ? `urn:item:${++counter}`
        : `urn:obj:${path.join("-")}`,
      extraProps: { createdAt: new Date().toISOString() },
    }),
    syntheticIdPropertyName: "@id",
  },
);

state.items.add({ name: "Item 1" });
// Attaches `{ name: "Item 1", `@id`: "urn:item:1", createdAt: <current date>`

state.foo = { bar: 42 };
// Attaches `{bar: 42, "@id": "urn:obj:foo", createdAt: <current date>}`

DeepSignalSet

DeepSignalSet<T> = DeepSignalSet_<T> & DeepSignalSetProps<T> & DeepSignalObjectProps<T>

Defined in: sdk/js/alien-deepsignals/src/types.ts:193

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:162

Utility functions for sets.

Type Parameters

T

T

Methods

first()

first(): T extends object ? DeepSignal<T> : T | undefined

Defined in: sdk/js/alien-deepsignals/src/types.ts:164

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:180

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.

getById()

getById(id): DeepSignal<T> | undefined

Defined in: sdk/js/alien-deepsignals/src/types.ts:171

Retrieve an entry from the Set by its synthetic set ID.

Parameters
id

The synthetic ID (string or number) assigned to the entry.

string | number

Returns

DeepSignal<T> | undefined

The proxied entry if found, undefined otherwise.


ExternalSubscriberFactory()

ExternalSubscriberFactory<T> = () => object

Defined in: sdk/js/alien-deepsignals/src/types.ts:74

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<typeof alienSignal>

Defined in: sdk/js/alien-deepsignals/src/types.ts:249

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:251

Union allowing value, writable signal, computed signal or plain getter function.

Type Parameters

T

T = any


RegisterCleanup()

RegisterCleanup = (cleanupFn) => void

Defined in: sdk/js/alien-deepsignals/src/watch.ts:25

Parameters

cleanupFn

() => void

Returns

void


SignalLike

SignalLike<T> = WritableSignal<T> | ComputedSignal<T>

Defined in: sdk/js/alien-deepsignals/src/types.ts:151

Type Parameters

T

T = any


UnwrapDeepSignal

UnwrapDeepSignal<T> = T extends DeepSignal<infer S> ? S : T

Defined in: sdk/js/alien-deepsignals/src/types.ts:246

Type Parameters

T

T


WatchPatchCallback()

WatchPatchCallback<T> = (event) => void

Defined in: sdk/js/alien-deepsignals/src/watch.ts:50

Type Parameters

T

T extends object

Parameters

event

WatchPatchEvent<T>

Returns

void

Variables

alienSignal()

const alienSignal: {<T>(): {(): T | undefined; (value): void; }; <T>(initialValue): {(): T; (value): void; }; } = alienSignal_

Defined in: sdk/js/alien-deepsignals/src/core.ts:99

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()

const computed: <T>(getter) => () => T = alienComputed

Defined in: sdk/js/alien-deepsignals/src/core.ts:94

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()

const effect: (fn) => () => void = alienEffect

Defined in: sdk/js/alien-deepsignals/src/core.ts:107

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

Functions

addWithId()

addWithId<T>(set, entry, id): T

Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1458

Convenience helper to add an entry to a proxied Set with a pre-defined synthetic ID.

Type Parameters

T

T

Parameters

set

Set<T>

entry

T

id

string | number

Returns

T


batch()

batch<T>(fn): T

Defined in: sdk/js/alien-deepsignals/src/core.ts:34

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

deepSignal()

deepSignal<T>(input, specialOptions?): DeepSignal<T>

Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1347

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.

specialOptions?

DeepSignalOptions

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.
  • For all objects: __raw__ which gives you the 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. If you use __raw__, that won’t work anymore.

Throws

if provided with unsupported input types.


getRaw()

getRaw<T>(value): any

Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1474

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 any

Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1314

Runtime guard that checks whether a value is a deepSignal proxy.

Parameters

value

unknown

Returns

value is any


shallow()

shallow<T>(obj): T

Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1445

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:1397

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

DeepPatchJITSubscriber | DeepPatchSubscriber

triggerInstantly?

boolean = false

Returns

(): void

Returns

void


watch()

watch<T>(source, callback, options?): object

Defined in: sdk/js/alien-deepsignals/src/watch.ts:89

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.

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","type":"object"},
//   {"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"}
// ]

Type Parameters

T

T extends object

Parameters

source

DeepSignalSet<T> | DeepSignalObject<T> | DeepSignal<T>

callback

WatchPatchCallback<T>

options?

WatchOptions = {}

Returns

object

registerCleanup

registerCleanup: RegisterCleanup

stopListening()

stopListening: () => void

Returns

void