commit
1dad780114
@ -0,0 +1,22 @@ |
||||
# Logs |
||||
logs |
||||
*.log |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
pnpm-debug.log* |
||||
lerna-debug.log* |
||||
|
||||
node_modules |
||||
dist |
||||
dist-ssr |
||||
*.local |
||||
|
||||
# Editor directories and files |
||||
.vscode/* |
||||
!.vscode/extensions.json |
||||
.idea |
||||
.DS_Store |
||||
*.suo |
||||
*.sln |
||||
*.sw? |
@ -0,0 +1,49 @@ |
||||
# 🧶 AlienDeepSignals |
||||
|
||||
Use [alien-signals](https://github.com/stackblitz/alien-signals) with the interface of a plain JavaScript object. |
||||
|
||||
- **DeepSignal** works by wrapping the object with a `Proxy` that intercepts all property accesses and returns the signal value by default. |
||||
- This allows you to easily create a **deep object that can be observed for changes**, while still being able to **mutate the object normally**. |
||||
- Nested objects and arrays are also converted to deep signal objects/arrays, allowing you to create **fully reactive data structures**. |
||||
- The `$` prefix returns the signal instance: `state.$prop`. |
||||
|
||||
## Credits |
||||
|
||||
- The ability of deepsignal comes from [deepsignal](https://github.com/luisherranz/deepsignal) |
||||
|
||||
## Features |
||||
|
||||
- **Transparent**: `deepsignal` wraps the object with a proxy that intercepts all property accesses, but does not modify how you interact with the object. This means that you can still use the object as you normally would, and it will behave exactly as you would expect, except that mutating the object also updates the value of the underlying signals. |
||||
- **Tiny (less than 1kB)**: `deepsignal` is designed to be lightweight and has a minimal footprint, making it easy to include in your projects. It's just a small wrapper around `@preact/signals-core`. |
||||
- **Full array support**: `deepsignal` fully supports arrays, including nested arrays. |
||||
- **Deep**: `deepsignal` converts nested objects and arrays to deep signal objects/arrays, allowing you to create fully reactive data structures. |
||||
- **Lazy initialization**: `deepsignal` uses lazy initialization, which means that signals and proxies are only created when they are accessed for the first time. This reduces the initialization time to almost zero and improves the overall performance in cases where you only need to observe a small subset of the object's properties. |
||||
- **Stable references**: `deepsignal` uses stable references, which means that the same `Proxy` instances will be returned for the same objects so they can exist in different places of the data structure, just like regular JavaScript objects. |
||||
- **Automatic derived state**: getters are automatically converted to computeds instead of signals. |
||||
- **TypeScript support**: `deepsignal` is written in TypeScript and includes type definitions, so you can use it seamlessly with your TypeScript projects, including access to the signal value through the prefix `state.$prop`. |
||||
- **State management**: `deepsignal` can be used as a state manager, including state and actions in the same object. |
||||
|
||||
The most important feature is that **it just works**. You don't need to do anything special. Just create an object, mutate it normally and all your components will know when they need to rerender. |
||||
|
||||
## Installation |
||||
|
||||
```bash |
||||
npm install alien-deepsignals |
||||
``` |
||||
|
||||
## Usage |
||||
|
||||
```ts |
||||
import { deepSignal } from 'alien-deepsignals'; |
||||
const state = deepSignal({ |
||||
count: 0, |
||||
name: 'John', |
||||
nested: { |
||||
deep: 'value', |
||||
}, |
||||
array: [1, 2, 3], |
||||
}); |
||||
state.count++; |
||||
state.$nested.deep = 'new value'; |
||||
state.$array.push(4); |
||||
``` |
@ -0,0 +1,36 @@ |
||||
import { defineConfig } from '@farmfe/core'; |
||||
import farmDtsPlugin from '@farmfe/js-plugin-dts'; |
||||
|
||||
const format = (process.env.FARM_FORMAT as 'esm' | 'cjs') || 'esm'; |
||||
const ext = format === 'esm' ? 'mjs' : 'cjs'; |
||||
|
||||
export default defineConfig({ |
||||
compilation: { |
||||
input: { |
||||
index: './src/index.ts', |
||||
}, |
||||
output: { |
||||
path: `dist/${format}`, |
||||
entryFilename: `[entryName].${ext}`, |
||||
targetEnv: 'library', |
||||
format, |
||||
clean: false, |
||||
}, |
||||
external: ['!^(\\./|\\.\\./|[A-Za-z]:\\\\|/|^@/).*'], |
||||
partialBundling: { |
||||
enforceResources: [ |
||||
{ |
||||
name: 'index', |
||||
test: ['.+'], |
||||
}, |
||||
], |
||||
}, |
||||
minify: false, |
||||
sourcemap: false, |
||||
presetEnv: false, |
||||
lazyCompilation: false, |
||||
persistentCache: true, |
||||
externalNodeBuiltins: false, |
||||
}, |
||||
plugins: [farmDtsPlugin()], |
||||
}); |
@ -0,0 +1,24 @@ |
||||
{ |
||||
"name": "alien-deepsignals", |
||||
"version": "0.0.1", |
||||
"type": "module", |
||||
"scripts": { |
||||
"dev": "farm", |
||||
"start": "farm", |
||||
"build": "farm build", |
||||
"preview": "farm preview", |
||||
"clean": "farm clean", |
||||
"test": "vitest" |
||||
}, |
||||
"devDependencies": { |
||||
"@farmfe/cli": "^1.0.2", |
||||
"@farmfe/core": "^1.3.0", |
||||
"typescript": "^5.4.3", |
||||
"vitest": "^2.1.8" |
||||
}, |
||||
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387", |
||||
"dependencies": { |
||||
"@farmfe/js-plugin-dts": "^0.6.4", |
||||
"alien-signals": "^0.4.12" |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@ |
||||
import { Computed as AlienComputed } from 'alien-signals'; |
||||
|
||||
export function computed<T>(getter: (cachedValue?: T) => T): Computed<T> { |
||||
return new Computed<T>(getter); |
||||
} |
||||
|
||||
export class Computed<T = any> extends AlienComputed { |
||||
constructor(getter: (cachedValue?: T) => T) { |
||||
super(getter); |
||||
} |
||||
get value(): T { |
||||
return this.get(); |
||||
} |
||||
} |
@ -0,0 +1,307 @@ |
||||
import { computed } from "./computed"; |
||||
import { signal, Signal } from "./signal" |
||||
|
||||
const proxyToSignals = new WeakMap(); |
||||
const objToProxy = new WeakMap(); |
||||
const arrayToArrayOfSignals = new WeakMap(); |
||||
const ignore = new WeakSet(); |
||||
const objToIterable = new WeakMap(); |
||||
const rg = /^\$/; |
||||
const descriptor = Object.getOwnPropertyDescriptor; |
||||
let peeking = false; |
||||
|
||||
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)) |
||||
objToProxy.set(obj, createProxy(obj, objectHandlers) as DeepSignal<T>); |
||||
return objToProxy.get(obj); |
||||
}; |
||||
|
||||
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 isShallow = Symbol("shallow"); |
||||
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>) => { |
||||
const proxy = new Proxy(target, handlers); |
||||
ignore.add(proxy); |
||||
return proxy; |
||||
}; |
||||
|
||||
const throwOnMutation = () => { |
||||
throw new Error("Don't mutate the signals directly."); |
||||
}; |
||||
|
||||
const get = |
||||
(isArrayOfSignals: boolean) => |
||||
(target: object, fullKey: string, receiver: object): unknown => { |
||||
if (peeking) return Reflect.get(target, fullKey, receiver); |
||||
let returnSignal = isArrayOfSignals || fullKey[0] === "$"; |
||||
if (!isArrayOfSignals && returnSignal && Array.isArray(target)) { |
||||
if (fullKey === "$") { |
||||
if (!arrayToArrayOfSignals.has(target)) |
||||
arrayToArrayOfSignals.set(target, createProxy(target, arrayHandlers)); |
||||
return arrayToArrayOfSignals.get(target); |
||||
} |
||||
returnSignal = fullKey === "$length"; |
||||
} |
||||
if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map()); |
||||
const signals = proxyToSignals.get(receiver); |
||||
const key = returnSignal ? fullKey.replace(rg, "") : fullKey; |
||||
if ( |
||||
!signals.has(key) && |
||||
typeof descriptor(target, key)?.get === "function" |
||||
) { |
||||
console.log("get", target, fullKey, receiver, key); |
||||
|
||||
signals.set( |
||||
key, |
||||
computed(() => Reflect.get(target, key, receiver)) |
||||
); |
||||
} else { |
||||
let value = Reflect.get(target, key, receiver); |
||||
if (returnSignal && typeof value === "function") return; |
||||
if (typeof key === "symbol" && wellKnownSymbols.has(key)) return value; |
||||
if (!signals.has(key)) { |
||||
if (shouldProxy(value)) { |
||||
if (!objToProxy.has(value)) |
||||
objToProxy.set(value, createProxy(value, objectHandlers)); |
||||
value = objToProxy.get(value); |
||||
} |
||||
signals.set(key, signal(value)); |
||||
} |
||||
} |
||||
return returnSignal ? signals.get(key) : signals.get(key).get(); |
||||
}; |
||||
|
||||
const objectHandlers = { |
||||
get: get(false), |
||||
set(target: object, fullKey: string, val: any, receiver: object): boolean { |
||||
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 (!(val instanceof Signal)) 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)) |
||||
objToProxy.set(val, createProxy(val, objectHandlers)); |
||||
internal = objToProxy.get(val); |
||||
} |
||||
const isNew = !(fullKey in target); |
||||
const result = Reflect.set(target, fullKey, val, receiver); |
||||
|
||||
if (!signals.has(fullKey)) signals.set(fullKey, signal(internal)); |
||||
else 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); |
||||
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++; |
||||
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); |
||||
}, |
||||
}; |
||||
|
||||
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") |
||||
); |
||||
const supported = new Set([Object, Array]); |
||||
const shouldProxy = (val: any): boolean => { |
||||
if (typeof val !== "object" || val === null) return false; |
||||
return supported.has(val.constructor) && !ignore.has(val); |
||||
}; |
||||
|
||||
/** TYPES **/ |
||||
|
||||
export type DeepSignal<T> = T extends Function |
||||
? T |
||||
: T extends { [isShallow]: true } |
||||
? T |
||||
: T extends Array<unknown> |
||||
? DeepSignalArray<T> |
||||
: T extends object |
||||
? DeepSignalObject<T> |
||||
: T; |
||||
|
||||
type DeepSignalObject<T extends object> = { |
||||
[P in keyof T & string as `$${P}`]?: T[P] extends Function |
||||
? never |
||||
: Signal<T[P]>; |
||||
} & { |
||||
[P in keyof T]: DeepSignal<T[P]>; |
||||
}; |
||||
|
||||
/** @ts-expect-error **/ |
||||
interface DeepArray<T> extends 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; |
||||
} |
||||
type ArrayType<T> = T extends Array<infer I> ? I : T; |
||||
type DeepSignalArray<T> = DeepArray<ArrayType<T>> & { |
||||
[key: number]: DeepSignal<ArrayType<T>>; |
||||
$?: { [key: number]: Signal<ArrayType<T>> }; |
||||
$length?: Signal<number>; |
||||
}; |
||||
|
||||
export type Shallow<T extends object> = T & { [isShallow]: true }; |
||||
|
||||
export declare const useDeepSignal: <T extends object>(obj: T) => DeepSignal<T>; |
||||
|
||||
type FilterSignals<K> = K extends `$${infer P}` ? never : K; |
||||
type RevertDeepSignalObject<T> = Pick<T, FilterSignals<keyof T>>; |
||||
type RevertDeepSignalArray<T> = Omit<T, "$" | "$length">; |
||||
|
||||
export type RevertDeepSignal<T> = T extends Array<unknown> |
||||
? RevertDeepSignalArray<T> |
||||
: T extends object |
||||
? RevertDeepSignalObject<T> |
||||
: T; |
||||
|
||||
type WellKnownSymbols = |
||||
| "asyncIterator" |
||||
| "hasInstance" |
||||
| "isConcatSpreadable" |
||||
| "iterator" |
||||
| "match" |
||||
| "matchAll" |
||||
| "replace" |
||||
| "search" |
||||
| "species" |
||||
| "split" |
||||
| "toPrimitive" |
||||
| "toStringTag" |
||||
| "unscopables"; |
@ -0,0 +1,3 @@ |
||||
export * from "./computed"; |
||||
export * from "./signal"; |
||||
export * from "./deepSignal"; |
@ -0,0 +1,26 @@ |
||||
import { Signal as AlienSignal } from 'alien-signals'; |
||||
export function signal<T>(): Signal<T | undefined>; |
||||
export function signal<T>(oldValue: T): Signal<T>; |
||||
export function signal<T>(oldValue?: T): Signal<T | undefined> { |
||||
return new Signal(oldValue); |
||||
} |
||||
|
||||
export class Signal<T = any> extends AlienSignal { |
||||
constructor( |
||||
currentValue: T |
||||
) { |
||||
super(currentValue); |
||||
} |
||||
|
||||
get value(): T { |
||||
return this.get() |
||||
} |
||||
|
||||
set value(value: T) { |
||||
this.set(value) |
||||
} |
||||
|
||||
peek(): T { |
||||
return this.currentValue; |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"target": "ES2020", |
||||
"useDefineForClassFields": true, |
||||
"module": "ESNext", |
||||
"lib": [ |
||||
"ES2020", |
||||
"DOM", |
||||
"DOM.Iterable" |
||||
], |
||||
"skipLibCheck": true, |
||||
/* Bundler mode */ |
||||
"moduleResolution": "bundler", |
||||
"allowImportingTsExtensions": true, |
||||
"resolveJsonModule": true, |
||||
"isolatedModules": true, |
||||
"noEmit": true, |
||||
/* Linting */ |
||||
"strict": true, |
||||
"noUnusedLocals": true, |
||||
"noUnusedParameters": true, |
||||
"noFallthroughCasesInSwitch": true |
||||
}, |
||||
"include": [ |
||||
"./src", |
||||
] |
||||
} |
Loading…
Reference in new issue