Compare commits

...

No commits in common. '7dbc947abd7596a10bc7625a0443417acefe6403' and '400ba719b50530e3fc08a7867568c97d4d63fa47' have entirely different histories.

  1. 54
      .gitignore
  2. 170
      README.md
  3. 11
      astro.config.mjs
  4. 9410
      package-lock.json
  5. 80
      package.json
  6. 79
      playwright.config.ts
  7. 2200
      pnpm-lock.yaml
  8. 42
      src/app/components/Highlight.astro
  9. 7
      src/app/components/ReactRoot.tsx
  10. 5
      src/app/components/SvelteRoot.svelte
  11. 9
      src/app/components/VueRoot.vue
  12. 21
      src/app/layouts/Layout.astro
  13. 24
      src/app/pages/index.astro
  14. 3
      src/app/store/state.ts
  15. 5
      src/contents.ts
  16. 163
      src/core.ts
  17. 810
      src/deepSignal.ts
  18. 26
      src/frontends/react/HelloWorld.tsx
  19. 19
      src/frontends/svelte/HelloWorld.svelte
  20. 10
      src/frontends/tests/reactiveCrossFramework.spec.ts
  21. 29
      src/frontends/vue/HelloWorld.vue
  22. 17
      src/index.ts
  23. 9
      src/ng-mock/js-land/connector/applyDiff.ts
  24. 84
      src/ng-mock/js-land/connector/createSignalObjectForShape.ts
  25. 22
      src/ng-mock/js-land/frontendAdapters/react/useShape.ts
  26. 16
      src/ng-mock/js-land/frontendAdapters/svelte/useShape.ts
  27. 14
      src/ng-mock/js-land/frontendAdapters/vue/useShape.ts
  28. 14
      src/ng-mock/js-land/types.ts
  29. 0
      src/ng-mock/tests/subscriptionToStore.test.ts
  30. 30
      src/ng-mock/tests/updatesWithWasm.test.ts
  31. 53
      src/ng-mock/wasm-land/requestShape.ts
  32. 10
      src/ng-mock/wasm-land/shapeManager.ts
  33. 17
      src/ng-mock/wasm-land/types.ts
  34. 22
      src/ng-mock/wasm-land/updateShape.ts
  35. 81
      src/test/core.test.ts
  36. 1119
      src/test/index.test.ts
  37. 47
      src/test/patchOptimized.test.ts
  38. 148
      src/test/tier3.test.ts
  39. 91
      src/test/watch.test.ts
  40. 336
      src/test/watchPatches.test.ts
  41. 41
      src/utils.ts
  42. 188
      src/watch.ts
  43. 32
      src/watchEffect.ts
  44. 5
      svelte.config.js
  45. 27
      tsconfig.json
  46. 14
      tsup.config.ts

54
.gitignore vendored

@ -1,42 +1,24 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
# Logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
node_modules
dist
dist-ssr
*.local
# IntelliJ based IDEs
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
# Finder (MacOS) folder config
.DS_Store
*.suo
*.sln
*.sw?
.astro
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
coverage

@ -1,4 +1,168 @@
# Multi-Framework Signal Proxies
# alien-deepsignals
Deep structural reactivity for plain objects / arrays / Sets built on top of `alien-signals`.
Core idea: wrap a data tree in a `Proxy` that lazily creates per-property signals the first time you read them. Accessing a property returns the plain value; accessing `$prop` returns the underlying signal function. Deep mutations emit compact batched patch objects you can observe with `watch()`.
## Features
* Lazy: signals & child proxies created only when touched.
* Deep: nested objects, arrays, Sets proxied on demand.
* [ ] TODO: Methods might not be proxied (e.g. array.push)?
* Per-property signals: fine‑grained invalidation without traversal on each change.
* Patch stream: microtask‑batched granular mutations (paths + op) for syncing external stores / framework adapters.
* Getter => computed: property getters become derived (readonly) signals automatically.
* `$` accessors: TypeScript exposes `$prop` for each non‑function key plus `$` / `$length` for arrays.
* Sets: structural `add/delete/clear` emit patches; object entries get synthetic stable ids (prefers `id` / `@id` fields or auto‑generated blank IDs).
* Shallow escape hatch: wrap sub-objects with `shallow(obj)` to track only reference replacement.
## Install
```bash
pnpm add alien-deepsignals
# or
npm i alien-deepsignals
```
## Quick start
```ts
import { deepSignal } from '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')
// Direct signal access
state.$count!.set(5) // update via signal
console.log(state.$count!()) // read via signal function
```
## Watching patches
`watch(root, cb, options?)` observes a deepSignal root and invokes your callback with microtask‑batched mutation patches plus snapshots.
```ts
import { watch } from 'alien-deepsignals'
const stop = watch(state, ({ patches, oldValue, newValue }) => {
for (const p of patches) {
console.log(p.op, p.path.join('.'), 'value' in p ? p.value : p.type)
}
})
state.user.name = 'Lin'
state.items[0].qty = 3
await Promise.resolve() // flush microtask
stop()
```
### Callback event shape
```ts
type WatchPatchEvent<T> = {
patches: DeepPatch[] // empty only on immediate
oldValue: T | undefined // deep-cloned snapshot before batch
newValue: T // live proxy (already mutated)
registerCleanup(fn): void // register disposer for next batch/stop
stopListening(): void // unsubscribe
}
```
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `immediate` | boolean | false | Fire once right away with `patches: []`. |
| `once` | boolean | false | Auto stop after first callback (immediate counts). |
`observe()` is an alias of `watch()`.
## DeepPatch format
```ts
type DeepPatch = {
root: symbol // stable id per deepSignal root
path: (string | number)[] // root-relative segments
} & (
| { op: 'add'; type: 'object' } // assigned object/array/Set entry object
| { op: 'add'; value: string | number | boolean } // primitive write
| { op: 'remove' } // deletion
| { op: 'add'; type: 'set'; value: [] } // Set.clear()
| { op: 'add'; type: 'set'; value: (string|number|boolean)[] | { [id: string]: object } } // (reserved)
)
```
Notes:
* `type:'object'` omits value to avoid deep cloning; read from `newValue` if needed.
* `Set.add(entry)` emits object vs primitive form depending on entry type; path ends with synthetic id.
* `Set.clear()` emits one structural patch and suppresses per‑entry removals in same batch.
## Sets & synthetic ids
Object entries inside Sets need a stable key. Priority:
1. `entry.id`
2. `entry['@id']`
3. Custom via `setSetEntrySyntheticId(entry, 'myId')` before `add`
4. Auto `_bN` blank id
Helpers:
```ts
import { addWithId, setSetEntrySyntheticId } from 'alien-deepsignals'
setSetEntrySyntheticId(obj, 'custom')
state.settings.add(obj)
addWithId(state.settings as any, { x:1 }, 'x1')
```
## Shallow
Skip deep proxying of a subtree (only reference replacement tracked):
```ts
import { shallow } from 'alien-deepsignals'
state.config = shallow({ huge: { blob: true } })
```
## TypeScript ergonomics
`DeepSignal<T>` exposes both plain properties and optional `$prop` signal accessors (excluded for function members). Arrays add `$` (index signal map) and `$length`.
```ts
const state = deepSignal({ count: 0, user: { name: 'A' } })
state.count++ // ok
state.$count!.set(9) // write via signal
const n: number = state.$count!() // typed number
```
## API surface
| Function | Description |
|----------|-------------|
| `deepSignal(obj)` | Create (or reuse) reactive deep proxy. |
| `watch(root, cb, opts?)` | Observe batched deep mutations. |
| `observe(root, cb, opts?)` | Alias of `watch`. |
| `peek(obj,key)` | Untracked property read. |
| `shallow(obj)` | Mark object to skip deep proxying. |
| `isDeepSignal(val)` | Runtime predicate. |
| `isShallow(val)` | Was value marked shallow. |
| `setSetEntrySyntheticId(obj,id)` | Assign custom Set entry id. |
| `addWithId(set, entry, id)` | Insert with desired synthetic id. |
| `subscribeDeepMutations(root, cb)` | Low-level patch stream (used by watch). |
## Credits
Inspired by [deepsignal](https://github.com/luisherranz/deepsignal) – thanks to @luisherranz. Re-imagined with patch batching & Set support.
## License
MIT
Thanks for providing the basic multi-framework template:
https://github.com/aleksadencic/multi-framework-app

@ -1,11 +0,0 @@
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import vue from "@astrojs/vue";
import svelte from "@astrojs/svelte";
// https://astro.build/config
export default defineConfig({
integrations: [react(), vue(), svelte()],
srcDir: "./src/app",
});

9410
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,43 +1,53 @@
{
"name": "multi-framework-signal-proxies",
"name": "alien-deepsignals",
"version": "0.1.0",
"private": true,
"type": "module",
"private": false,
"author": "CCherry07",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"description": "AlienDeepSignals 🧶 -alien signals, but using regular JavaScript objects",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"test": "vitest",
"test:e2e": "playwright test"
"test": "vitest --coverage",
"dev": "tsup --watch src",
"build": "tsup",
"release": "bumpp && npm run build && npm publish --registry=https://registry.npmjs.org/"
},
"dependencies": {
"@astrojs/react": "4.3.0",
"@astrojs/svelte": "7.1.0",
"@astrojs/vue": "^5.1.0",
"@gn8/alien-signals-react": "^0.1.1",
"@gn8/alien-signals-solid": "^0.1.1",
"@gn8/alien-signals-svelte": "^0.1.1",
"@gn8/alien-signals-vue": "^0.1.1",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"alien-deepsignals": "^0.1.0",
"alien-signals": "^2.0.7",
"astro": "5.13.2",
"install": "^0.13.0",
"npm": "^11.5.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"svelte": "5.38.2",
"vue": "3.5.19"
"alien-signals": "^2.0.7"
},
"devDependencies": {
"@playwright/test": "^1.55.0",
"@types/node": "24.3.0",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"vite": "7.1.3",
"vitest": "^3.2.4"
}
"@types/node": "^22.10.9",
"@vitest/coverage-v8": "3.0.2",
"bumpp": "^9.9.2",
"tsup": "^8.3.5",
"typescript": "^5.4.3",
"vitest": "^3.0.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/CCherry07/alien-deepsignals.git"
},
"keywords": [
"signal",
"signals",
"deepsignals",
"alien-signals",
"alien-deepsignals"
],
"bugs": {
"url": "https://github.com/CCherry07/alien-deepsignals/issues"
},
"homepage": "https://github.com/CCherry07/alien-deepsignals#readme",
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387"
}

@ -1,79 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./src/frontends/tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:4321",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev",
url: "http://localhost:4321",
reuseExistingServer: !process.env.CI,
},
});

File diff suppressed because it is too large Load Diff

@ -1,42 +0,0 @@
---
const { react, svelte, vue, astro } = Astro.props;
const frameworkName = Object.keys(Astro.props)[0];
---
<div class:list={["box", { react, svelte, vue, astro }]}>
<p class="title">{frameworkName}</p>
<div class="wrap">
<slot />
</div>
</div>
<style>
.box {
border: 2px solid rgb(88, 88, 81);
border-radius: 1em;
padding: 1em;
margin-top: 1em;
margin-left: auto;
margin-right: auto;
max-width: 60%;
}
.wrap {
padding: 10px;
}
.title {
background: var(--highlightColor);
padding: 0 10px;
}
.vue {
background-color: rgb(255, 212, 255);
}
.react {
background-color: rgb(189, 216, 255);
}
.svelte {
background-color: rgb(255, 216, 189);
}
</style>

@ -1,7 +0,0 @@
import { HelloWorldReact } from "src/frontends/react/HelloWorld";
const Root = () => {
return <HelloWorldReact />;
};
export default Root;

@ -1,5 +0,0 @@
<script lang="ts">
import HelloWorld from "src/frontends/svelte/HelloWorld.svelte";
</script>
<HelloWorld />

@ -1,9 +0,0 @@
<script setup lang="ts">
import HelloWorld from 'src/frontends/vue/HelloWorld.vue';
//
</script>
<template>
<HelloWorld></HelloWorld>
</template>

@ -1,21 +0,0 @@
---
import Highlight from "../components/Highlight.astro";
---
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Multi-Framework Signal Experiments</title>
</head>
<body>
<Highlight astro>
<header class="container">Multi-Framework Signal Experiments</header>
<main class="container">
<slot />
</main>
</Highlight>
</body>
</html>

@ -1,24 +0,0 @@
---
import Layout from "../layouts/Layout.astro";
import Highlight from "../components/Highlight.astro";
import VueRoot from "../components/VueRoot.vue";
import ReactRoot from "../components/ReactRoot";
import SvelteRoot from "../components/SvelteRoot.svelte";
const title = "Multi-framework app";
---
<Layout title={title}>
<Highlight vue>
<VueRoot client:only />
</Highlight>
<Highlight react>
<ReactRoot client:only="react" />
</Highlight>
<Highlight svelte>
<SvelteRoot client:only />
</Highlight>
</Layout>

@ -1,3 +0,0 @@
import { atom } from "nanostores";
export const selectedFilters = atom<string[]>([]);

@ -0,0 +1,5 @@
export enum ReactiveFlags {
IS_SIGNAL = '__v_isSignal',
SKIP = "__v_skip",
IS_SHALLOW = "__v_isShallow",
}

@ -0,0 +1,163 @@
/** Lightweight façade adding ergonomic helpers (.value/.peek/.get/.set) to native alien-signals function signals. */
// Native re-exports for advanced usage.
export {
signal as _rawSignal,
computed as _rawComputed,
effect,
startBatch,
endBatch,
getCurrentSub,
setCurrentSub,
} from "alien-signals";
import {
signal as alienSignal,
computed as alienComputed,
effect as alienEffect,
startBatch as alienStartBatch,
endBatch as alienEndBatch,
} from "alien-signals";
import { ReactiveFlags as ReactiveFlags_ } from "./contents";
import { isFunction } from "./utils";
// Nominal constructor removal: we no longer expose classes; signals are plain tagged functions.
/** Internal shape of a tagged writable signal after adding ergonomic helpers. */
type TaggedSignal<T> = ReturnType<typeof alienSignal<T>> & {
/** Tracking read / write via property syntax */
value: T;
/** Non-tracking read */
peek(): T;
/** Alias for tracking read */
get(): T;
/** Write helper */
set(v: T): void;
};
/**
* Decorate a native signal function with legacy helpers & identity.
*/
function tagSignal(fn: any): TaggedSignal<any> {
Object.defineProperty(fn, ReactiveFlags_.IS_SIGNAL, { value: true });
Object.defineProperty(fn, "value", {
get: () => fn(),
set: (v) => fn(v),
});
// Add peek to mirror old API (non-tracking read)
if (!fn.peek) Object.defineProperty(fn, "peek", { value: () => fn() });
if (!fn.get) Object.defineProperty(fn, "get", { value: () => fn() });
if (!fn.set) Object.defineProperty(fn, "set", { value: (v: any) => fn(v) });
return fn;
}
/**
* Decorate a native computed function similarly (readonly value accessor).
*/
function tagComputed(fn: any) {
Object.defineProperty(fn, ReactiveFlags_.IS_SIGNAL, { value: true });
Object.defineProperty(fn, "value", { get: () => fn() });
if (!fn.peek) Object.defineProperty(fn, "peek", { value: () => fn() });
if (!fn.get) Object.defineProperty(fn, "get", { value: () => fn() });
return fn;
}
/**
* Create a new writable function-form signal enhanced with `.value`, `.peek()`, `.get()`, `.set()`.
*
* @example
* const count = signal(0);
* count(); // 0 (track)
* count(1); // write
* count.value; // 1 (track)
* count.peek(); // 1 (non-tracking)
*/
export const signal = <T>(v?: T) => tagSignal(alienSignal(v));
/**
* Create a lazy computed (readonly) signal derived from other signals.
* The returned function is tagged with `.value` and `.peek()` for convenience.
*/
export const computed = <T>(getter: () => T) =>
tagComputed(alienComputed(getter));
/** Union allowing a plain value or a writable signal wrapping that value. */
export type MaybeSignal<T = any> = T | ReturnType<typeof signal>;
/** Union allowing value, writable signal, computed signal or plain getter function. */
export type MaybeSignalOrGetter<T = any> =
| MaybeSignal<T>
| ReturnType<typeof computed>
| (() => T);
/** Runtime guard that an unknown value is one of our tagged signals/computeds. */
export const isSignal = (s: any): boolean =>
typeof s === "function" && !!s && !!s[ReactiveFlags_.IS_SIGNAL];
/**
* Minimal Effect wrapper for legacy watch implementation.
* Provides: active, dirty, scheduler hook, run() & stop().
*/
/**
* Minimal Effect wrapper mimicking the legacy interface used by the watch implementation.
*
* Each instance wraps a native alien `effect`, setting `dirty=true` on invalidation and invoking
* the provided scheduler callback. Consumers may manually `run()` the getter (marks clean) or `stop()`
* to dispose the underlying reactive subscription.
*/
export class Effect {
public active = true;
public dirty = true;
public scheduler: (immediateFirstRun?: boolean) => void = () => {};
private _runner: any;
constructor(private _getter: () => any) {
const self = this;
this._runner = alienEffect(function wrapped() {
self.dirty = true;
self._getter();
self.scheduler();
});
}
run() {
this.dirty = false;
return this._getter();
}
stop() {
if (this.active) {
this._runner();
this.active = false;
}
}
}
/** Resolve a plain value, a signal/computed or a getter function to its current value. */
// Lightweight direct resolver (inlined former toValue/unSignal logic)
/**
* Resolve a possibly reactive input to its current value.
* Accepts: plain value, writable signal, computed signal, or getter function.
* Signals & getters are invoked once; plain values are returned directly.
*/
export function toValue<T>(src: MaybeSignalOrGetter<T>): T {
return isFunction(src)
? (src as any)()
: isSignal(src)
? (src as any)()
: (src as any);
}
/**
* 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.
*
* @example
* batch(() => {
* count(count() + 1);
* other(other() + 2);
* }); // effects observing both run only once
*/
export function batch<T>(fn: () => T): T {
alienStartBatch();
try {
return fn();
} finally {
alienEndBatch();
}
}

@ -0,0 +1,810 @@
import { ReactiveFlags } from "./contents";
import { computed, signal, isSignal } from "./core";
/**
* deepSignal: wrap an object / array / Set graph in lazy per-property signals plus an optional deep patch stream.
* - `$prop` returns a signal; plain prop returns its current value.
* - Getter props become computed signals.
* - Arrays expose `$` (index signals) & `$length`; Sets emit structural entry patches with synthetic ids.
* - subscribeDeepMutations(root, cb) batches set/delete ops per microtask (DeepPatch[]).
* - shallow(obj) skips deep proxying of a subtree.
*/
/** A batched deep mutation (set/add/remove) from a deepSignal root. */
export type DeepPatch = {
/** Unique identifier for the deep signal root which produced this patch. */
root: symbol;
/** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */
path: (string | number)[];
} & (
| DeepSetAddPatch
| DeepSetRemovePatch
| DeepObjectAddPatch
| DeepRemovePatch
| DeepLiteralAddPatch
);
export interface DeepSetAddPatch {
/** Mutation kind applied at the resolved `path`. */
op: "add";
type: "set";
/** New value for `set` mutations (omitted for `delete`). */
value: (number | string | boolean)[] | { [id: string]: object };
}
export interface DeepSetRemovePatch {
/** Mutation kind applied at the resolved `path`. */
op: "remove";
type: "set";
/** The value to be removed from the set. Either a literal or the key (id) of an object. */
value: string | number | boolean;
}
export interface DeepObjectAddPatch {
/** Mutation kind applied at the resolved `path`. */
op: "add";
type: "object";
}
export interface DeepRemovePatch {
/** Mutation kind applied at the resolved `path`. */
op: "remove";
}
export interface DeepLiteralAddPatch {
/** Mutation kind applied at the resolved `path` */
op: "add";
/** The literal value to be added at the resolved `path` */
value: string | number | boolean;
}
/** Callback signature for subscribeDeepMutations. */
export type DeepPatchSubscriber = (patches: DeepPatch[]) => void;
/** Minimal per-proxy metadata for path reconstruction. */
interface ProxyMeta {
/** Parent proxy in the object graph (undefined for root). */
parent?: object;
/** Key within the parent pointing to this proxy (undefined for root). */
key?: string | number;
/** Stable root id symbol shared by the entire deepSignal tree. */
root: symbol;
}
// Proxy -> metadata
const proxyMeta = new WeakMap<object, ProxyMeta>();
// Root symbol -> subscribers
const mutationSubscribers = new Map<symbol, Set<DeepPatchSubscriber>>();
// Pending patches grouped per root (flushed once per microtask)
let pendingPatches: Map<symbol, DeepPatch[]> | null = null;
let microtaskScheduled = false;
/** Sentinel symbol; get concrete root id via getDeepSignalRootId(proxy). */
export const DEEP_SIGNAL_ROOT_ID = Symbol("alienDeepSignalRootId");
function buildPath(
startProxy: object,
leafKey: string | number
): (string | number)[] {
const path: (string | number)[] = [leafKey];
let cur: object | undefined = startProxy;
while (cur) {
const meta = proxyMeta.get(cur);
if (!meta) break; // Defensive: metadata should always exist.
if (meta.key === undefined) break; // Reached root (no key recorded).
path.unshift(meta.key);
cur = meta.parent;
}
return path;
}
function queuePatch(patch: DeepPatch) {
if (!pendingPatches) pendingPatches = new Map();
const root = patch.root;
let list = pendingPatches.get(root);
if (!list) {
list = [];
pendingPatches.set(root, list);
}
list.push(patch);
if (!microtaskScheduled) {
microtaskScheduled = true;
queueMicrotask(() => {
microtaskScheduled = false;
const groups = pendingPatches;
pendingPatches = null;
if (!groups) return;
for (const [rootId, patches] of groups) {
if (!patches.length) continue;
const subs = mutationSubscribers.get(rootId);
if (subs) subs.forEach((cb) => cb(patches));
}
});
}
}
/** Subscribe to microtask-batched deep patches for a root (returns unsubscribe). */
export function subscribeDeepMutations(
root: object | symbol,
sub: DeepPatchSubscriber
): () => void {
const rootId = typeof root === "symbol" ? root : getDeepSignalRootId(root);
if (!rootId)
throw new Error(
"subscribeDeepMutations() expects a deepSignal root proxy or root id symbol"
);
let set = mutationSubscribers.get(rootId);
if (!set) {
set = new Set();
mutationSubscribers.set(rootId, set);
}
set.add(sub);
return () => {
const bucket = mutationSubscribers.get(rootId);
if (!bucket) return;
bucket.delete(sub);
if (bucket.size === 0) mutationSubscribers.delete(rootId);
};
}
/** Return the stable root symbol for any deepSignal proxy (undefined if not one). */
export function getDeepSignalRootId(obj: any): symbol | undefined {
return proxyMeta.get(obj)?.root;
}
// Proxy -> Map of property name -> signal function
/** Proxy -> Map<propertyName, signalFn> (lazy). */
const proxyToSignals = new WeakMap();
// Raw object/array/Set -> stable proxy
const objToProxy = new WeakMap();
// Raw array -> `$` meta proxy with index signals
const arrayToArrayOfSignals = new WeakMap();
// Objects already proxied or marked shallow
const ignore = new WeakSet();
// Object -> signal counter for enumeration invalidation
const objToIterable = new WeakMap();
const rg = /^\$/;
const descriptor = Object.getOwnPropertyDescriptor;
let peeking = false;
// Deep array interface refining callback parameter types.
type DeepArray<T> = Array<T> & {
map: <U>(
callbackfn: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => U,
thisArg?: any
) => U[];
forEach: (
callbackfn: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => void,
thisArg?: any
) => void;
concat(...items: ConcatArray<T>[]): DeepSignalArray<T[]>;
concat(...items: (T | ConcatArray<T>)[]): DeepSignalArray<T[]>;
reverse(): DeepSignalArray<T[]>;
shift(): DeepSignal<T> | undefined;
slice(start?: number, end?: number): DeepSignalArray<T[]>;
splice(start: number, deleteCount?: number): DeepSignalArray<T[]>;
splice(
start: number,
deleteCount: number,
...items: T[]
): DeepSignalArray<T[]>;
filter<S extends T>(
predicate: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => value is DeepSignal<S>,
thisArg?: any
): DeepSignalArray<S[]>;
filter(
predicate: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => unknown,
thisArg?: any
): DeepSignalArray<T[]>;
reduce(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => T
): DeepSignal<T>;
reduce(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => DeepSignal<T>,
initialValue: T
): DeepSignal<T>;
reduce<U>(
callbackfn: (
previousValue: U,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => U,
initialValue: U
): U;
reduceRight(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => T
): DeepSignal<T>;
reduceRight(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => DeepSignal<T>,
initialValue: T
): DeepSignal<T>;
reduceRight<U>(
callbackfn: (
previousValue: U,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => U,
initialValue: U
): U;
};
// Synthetic ids for Set entry objects (stable key for patches)
let __blankNodeCounter = 0;
const setObjectIds = new WeakMap<object, string>();
const assignBlankNodeId = (obj: any) => {
if (setObjectIds.has(obj)) return setObjectIds.get(obj)!;
const id = `_b${++__blankNodeCounter}`;
setObjectIds.set(obj, id);
return id;
};
/** Assign (or override) synthetic id before Set.add(). */
export function setSetEntrySyntheticId(obj: object, id: string | number) {
setObjectIds.set(obj, String(id));
}
const getSetEntryKey = (val: any): string | number => {
if (val && typeof val === "object") {
if (setObjectIds.has(val)) return setObjectIds.get(val)!;
if (
typeof (val as any).id === "string" ||
typeof (val as any).id === "number"
)
return (val as any).id;
if (
typeof (val as any)["@id"] === "string" ||
typeof (val as any)["@id"] === "number"
)
return (val as any)["@id"];
return assignBlankNodeId(val);
}
return val as any;
};
/** Add entry with synthetic id; returns proxied object if applicable. */
export function addWithId<T extends object>(
set: Set<T>,
entry: T,
id: string | number
): DeepSignal<T>;
export function addWithId<T>(set: Set<T>, entry: T, id: string | number): T;
export function addWithId(set: Set<any>, entry: any, id: string | number) {
if (entry && typeof entry === "object") setSetEntrySyntheticId(entry, id);
(set as any).add(entry);
if (entry && typeof entry === "object" && objToProxy.has(entry))
return objToProxy.get(entry);
return entry;
}
/** Is value a deepSignal-managed proxy? */
export const isDeepSignal = (source: any) => {
return proxyToSignals.has(source);
};
/** Was value explicitly marked shallow? */
export const isShallow = (source: any) => {
return ignore.has(source);
};
/** Create (or reuse) a deep reactive proxy for an object / array / Set. */
export const deepSignal = <T extends object>(obj: T): DeepSignal<T> => {
if (!shouldProxy(obj)) throw new Error("This object can't be observed.");
if (!objToProxy.has(obj)) {
// Create a unique root id symbol to identify this deep signal tree in patches.
const rootId = Symbol("deepSignalRoot");
const proxy = createProxy(obj, objectHandlers, rootId) as DeepSignal<T>;
const meta = proxyMeta.get(proxy)!;
meta.parent = undefined; // root has no parent
meta.key = undefined; // root not addressed by a key
meta.root = rootId; // ensure root id stored (explicit)
// Pre-register an empty signals map so isDeepSignal() is true before any property access.
if (!proxyToSignals.has(proxy)) proxyToSignals.set(proxy, new Map());
objToProxy.set(obj, proxy);
}
return objToProxy.get(obj);
};
/** Read property without tracking (untracked read). */
export const peek = <
T extends DeepSignalObject<object>,
K extends keyof RevertDeepSignalObject<T>
>(
obj: T,
key: K
): RevertDeepSignal<RevertDeepSignalObject<T>[K]> => {
peeking = true;
const value = obj[key];
try {
peeking = false;
} catch (e) {}
return value as RevertDeepSignal<RevertDeepSignalObject<T>[K]>;
};
const shallowFlag = Symbol(ReactiveFlags.IS_SHALLOW);
/** Mark object to skip deep proxying (only reference changes tracked). */
export function shallow<T extends object>(obj: T): Shallow<T> {
ignore.add(obj);
return obj as Shallow<T>;
}
const createProxy = (
target: object,
handlers: ProxyHandler<object>,
rootId?: symbol
) => {
const proxy = new Proxy(target, handlers);
ignore.add(proxy);
// Initialize proxy metadata if not present. Root proxies provide a stable root id.
if (!proxyMeta.has(proxy)) {
proxyMeta.set(proxy, { root: rootId || Symbol("deepSignalDetachedRoot") });
}
return proxy;
};
// Set-specific access & structural patch emission.
function getFromSet(raw: Set<any>, key: string, receiver: object): any {
const meta = proxyMeta.get(receiver);
// Helper to proxy a single entry (object) & assign synthetic id if needed.
const ensureEntryProxy = (entry: any) => {
if (
entry &&
typeof entry === "object" &&
shouldProxy(entry) &&
!objToProxy.has(entry)
) {
const synthetic = getSetEntryKey(entry);
const childProxy = createProxy(entry, objectHandlers, meta!.root);
const childMeta = proxyMeta.get(childProxy)!;
childMeta.parent = receiver;
childMeta.key = synthetic;
objToProxy.set(entry, childProxy);
return childProxy;
}
if (objToProxy.has(entry)) return objToProxy.get(entry);
return entry;
};
// Pre-pass to ensure any existing non-proxied object entries are proxied (enables deep patches after iteration)
if (meta) raw.forEach(ensureEntryProxy);
if (key === "add" || key === "delete" || key === "clear") {
const fn: Function = (raw as any)[key];
return function (this: any, ...args: any[]) {
const sizeBefore = raw.size;
const result = fn.apply(raw, args);
if (raw.size !== sizeBefore) {
const metaNow = proxyMeta.get(receiver);
if (
metaNow &&
metaNow.parent !== undefined &&
metaNow.key !== undefined
) {
const containerPath = buildPath(metaNow.parent, metaNow.key);
if (key === "add") {
const entry = args[0];
let synthetic = getSetEntryKey(entry);
if (entry && typeof entry === "object") {
for (const existing of raw.values()) {
if (existing === entry) continue;
if (getSetEntryKey(existing) === synthetic) {
synthetic = assignBlankNodeId(entry);
break;
}
}
}
let entryVal = entry;
if (
entryVal &&
typeof entryVal === "object" &&
shouldProxy(entryVal) &&
!objToProxy.has(entryVal)
) {
const childProxy = createProxy(
entryVal,
objectHandlers,
metaNow.root
);
const childMeta = proxyMeta.get(childProxy)!;
childMeta.parent = receiver;
childMeta.key = synthetic;
objToProxy.set(entryVal, childProxy);
entryVal = childProxy;
}
// Set entry add: emit object vs literal variant.
if (entryVal && typeof entryVal === "object") {
queuePatch({
root: metaNow.root,
path: [...containerPath, synthetic],
op: "add",
type: "object",
});
} else {
queuePatch({
root: metaNow.root,
path: [...containerPath, synthetic],
op: "add",
value: entryVal,
});
}
} else if (key === "delete") {
const entry = args[0];
const synthetic = getSetEntryKey(entry);
queuePatch({
root: metaNow.root,
path: [...containerPath, synthetic],
op: "remove",
});
} else if (key === "clear") {
// Structural clear: remove prior entry-level patches for this Set this tick.
if (pendingPatches) {
const group = pendingPatches.get(metaNow.root);
if (group && group.length) {
for (let i = group.length - 1; i >= 0; i--) {
const p = group[i];
if (
p.path.length === containerPath.length + 1 &&
containerPath.every((seg, idx) => p.path[idx] === seg)
) {
group.splice(i, 1);
}
}
}
}
queuePatch({
root: metaNow.root,
path: containerPath,
op: "add",
type: "set",
value: [],
});
}
}
}
return result;
};
}
const makeIterator = (pair: boolean) => {
return function thisIter(this: any) {
const iterable = raw.values();
return {
[Symbol.iterator]() {
return {
next() {
const n = iterable.next();
if (n.done) return n;
const entry = ensureEntryProxy(n.value);
return { value: pair ? [entry, entry] : entry, done: false };
},
};
},
} as Iterable<any>;
};
};
if (key === "values" || key === "keys") return makeIterator(false);
if (key === "entries") return makeIterator(true);
if (key === "forEach") {
return function thisForEach(this: any, cb: any, thisArg?: any) {
raw.forEach((entry: any) => {
cb.call(thisArg, ensureEntryProxy(entry), ensureEntryProxy(entry), raw);
});
};
}
if (key === Symbol.iterator.toString()) {
// string form access of iterator symbol; pass through
}
const val = (raw as any)[key];
if (typeof val === "function") return val.bind(raw);
return val;
}
const throwOnMutation = () => {
throw new Error(
"Don't mutate the signals directly (use the underlying property/value instead)."
);
};
// Does target define a getter for key?
function hasGetter(target: any, key: any) {
return typeof descriptor(target, key)?.get === "function";
}
// Lazily allocate / fetch signal map for a proxy receiver.
function getSignals(receiver: object) {
if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map());
return proxyToSignals.get(receiver)!;
}
// Wrap & link child object/array/Set if needed.
function ensureChildProxy(value: any, parent: object, key: string | number) {
if (!shouldProxy(value)) return value;
if (!objToProxy.has(value)) {
const parentMeta = proxyMeta.get(parent)!;
const childProxy = createProxy(value, objectHandlers, parentMeta.root);
const childMeta = proxyMeta.get(childProxy)!;
childMeta.parent = parent;
childMeta.key = key as string;
objToProxy.set(value, childProxy);
}
return objToProxy.get(value);
}
// Normalize raw property key (handles $-prefix & array meta) -> { key, returnSignal }
function normalizeKey(
target: any,
fullKey: string,
isArrayMeta: boolean,
receiver: object
) {
let returnSignal = isArrayMeta || fullKey[0] === "$";
if (!isArrayMeta && Array.isArray(target) && returnSignal) {
if (fullKey === "$") {
// Provide $ meta proxy for array index signals
if (!arrayToArrayOfSignals.has(target)) {
arrayToArrayOfSignals.set(
target,
createProxy(target, arrayHandlers, proxyMeta.get(receiver)?.root)
);
}
return { shortCircuit: arrayToArrayOfSignals.get(target) };
}
returnSignal = fullKey === "$length";
}
const key = returnSignal ? fullKey.replace(rg, "") : fullKey;
return { key, returnSignal } as any;
}
// Create computed signal for getter property if needed.
function ensureComputed(
signals: Map<any, any>,
target: any,
key: any,
receiver: any
) {
if (!signals.has(key) && hasGetter(target, key)) {
signals.set(
key,
computed(() => Reflect.get(target, key, receiver))
);
}
}
// Unified get trap factory (object / array meta variant)
const get =
(isArrayMeta: boolean) =>
(target: object, fullKey: string, receiver: object): unknown => {
if (peeking) return Reflect.get(target, fullKey, receiver);
// Set handling delegated completely.
if (target instanceof Set && typeof fullKey === "string") {
return getFromSet(target as Set<any>, fullKey, receiver);
}
const norm = normalizeKey(target, fullKey, isArrayMeta, receiver);
if ((norm as any).shortCircuit) return (norm as any).shortCircuit; // returned meta proxy
const { key, returnSignal } = norm as {
key: string;
returnSignal: boolean;
};
// Symbol fast-path
if (typeof key === "symbol" && wellKnownSymbols.has(key))
return Reflect.get(target, key, receiver);
const signals = getSignals(receiver);
ensureComputed(signals, target, key, receiver);
if (!signals.has(key)) {
let value = Reflect.get(target, key, receiver);
if (returnSignal && typeof value === "function") return; // user asked for signal wrapper of function => ignore
value = ensureChildProxy(value, receiver, key);
signals.set(key, signal(value));
}
const sig = signals.get(key);
return returnSignal ? sig : sig();
};
// Standard object / array handlers
const objectHandlers = {
get: get(false),
set(target: object, fullKey: string, val: any, receiver: object): boolean {
// Respect original getter/setter semantics
if (typeof descriptor(target, fullKey)?.set === "function")
return Reflect.set(target, fullKey, val, receiver);
if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map());
const signals = proxyToSignals.get(receiver);
if (fullKey[0] === "$") {
if (!isSignal(val)) throwOnMutation();
const key = fullKey.replace(rg, "");
signals.set(key, val);
return Reflect.set(target, key, val.peek(), receiver);
} else {
let internal = val;
if (shouldProxy(val)) {
if (!objToProxy.has(val)) {
// Link newly wrapped child to its parent for path reconstruction.
// In some edge cases parent metadata might not yet be initialized (e.g.,
// if a proxied structure was reconstructed in a way that bypassed the
// original deepSignal root path). Fall back to creating/assigning it.
let parentMeta = proxyMeta.get(receiver);
if (!parentMeta) {
// Assign a root id (new symbol) so downstream patches remain groupable.
const created: ProxyMeta = {
root: Symbol("deepSignalRootAuto"),
} as ProxyMeta;
proxyMeta.set(receiver, created);
parentMeta = created;
}
const childProxy = createProxy(val, objectHandlers, parentMeta!.root);
const childMeta = proxyMeta.get(childProxy)!;
childMeta.parent = receiver;
childMeta.key = fullKey;
objToProxy.set(val, childProxy);
}
internal = objToProxy.get(val);
}
const isNew = !(fullKey in target);
const result = Reflect.set(target, fullKey, val, receiver);
if (!signals.has(fullKey)) {
// First write after structure change -> create signal.
signals.set(fullKey, signal(internal));
} else {
// Subsequent writes -> update underlying signal.
signals.get(fullKey).set(internal);
}
if (isNew && objToIterable.has(target)) objToIterable.get(target).value++;
if (Array.isArray(target) && signals.has("length"))
signals.get("length").set(target.length);
// Emit patch (after mutation) so subscribers get final value snapshot.
const meta = proxyMeta.get(receiver);
if (meta) {
// Object/Array/Set assignment at property path.
if (val && typeof val === "object") {
queuePatch({
root: meta.root,
path: buildPath(receiver, fullKey),
op: "add",
type: "object",
});
} else {
queuePatch({
root: meta.root,
path: buildPath(receiver, fullKey),
op: "add",
value: val,
});
}
}
return result;
}
},
deleteProperty(target: object, key: string): boolean {
if (key[0] === "$") throwOnMutation();
const signals = proxyToSignals.get(objToProxy.get(target));
const result = Reflect.deleteProperty(target, key);
if (signals && signals.has(key)) signals.get(key).value = undefined;
objToIterable.has(target) && objToIterable.get(target).value++;
// Emit deletion patch
const receiverProxy = objToProxy.get(target);
const meta = receiverProxy && proxyMeta.get(receiverProxy);
if (meta) {
queuePatch({
root: meta.root,
path: buildPath(receiverProxy, key),
op: "remove",
});
}
return result;
},
ownKeys(target: object): (string | symbol)[] {
if (!objToIterable.has(target)) objToIterable.set(target, signal(0));
(objToIterable as any)._ = objToIterable.get(target).get();
return Reflect.ownKeys(target);
},
};
// Array `$` meta proxy handlers (index signals only)
const arrayHandlers = {
get: get(true),
set: throwOnMutation,
deleteProperty: throwOnMutation,
};
const wellKnownSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map((key) => Symbol[key as WellKnownSymbols])
.filter((value) => typeof value === "symbol")
);
// Supported constructors (Map intentionally excluded for now)
const supported = new Set([Object, Array, Set]);
const shouldProxy = (val: any): boolean => {
if (typeof val !== "object" || val === null) return false;
return supported.has(val.constructor) && !ignore.has(val);
};
/** TYPES **/ // Structural deep reactive view of an input type.
export type DeepSignal<T> = T extends Function
? T
: T extends { [shallowFlag]: true }
? T
: T extends Array<unknown>
? DeepSignalArray<T>
: T extends object
? DeepSignalObject<T>
: T;
/** Recursive mapped type converting an object graph into its deepSignal proxy shape. */
export type DeepSignalObject<T extends object> = {
[P in keyof T & string as `$${P}`]?: T[P] extends Function
? never
: ReturnType<typeof signal<T[P]>>;
} & {
[P in keyof T]: DeepSignal<T[P]>;
};
/** Extract element type from an array. */
type ArrayType<T> = T extends Array<infer I> ? I : T;
/** DeepSignal-enhanced array type (numeric indices & `$` meta accessors). */
type DeepSignalArray<T> = DeepArray<ArrayType<T>> & {
[key: number]: DeepSignal<ArrayType<T>>;
$?: { [key: number]: ReturnType<typeof signal<ArrayType<T>>> };
$length?: ReturnType<typeof signal<number>>;
};
/** Marker utility type for objects passed through without deep proxying. */
export type Shallow<T extends object> = T & { [shallowFlag]: true };
/** Framework adapter hook returning a DeepSignal proxy. */
export declare const useDeepSignal: <T extends object>(obj: T) => DeepSignal<T>;
// @ts-ignore
// Strip `$`-prefixed synthetic signal accessors from key union.
type FilterSignals<K> = K extends `$${string}` ? never : K;
/** Reverse of DeepSignalObject: remove signal accessors to obtain original object shape. */
type RevertDeepSignalObject<T> = Pick<T, FilterSignals<keyof T>>;
/** Reverse of DeepSignalArray: omit meta accessors. */
type RevertDeepSignalArray<T> = Omit<T, "$" | "$length">;
/** Inverse mapped type removing deepSignal wrapper affordances. */
export type RevertDeepSignal<T> = T extends Array<unknown>
? RevertDeepSignalArray<T>
: T extends object
? RevertDeepSignalObject<T>
: T;
/** Subset of ECMAScript well-known symbols we explicitly pass through without proxy wrapping. */
type WellKnownSymbols =
| "asyncIterator"
| "hasInstance"
| "isConcatSpreadable"
| "iterator"
| "match"
| "matchAll"
| "replace"
| "search"
| "species"
| "split"
| "toPrimitive"
| "toStringTag"
| "unscopables";

@ -1,26 +0,0 @@
import React from "react";
import useShape from "../../ng-mock/js-land/frontendAdapters/react/useShape";
export function HelloWorldReact() {
const state = useShape("Shape1", "");
window.reactState = state;
console.log("react render", state);
if (!state) return <>Loading state</>;
return (
<div>
<p>Hello World from React!</p>
{state.name} lives at {state.address.street}
<p></p>
<button
onClick={() => {
state.name = `${state.name} ${state.name}`;
}}
>
Double name
</button>
</div>
);
}

@ -1,19 +0,0 @@
<script>
import useShape from "../../ng-mock/js-land/frontendAdapters/svelte/useShape";
const nestedObject = useShape("Shape1", null);
</script>
<div>
<p>Hello World from Svelte!</p>
<pre>{JSON.stringify($nestedObject, null, 4)}</pre>
<button
on:click={() => {
window.svelteState = $nestedObject;
$nestedObject.name = $nestedObject.name.toUpperCase();
}}
>
upper-case name
</button>
</div>

@ -1,10 +0,0 @@
import { test, expect } from "@playwright/test";
test("components load", async ({ page }) => {
await page.goto("/");
await page.waitForSelector(".vue astro-island");
await expect(page.locator(".vue .title")).toHaveText("vue");
await expect(page.locator(".react .title")).toHaveText("react");
await expect(page.locator(".svelte .title")).toHaveText("svelte");
});

@ -1,29 +0,0 @@
<script setup lang="ts">
import type { WritableComputedRef } from 'vue';
import useShape from '../../ng-mock/js-land/frontendAdapters/vue/useShape';
const shapeObj = useShape("Shape1", "null");
window.vueState= shapeObj;
const setName = () => {
shapeObj.value.name = "Bobby"
}
</script>
<template>
<div>
<p>Hello World from Vue!</p>
<div v-if="shapeObj != null">
<p>Type is <em>{{shapeObj.type}}</em> with street <em>{{shapeObj.address.street}}</em></p>
<button @click="setName">
Click to switch name
</button>
</div>
<div v-else>
Loading state
</div>
</div>
</template>

@ -0,0 +1,17 @@
export * from "./core";
export * from "./deepSignal";
export * from "./watch";
export * from "./watchEffect";
export {
isArray,
isDate,
isFunction,
isMap,
isObject,
isPlainObject,
isPromise,
isRegExp,
isSet,
isString,
isSymbol,
} from "./utils";

@ -1,9 +0,0 @@
import type { Diff } from "../types";
/** Mock function to apply diffs. Just uses a copy of the diff as the new object. */
export function applyDiff(currentState: any, diff: Diff) {
const clone = JSON.parse(JSON.stringify(diff));
Object.keys(clone).forEach((k) => {
currentState[k] = clone[k];
});
}

@ -1,84 +0,0 @@
import updateShape from "src/ng-mock/wasm-land/updateShape";
import type { Connection, Diff, Scope, Shape } from "../types";
import requestShape from "src/ng-mock/wasm-land/requestShape";
import { applyDiff } from "./applyDiff";
import { batch, deepSignal, watch } from "alien-deepsignals";
import { signal as createSignal, computed } from "alien-signals";
type ReactiveShapeObject = object;
type ShapeObjectSignal = ReturnType<
typeof createSignal<{
content: ReactiveShapeObject | null;
}>
>;
const openConnections: Partial<Record<Shape, ShapeObjectSignal>> = {};
/**
* Create a signal for a shape object.
* The function returns a signal of a shape object in the form:
* `{content: <shapeObject>}`
**/
export function createSignalObjectForShape(
shape: Shape,
scope?: Scope,
poolSignal = true
) {
if (poolSignal && openConnections[shape]) return openConnections[shape];
// DeepSignal has a different API to alien-signals.
// Therefore, we need to create a "root signal" wrapper that is
// triggered on deepSignal changes.
const rootSignal = createSignal<{
content: ReactiveShapeObject | null;
}>({ content: null });
// State
let stopWatcher: any = null;
let suspendDeepWatcher = false;
const onUpdateFromDb = (diff: Diff, connectionId: Connection["id"]) => {
const rootSignalValue = rootSignal();
console.log("Update received", connectionId, diff);
// Set new value from applying the diffs to the old value.
suspendDeepWatcher = true;
// We need to replace the root signal for now, so this is redundant.
batch(() => {
if (!rootSignalValue) return; // This shouldn't happen but we make the compiler happy.
const { content: proxiedShapeObj } = rootSignalValue;
applyDiff(proxiedShapeObj, diff);
// We put the proxied object into a new object for the root signal to trigger.
rootSignal({ content: proxiedShapeObj });
});
suspendDeepWatcher = false;
};
// Do the actual db request.
requestShape(shape, scope, onUpdateFromDb).then(
({ connectionId, shapeObject }) => {
// Create a deepSignal to put into the vanilla alien-signal.
const proxiedShapeObj = deepSignal(shapeObject);
// Notify DB on changes.
stopWatcher = watch(
proxiedShapeObj,
(newVal, oldVal, onCleanup) => {
// Don't update when applying changes from db diffs from the db.
if (!suspendDeepWatcher) updateShape(connectionId, newVal);
},
{ deep: true }
);
// Update the root signal.
rootSignal({ content: proxiedShapeObj });
}
);
if (poolSignal) openConnections[shape] = rootSignal;
// TODO: Dispose deepSignal root signal disposal.
return rootSignal;
}

@ -1,22 +0,0 @@
import { useSignal } from "@gn8/alien-signals-react";
import { useMemo, useRef } from "react";
import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape";
import type { Scope, Shape } from "src/ng-mock/js-land/types";
// TODO: The universe-signal library causes two renders on change instead of one.
// Find a fix
const useShape = (shape: Shape, scope: Scope) => {
const signalOfShape = useMemo(() => {
console.log("react memo called...");
return createSignalObjectForShape(shape, scope);
}, [shape, scope]);
const [{ content: shapeObject }, setShapeObject] = useSignal(signalOfShape);
// We don't need the setter.
// The object is recursively proxied and changes are recorded there.
return shapeObject;
};
export default useShape;

@ -1,16 +0,0 @@
import { derived } from "svelte/store";
import { useSignal } from "@gn8/alien-signals-svelte";
import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape";
import type { Scope, Shape } from "src/ng-mock/js-land/types";
const useShape = (shape: Shape, scope: Scope) => {
const signalOfShape = createSignalObjectForShape(shape, scope);
const writeableStoreForShape = useSignal(signalOfShape);
// Get the content "deepSignal"
const content = derived(writeableStoreForShape, (value) => value.content);
return content;
};
export default useShape;

@ -1,14 +0,0 @@
import { useSignal } from "@gn8/alien-signals-vue";
import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape";
import type { Scope, Shape } from "src/ng-mock/js-land/types";
import { computed } from "vue";
const useShape = (shape: Shape, scope: Scope) => {
const signalOfShape = createSignalObjectForShape(shape, scope);
const refOfShape = useSignal(signalOfShape);
// TODO: Maybe `refOfShape.value.content` works too?
return computed(() => refOfShape.value.content);
};
export default useShape;

@ -1,14 +0,0 @@
/** The shape of an object requested. */
export type Shape = "Shape1" | "Shape2";
/** The Scope of a shape request */
export type Scope = string | string[];
/** The diff format used to communicate updates between wasm-land and js-land. */
export type Diff = object;
/** A connection established between wasm-land and js-land for subscription of a shape. */
export type Connection = {
id: string;
onUpdateFromWasm: (diff: Diff) => void;
};

@ -1,30 +0,0 @@
import { expect, test } from "vitest";
import { createSignalObjectForShape } from "../js-land/connector/createSignalObjectForShape";
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// TODO: Redo
test("shape object notification comes back to others", async () => {
const object1 = createSignalObjectForShape("Shape1");
const object2 = createSignalObjectForShape("Shape1");
const object3 = createSignalObjectForShape("Shape2");
const object4 = createSignalObjectForShape("Shape2");
wait(50);
// Update object 1 and expect object 2 to update as well.
object1()!.name = "Updated name from object1";
wait(30);
expect(object2()!.name).toBe("Updated name from object1");
// Expect object of different shape not to have changed.
expect(object3().name).toBe("Niko's cat");
// Update object 4 and expect object 3 with same shape to have updated.
object4()!.name = "Updated name from object4";
wait(30);
expect(object3()!.name).toBe("Updated name from object4");
});

@ -1,53 +0,0 @@
import * as shapeManager from "./shapeManager";
import type { WasmConnection, Diff, Scope } from "./types";
import type { Shape } from "../js-land/types";
const mockShapeObject1 = {
type: "Person",
name: "Bob",
address: {
street: "First street",
houseNumber: "15",
},
hasChildren: true,
numberOfHouses: 0,
};
const mockShapeObject2 = {
type: "Cat",
name: "Niko's cat",
age: 12,
numberOfHomes: 3,
address: {
street: "Niko's street",
houseNumber: "15",
floor: 0,
},
};
let connectionIdCounter = 1;
export default async function requestShape(
shape: Shape,
scope: Scope | undefined,
callback: (diff: Diff, connectionId: WasmConnection["id"]) => void
): Promise<{
connectionId: string;
shapeObject: object;
}> {
const connection: WasmConnection = {
id: String(connectionIdCounter++),
shape,
// Create a deep copy to prevent accidental by-reference changes.
state: JSON.parse(
JSON.stringify(shape === "Shape1" ? mockShapeObject1 : mockShapeObject2)
),
callback,
};
shapeManager.connections.set(connection.id, connection);
return {
connectionId: connection.id,
shapeObject: connection.state,
};
}

@ -1,10 +0,0 @@
import type { Diff, ObjectState, WasmConnection } from "./types";
const connections: Map<WasmConnection["id"], WasmConnection> = new Map();
/** Mock function to apply diffs. Just uses a copy of the diff as the new object. */
export function applyDiff(currentState: ObjectState, diff: Diff): ObjectState {
return JSON.parse(JSON.stringify(diff));
}
export { connections };

@ -1,17 +0,0 @@
import type { Shape } from "../js-land/types";
/** The Scope of a shape request */
export type Scope = string | string[];
/** The diff format used to communicate updates between wasm-land and js-land. */
export type Diff = object;
export type ObjectState = object;
/** A connection established between wasm-land and js-land for subscription of a shape. */
export type WasmConnection = {
id: string;
shape: Shape;
state: ObjectState;
callback: (diff: Diff, connectionId: WasmConnection["id"]) => void;
};

@ -1,22 +0,0 @@
import * as shapeManager from "./shapeManager";
import type { WasmConnection, Diff } from "./types";
export default async function updateShape(
connectionId: WasmConnection["id"],
diff: Diff
) {
const connection = shapeManager.connections.get(connectionId);
if (!connection) throw new Error("No Connection found.");
console.log("BACKEND: Received update request from ", connectionId);
const newState = shapeManager.applyDiff(connection.state, diff);
connection.state = newState;
shapeManager.connections.forEach((con) => {
if (con.shape == connection.shape) {
con.state = newState;
con.callback(diff, con.id);
}
});
}

@ -0,0 +1,81 @@
import { describe, it, expect } from "vitest";
import { signal, computed, isSignal, Effect, toValue } from "../core";
import { deepSignal } from "../deepSignal";
describe("core.ts coverage", () => {
it("signal tagging helpers (.value/.peek/.get/.set)", () => {
const s: any = signal(1);
expect(isSignal(s)).toBe(true);
expect(s.value).toBe(1);
expect(s.peek()).toBe(1);
expect(s.get()).toBe(1);
s.set(2);
expect(s.value).toBe(2);
s.value = 3;
expect(s.peek()).toBe(3);
});
it("computed tagging helpers (.value/.peek/.get)", () => {
const s: any = signal(2);
const c: any = computed(() => s.value * 2);
expect(isSignal(c)).toBe(true);
expect(c.value).toBe(4);
expect(c.peek()).toBe(4);
expect(c.get()).toBe(4);
s.value = 3;
expect(c.value).toBe(6);
});
it("toValue resolves function, signal and plain value", () => {
const s: any = signal(5);
const fn = () => 10;
expect(toValue(fn)).toBe(10);
expect(toValue(s)).toBe(5);
expect(toValue(42)).toBe(42);
});
it("Effect wrapper run/stop behavior", () => {
let runs = 0;
const eff = new Effect(() => {
runs++;
});
// Constructing Effect registers alienEffect and schedules first run immediately when dependency accessed (none here), run() executes getter
eff.run();
// Construction may trigger an initial scheduler pass; ensure at least 1
expect(runs).toBeGreaterThanOrEqual(1);
// Add scheduler side effect and dependency in second effect
const dep = signal(0);
const eff2 = new Effect(() => {
dep();
runs++;
});
const base = runs;
dep.set(1); // triggers wrapped effect, increments runs again
expect(runs).toBeGreaterThan(base);
eff2.stop();
const prev = runs;
dep.set(2); // no further increment after stop
expect(runs).toBe(prev);
// stopping already stopped effect has no effect
eff2.stop();
expect(runs).toBe(prev);
});
});
describe("deepSignal.ts extra branches", () => {
it("access well-known symbol property returns raw value and not a signal", () => {
const tag = Symbol.toStringTag;
const ds = deepSignal({ [tag]: "Custom", x: 1 }) as any;
const val = ds[tag];
expect(val).toBe("Custom");
});
it("access Set Symbol.iterator.toString() key path (skip branch)", () => {
const ds = deepSignal({ set: new Set([1]) }) as any;
const iterKey = Symbol.iterator.toString(); // 'Symbol(Symbol.iterator)'
// Accessing this string property triggers skip branch (no special handling needed)
const maybe = ds.set[iterKey];
// underlying Set likely has undefined for that string key
expect(maybe).toBeUndefined();
});
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,47 @@
import { describe, it, expect, beforeEach } from "vitest";
import { deepSignal } from "../deepSignal";
import { watch } from "../watch";
// Goal: demonstrate that patchOptimized deep watch performs fewer traversals
// than standard deep watch for the same batch of nested mutations.
// We use the exported __traverseCount instrumentation to measure how many
// times traverse() executes under each strategy.
describe("watch patch-only simplified performance placeholder", () => {
let store: any;
const build = (breadth = 3, depth = 3) => {
const make = (d: number): any => {
if (d === 0) return { v: 0 };
const obj: any = {};
for (let i = 0; i < breadth; i++) obj["k" + i] = make(d - 1);
return obj;
};
return make(depth);
};
beforeEach(() => {
store = deepSignal(build());
});
function mutateAll(breadth = 3, depth = 3) {
const visit = (node: any, d: number) => {
if (d === 0) {
node.v++;
return;
}
for (let i = 0; i < breadth; i++) visit(node["k" + i], d - 1);
};
visit(store, depth);
}
it("receives a single batch of patches after deep mutations", async () => {
let batches = 0;
const { stopListening: stop } = watch(store, ({ patches }) => {
if (patches.length) batches++;
});
mutateAll();
await Promise.resolve();
expect(batches).toBe(1);
stop();
});
});

@ -0,0 +1,148 @@
import { describe, it, expect } from "vitest";
import { deepSignal, getDeepSignalRootId, peek } from "../deepSignal";
import {
watch,
__traverseCount,
__resetTraverseCount,
traverse,
} from "../watch";
import { effect } from "../core";
describe("watch advanced", () => {
it("basic patch watcher fires on deep mutations", async () => {
const st = deepSignal({ a: { b: { c: 1 } } });
let batches: number = 0;
watch(st, ({ patches }) => {
if (patches.length) batches++;
});
st.a.b.c = 2;
st.a.b = { c: 3 } as any;
await Promise.resolve();
expect(batches).toBeGreaterThan(0);
});
// multi-source value mode removed; patch-only now - skip equivalent
// getter source value mode removed in patch-only watcher
it("watch once option still stops after first batch", async () => {
const st = deepSignal({ a: 1 });
let count = 0;
watch(
st,
() => {
count++;
},
{ once: true, immediate: true }
);
st.a = 2;
st.a = 3;
await Promise.resolve();
expect(count).toBe(1);
});
// observe value mode removed; observe is alias of watch
});
describe("patches & root ids", () => {
it("root ids are unique", () => {
const a = deepSignal({});
const b = deepSignal({});
expect(getDeepSignalRootId(a)).not.toBe(getDeepSignalRootId(b));
});
// legacy watchPatches API removed; patch mode only valid for deepSignal roots
it("watch throws on non-deepSignal input", () => {
expect(() => watch({} as any, () => {})).toThrow();
});
it("Map unsupported does not emit patches", async () => {
const m = new Map<string, number>();
const st = deepSignal({ m });
const patches: any[] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
m.set("a", 1);
await Promise.resolve();
await Promise.resolve();
expect(patches.length).toBe(0);
stop();
});
});
describe("tier3: Set iteration variants", () => {
it("entries() iteration proxies nested mutation", async () => {
const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "eEnt", inner: { v: 1 } });
const paths: string[] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
paths.push(...patches.map((pp: any) => pp.path.join(".")))
);
for (const [val] of st.s.entries()) {
(val as any).inner.v;
} // ensure proxy
for (const [val] of st.s.entries()) {
(val as any).inner.v = 2;
}
await Promise.resolve();
await Promise.resolve();
expect(paths.some((p) => p.endsWith("eEnt.inner.v"))).toBe(true);
stop();
});
it("forEach iteration proxies nested mutation", async () => {
const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "fe1", data: { n: 1 } });
const { stopListening: stop } = watch(st, () => {});
st.s.forEach((e) => (e as any).data.n); // access
st.s.forEach((e) => {
(e as any).data.n = 2;
});
await Promise.resolve();
await Promise.resolve();
stop();
});
it("keys() iteration returns proxies", async () => {
const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "k1", foo: { x: 1 } });
const { stopListening: stop } = watch(st, () => {});
for (const e of st.s.keys()) {
(e as any).foo.x = 2;
}
await Promise.resolve();
await Promise.resolve();
stop();
});
});
describe("tier3: peek behavior", () => {
it("peek does not create reactive dependency on property", async () => {
const st = deepSignal({ a: 1 });
let runs = 0;
effect(() => {
runs++;
peek(st, "a");
});
expect(runs).toBe(1);
st.a = 2;
// Flush microtasks
await Promise.resolve();
await Promise.resolve();
expect(runs).toBe(1); // no rerun
});
});
describe("tier3: traverse helper direct calls (symbols & sets)", () => {
it("traverse counts and respects depth param", () => {
__resetTraverseCount();
const obj: any = { a: { b: { c: 1 } } };
traverse(obj, 1);
const shallowCount = __traverseCount;
__resetTraverseCount();
traverse(obj, 3);
const deepCount = __traverseCount;
expect(deepCount).toBeGreaterThan(shallowCount);
});
});

@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { deepSignal } from "../deepSignal";
import { watch } from "../watch";
import { watchEffect } from "../watchEffect";
describe("watch", () => {
it("watch immediate", () => {
const store = deepSignal({
userinfo: {
name: "tom",
},
});
let val!: string;
watch(
store,
({ newValue }) => {
val = newValue.userinfo.name;
},
{ immediate: true }
);
expect(val).toEqual("tom");
});
it("watch deep", () => {
const store = deepSignal({
userinfo: {
name: "tom",
},
});
let val!: string;
watch(
store,
({ newValue }) => {
val = newValue.userinfo.name;
},
{ immediate: true }
);
let value2!: string;
watch(
store,
({ newValue }) => {
value2 = newValue.userinfo.name;
},
{ immediate: true }
);
expect(val).toEqual("tom");
store.userinfo.name = "jon";
// patch delivery async (microtask)
return Promise.resolve().then(() => {
expect(val).toEqual("jon");
// With refactored watch using native effect, shallow watcher now also updates root reference
expect(value2).toEqual("jon");
});
});
it("watch once", () => {
const store = deepSignal({
userinfo: {
name: "tom",
},
});
let val!: string;
watch(
store,
({ newValue }) => {
val = newValue.userinfo.name;
},
{ immediate: true, once: true }
);
expect(val).toEqual("tom");
store.userinfo.name = "jon";
// once watcher shouldn't update after first run
expect(val).toEqual("tom");
});
it("watch effect", () => {
const store = deepSignal({
userinfo: {
name: "tom",
},
});
let x = undefined;
watchEffect(() => {
x = store.userinfo.name;
});
expect(x).toEqual("tom");
store.userinfo.name = "jon";
expect(x).toEqual("jon");
});
});

@ -0,0 +1,336 @@
import { describe, it, expect } from "vitest";
import {
deepSignal,
setSetEntrySyntheticId,
addWithId,
DeepPatch,
} from "../deepSignal";
import { watch, observe } from "../watch";
describe("watch (patch mode)", () => {
it("emits set patches with correct paths and batching", async () => {
const state = deepSignal({ a: { b: 1 }, arr: [1, { x: 2 }] });
const received: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) => {
received.push(patches);
});
state.a.b = 2;
(state.arr[1] as any).x = 3;
state.arr.push(5);
await Promise.resolve();
expect(received.length).toBe(1);
const batch = received[0];
const paths = batch.map((p) => p.path.join(".")).sort();
expect(paths).toContain("a.b");
expect(paths).toContain("arr.1.x");
expect(paths).toContain("arr.2");
const addOps = batch.filter((p) => p.op === "add").length;
expect(addOps).toBe(batch.length);
stop();
});
it("emits delete patches without value", async () => {
const state = deepSignal<{ a: { b?: number }; c?: number }>({
a: { b: 1 },
c: 2,
});
const out: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) =>
out.push(patches)
);
delete state.a.b;
delete state.c;
await Promise.resolve();
expect(out.length).toBe(1);
const [batch] = out;
const deletePatches = batch.filter((p) => p.op === "remove");
const delPaths = deletePatches.map((p) => p.path.join(".")).sort();
expect(delPaths).toEqual(["a.b", "c"]);
deletePatches.forEach((p: any) => expect(p.value).toBeUndefined());
stop();
});
it("observe patch mode mirrors watch patch mode", async () => {
const state = deepSignal({ a: 1 });
const wp: DeepPatch[][] = [];
const ob: DeepPatch[][] = [];
const { stopListening: stop1 } = watch(state, ({ patches }) =>
wp.push(patches)
);
const { stopListening: stop2 } = observe(state, ({ patches }) =>
ob.push(patches)
);
state.a = 2;
await Promise.resolve();
expect(wp.length).toBe(1);
expect(ob.length).toBe(1);
expect(wp[0][0].path.join(".")).toBe("a");
stop1();
stop2();
});
it("filters out patches from other roots", async () => {
const a = deepSignal({ x: 1 });
const b = deepSignal({ y: 2 });
const out: DeepPatch[][] = [];
const { stopListening: stop } = watch(a, ({ patches }) =>
out.push(patches)
);
b.y = 3;
a.x = 2;
await Promise.resolve();
expect(out.length).toBe(1);
expect(out[0][0].path.join(".")).toBe("x");
stop();
});
it("emits patches for Set structural mutations (add/delete)", async () => {
const state = deepSignal<{ s: Set<number> }>({ s: new Set([1, 2]) });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) =>
batches.push(patches)
);
state.s.add(3);
state.s.delete(1);
await Promise.resolve();
expect(batches.length >= 1).toBe(true);
const allPaths = batches.flatMap((b) => b.map((p) => p.path.join(".")));
expect(allPaths.some((p) => p.startsWith("s."))).toBe(true);
stop();
});
it("emits patches for nested objects added after initialization", async () => {
const state = deepSignal<{ root: any }>({ root: {} });
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
state.root.child = { level: { value: 1 } };
state.root.child.level.value = 2;
await Promise.resolve();
const flat = patches.flat().map((p) => p.path.join("."));
expect(flat).toContain("root.child");
expect(flat).toContain("root.child.level.value");
stop();
});
it("emits structural patches for sets of sets", async () => {
const innerA = new Set<any>([{ id: "node1", x: 1 }]);
const s = new Set<any>([innerA]);
const state = deepSignal<{ graph: Set<any> }>({ graph: s });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) =>
batches.push(patches)
);
const innerB = new Set<any>([{ id: "node2", x: 5 }]);
state.graph.add(innerB);
([...innerA][0] as any).x = 2;
await Promise.resolve();
const pathStrings = batches.flat().map((p) => p.path.join("."));
expect(pathStrings.some((p) => p.startsWith("graph."))).toBe(true);
stop();
});
it("tracks deep nested object mutation inside a Set entry after iteration", async () => {
const rawEntry = { id: "n1", data: { val: 1 } };
const st = deepSignal({ bag: new Set<any>([rawEntry]) });
const collected: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
collected.push(patches)
);
let proxied: any;
for (const e of st.bag.values()) {
proxied = e;
e.data.val;
}
proxied.data.val = 2;
await Promise.resolve();
const flat = collected.flat().map((p: DeepPatch) => p.path.join("."));
expect(flat.some((p: string) => p.endsWith("n1.data.val"))).toBe(true);
stop();
});
it("allows custom synthetic id for Set entry", async () => {
const node = { name: "x" };
const state = deepSignal({ s: new Set<any>() });
const collected2: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) =>
collected2.push(patches)
);
addWithId(state.s as any, node, "custom123");
await Promise.resolve();
const flat = collected2.flat().map((p: DeepPatch) => p.path.join("."));
expect(flat.some((p: string) => p === "s.custom123")).toBe(true);
stop();
});
describe("Set", () => {
it("emits single structural patch on Set.clear()", async () => {
const st = deepSignal({ s: new Set<any>() });
addWithId(st.s as any, { id: "a", x: 1 }, "a");
addWithId(st.s as any, { id: "b", x: 2 }, "b");
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
st.s.clear();
await Promise.resolve();
const all = batches.flat().map((p) => p.path.join("."));
expect(all).toEqual(["s"]);
stop();
});
it("emits delete patch for object entry", async () => {
const st = deepSignal({ s: new Set<any>() });
const obj = { id: "n1", x: 1 };
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.add(obj);
st.s.delete(obj);
await Promise.resolve();
const all = patches
.flat()
.filter((p) => p.op === "remove")
.map((p) => p.path.join("."));
expect(all).toContain("s.n1");
stop();
});
it("does not emit patch for duplicate add", async () => {
const st = deepSignal({ s: new Set<number>([1]) });
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.add(1);
await Promise.resolve();
expect(patches.length).toBe(0);
stop();
});
it("does not emit patch deleting non-existent entry", async () => {
const st = deepSignal({ s: new Set<number>([1]) });
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.delete(2);
await Promise.resolve();
expect(patches.length).toBe(0);
stop();
});
it("addWithId primitive returns primitive and emits patch with primitive key", async () => {
const st = deepSignal({ s: new Set<any>() });
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
const ret = addWithId(st.s as any, 5, "ignored");
expect(ret).toBe(5);
await Promise.resolve();
const paths = patches.flat().map((p) => p.path.join("."));
expect(paths).toContain("s.5");
stop();
});
it("setSetEntrySyntheticId applies custom id without helper", async () => {
const st = deepSignal({ s: new Set<any>() });
const obj = { name: "x" };
setSetEntrySyntheticId(obj, "customX");
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.add(obj);
await Promise.resolve();
const paths = patches.flat().map((p) => p.path.join("."));
expect(paths).toContain("s.customX");
stop();
});
it("values/entries/forEach proxy nested mutation", async () => {
const st = deepSignal({ s: new Set<any>() });
const entry = addWithId(st.s as any, { id: "e1", inner: { v: 1 } }, "e1");
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
for (const e of st.s.values()) {
e.inner.v;
}
entry.inner.v = 2;
await Promise.resolve();
const vPaths = batches.flat().map((p) => p.path.join("."));
expect(vPaths.some((p) => p.endsWith("e1.inner.v"))).toBe(true);
stop();
});
it("raw reference mutation produces no deep patch while proxied does", async () => {
const raw = { id: "id1", data: { x: 1 } };
const st = deepSignal({ s: new Set<any>([raw]) });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
raw.data.x = 2;
await Promise.resolve();
const afterRaw = batches.flat().map((p) => p.path.join("."));
expect(afterRaw.some((p) => p.endsWith("id1.data.x"))).toBe(false);
let proxied: any;
for (const e of st.s.values()) proxied = e;
proxied.data.x = 3;
await Promise.resolve();
const afterProxied = batches.flat().map((p) => p.path.join("."));
expect(afterProxied.some((p) => p.endsWith("id1.data.x"))).toBe(true);
stop();
});
it("synthetic id collision assigns unique blank node id", async () => {
const st = deepSignal({ s: new Set<any>() });
const a1 = { id: "dup", v: 1 };
const a2 = { id: "dup", v: 2 };
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch)
);
st.s.add(a1);
st.s.add(a2);
await Promise.resolve();
const keys = patches
.flat()
.filter((p) => p.op === "add")
.map((p) => p.path.slice(-1)[0]);
expect(new Set(keys).size).toBe(2);
stop();
});
});
describe("Arrays & mixed batch", () => {
it("emits patches for splice/unshift/shift in single batch", async () => {
const st = deepSignal({ arr: [1, 2, 3] });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
st.arr.splice(1, 1, 99, 100);
st.arr.unshift(0);
st.arr.shift();
await Promise.resolve();
const paths = batches.flat().map((p) => p.path.join("."));
expect(paths.some((p) => p.startsWith("arr."))).toBe(true);
stop();
});
it("mixed object/array/Set mutations batch together", async () => {
const st = deepSignal({ o: { a: 1 }, arr: [1], s: new Set<any>() });
const batches: DeepPatch[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches)
);
st.o.a = 2;
st.arr.push(2);
addWithId(st.s as any, { id: "z", v: 1 }, "z");
await Promise.resolve();
expect(batches.length).toBe(1);
const paths = batches[0].map((p) => p.path.join("."));
expect(paths).toContain("o.a");
expect(paths).toContain("arr.1");
expect(paths.some((p) => p.startsWith("s."))).toBe(true);
stop();
});
});
});

@ -0,0 +1,41 @@
export const objectToString: typeof Object.prototype.toString =
Object.prototype.toString
export const toTypeString = (value: unknown): string =>
objectToString.call(value)
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol,
): key is keyof typeof val => hasOwnProperty.call(val, key)
export const isArray: typeof Array.isArray = Array.isArray
export const isMap = (val: unknown): val is Map<any, any> =>
toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> =>
toTypeString(val) === '[object Set]'
export const isDate = (val: unknown): val is Date =>
toTypeString(val) === '[object Date]'
export const isRegExp = (val: unknown): val is RegExp =>
toTypeString(val) === '[object RegExp]'
export const isFunction = (val: unknown): val is Function =>
typeof val === 'function'
export const isString = (val: unknown): val is string => typeof val === 'string'
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return (
(isObject(val) || isFunction(val)) &&
isFunction((val as any).then) &&
isFunction((val as any).catch)
)
}
export const isPlainObject = (val: unknown): val is object =>
toTypeString(val) === '[object Object]'
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
export function NOOP() {}

@ -0,0 +1,188 @@
import { isObject, isPlainObject, isSet, isMap, isArray } from "./utils";
import { isSignal } from "./core";
import {
isDeepSignal,
subscribeDeepMutations,
getDeepSignalRootId,
DeepPatch,
} from "./deepSignal";
import { ReactiveFlags } from "./contents";
/** Function provided to register a disposer (runs before next callback or on stop). */
export type RegisterCleanup = (cleanupFn: () => void) => void;
/** Signature for watchEffect style sources receiving the cleanup registrar. */
export type WatchEffect = (registerCleanup: RegisterCleanup) => void;
/** Options for {@link watch}. */
export interface WatchOptions {
/** Trigger the callback immediately with the current value (default: false). */
immediate?: boolean;
/** Auto-stop the watcher after the first callback run that delivers patches (or immediate call if no patches). */
once?: boolean;
/** Allow legacy/unknown options (ignored) to avoid hard breaks while migrating. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[legacy: string]: any;
}
export interface WatchPatchEvent<Root = any> {
/** Patch batch that triggered this callback (may be empty for immediate). */
patches: DeepPatch[];
/** Previous snapshot (deep-cloned) of the root value before these patches. Undefined on first call. */
oldValue: Root | undefined;
/** Current root value (live proxy). */
newValue: Root;
}
export type WatchPatchCallback<Root = any> = (
event: WatchPatchEvent<Root>
) => any;
// Internal helper kept for external compatibility.
export const remove = <T>(arr: T[], el: T): void => {
const i = arr.indexOf(el);
if (i > -1) arr.splice(i, 1);
};
/** Observe patch batches on a deep signal root. */
export function watch<Root = any>(
source: Root,
callback: WatchPatchCallback<Root>,
options: WatchOptions = {}
) {
if (!isDeepSignal(source)) {
throw new Error(
"watch() now only supports deepSignal roots (patch mode only)"
);
}
const { immediate, once } = options;
const rootId = getDeepSignalRootId(source as any)!;
let active = true;
let cleanup: (() => void) | undefined;
const registerCleanup: RegisterCleanup = (fn) => {
cleanup = fn;
};
const runCleanup = () => {
if (cleanup) {
try {
cleanup();
} catch {
/* ignore */
} finally {
cleanup = undefined;
}
}
};
// Deep clone snapshot helper (JSON clone sufficient for typical reactive plain data)
const clone = (v: any) => {
try {
return JSON.parse(JSON.stringify(v));
} catch {
return undefined as any;
}
};
let lastSnapshot: Root | undefined = clone(source);
const stopListening = () => {
if (!active) return;
active = false;
runCleanup();
unsubscribe && unsubscribe();
};
const deliver = (patches: DeepPatch[]) => {
if (!active) return;
runCleanup();
const prev = lastSnapshot;
const next = source as any as Root; // live proxy
try {
callback({
patches,
oldValue: prev,
newValue: next,
});
} finally {
if (active) lastSnapshot = clone(next);
if (once) stopListening();
}
};
const unsubscribe = subscribeDeepMutations(rootId, (patches) => {
if (!patches.length) return; // ignore empty batches
deliver(patches);
});
if (immediate) {
// Immediate call with empty patch list (snapshot only)
deliver([]);
}
return {
/** Stop listening to future patch batches; idempotent. */
stopListening,
/** Register a cleanup callback run before the next invocation / stop. */
registerCleanup,
};
}
// observe alias
export function observe(
source: any,
cb: WatchPatchCallback,
options?: WatchOptions
) {
return watch(source, cb, options);
}
// Instrumentation counter for performance tests (number of traverse invocations)
/** Instrumentation counter tracking total `traverse()` invocations (used in tests). */
export let __traverseCount = 0; // retained for external tooling/tests although watch no longer uses traversal
/** Reset the traversal instrumentation counter back to 0. */
export function __resetTraverseCount() {
__traverseCount = 0;
}
/**
* Recursively touch (read) nested properties/entries/values of a reactive structure for dependency collection.
* Depth-limited; protects against cycles via `seen` set; respects ReactiveFlags.SKIP opt-out.
*/
export function traverse(
value: unknown,
depth: number = Infinity,
seen?: Set<unknown>
): unknown {
__traverseCount++;
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value;
}
seen = seen || new Set();
if (seen.has(value)) {
return value;
}
seen.add(value);
depth--;
if (isSignal(value)) {
traverse((value as any)(), depth, seen);
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], depth, seen);
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, depth, seen);
});
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], depth, seen);
}
for (const key of Object.getOwnPropertySymbols(value)) {
if (Object.prototype.propertyIsEnumerable.call(value, key)) {
traverse(value[key as any], depth, seen);
}
}
}
return value;
}

@ -0,0 +1,32 @@
import { effect as coreEffect } from "./core";
/** Run a reactive function and re-run on its dependencies; supports cleanup. */
export function watchEffect(
fn: (registerCleanup?: (cleanup: () => void) => void) => void
) {
let cleanup: (() => void) | undefined;
const registerCleanup = (cb: () => void) => {
cleanup = cb;
};
const stop = coreEffect(() => {
if (cleanup) {
try {
cleanup();
} catch {
/* ignore */
} finally {
cleanup = undefined;
}
}
fn(registerCleanup);
});
return () => {
if (cleanup) {
try {
cleanup();
} catch {
/* ignore */
}
}
stop();
};
}

@ -1,5 +0,0 @@
import { vitePreprocess } from "@astrojs/svelte";
export default {
preprocess: vitePreprocess(),
};

@ -1,8 +1,25 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": "."
}
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020"
],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": false,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
},
"include": [
"src"
]
}

@ -0,0 +1,14 @@
import type { Options } from 'tsup'
export const tsup: Options = {
entry: [
'src/index.ts',
],
format: ['esm', 'cjs'],
dts: true,
splitting: true,
clean: true,
shims: false,
minify: false,
external: ['alien-signals'],
}
Loading…
Cancel
Save