@echojs-ecosystem/reactivity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ <div align="center">
2
+
3
+ # @echojs-ecosystem/reactivity
4
+
5
+ **Fine-grained reactive primitives — signals, computed, effects, and batching.**
6
+
7
+ [![npm](https://img.shields.io/npm/v/@echojs-ecosystem/reactivity)](https://www.npmjs.com/package/@echojs-ecosystem/reactivity)
8
+ [![docs](https://img.shields.io/badge/docs-echojs.dev-blue)](https://echojs.dev/docs/packages/reactivity)
9
+
10
+ </div>
11
+
12
+ ---
13
+
14
+ The reactive foundation of EchoJS. Built on a fast signal engine, wrapped in a **strict, object-oriented API** that encourages predictable updates and immutable patterns.
15
+
16
+ ## Features
17
+
18
+ - **`signal`** — writable cells with `.value()`, `.set()`, `.update()`, `.peek()`, `.subscribe()`
19
+ - **`computed`** — lazy derived values with automatic dependency tracking
20
+ - **`effect` / `scope`** — side effects with automatic cleanup
21
+ - **`batch`** — coalesce multiple writes into one notification wave
22
+ - **`DeepReadonly`** — object reads cannot be mutated through `.value()`; use `.set()` / `.update()`
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install @echojs-ecosystem/reactivity
28
+ # bun add @echojs-ecosystem/reactivity
29
+ # pnpm add @echojs-ecosystem/reactivity
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ ```ts
35
+ import { signal, computed, effect, batch } from "@echojs-ecosystem/reactivity";
36
+
37
+ const $count = signal(0);
38
+ const $double = computed(() => $count.value() * 2);
39
+
40
+ effect(() => {
41
+ console.log("count:", $count.value(), "double:", $double.value());
42
+ });
43
+
44
+ batch(() => {
45
+ $count.set(1);
46
+ $count.update((n) => n + 1);
47
+ });
48
+ ```
49
+
50
+ ## API
51
+
52
+ | Export | Description |
53
+ |--------|-------------|
54
+ | `signal(initial)` | Writable reactive cell |
55
+ | `computed(getter)` | Readonly derived signal |
56
+ | `effect(fn)` | Runs immediately, re-runs on dependency changes; returns disposer |
57
+ | `scope(fn)` | Effect scope — nested effects cleaned up together |
58
+ | `batch(fn)` | Defer reactions until the batch completes |
59
+ | `readonly($sig)` | Readonly facade without `.set()` / `.update()` |
60
+ | `isSignal` / `isReadonlySignal` | Runtime type guards |
61
+
62
+ ### Design notes
63
+
64
+ - **Write only via `.set()` / `.update()`** — no public `trigger()` API
65
+ - **`subscribe()`** fires on changes only, not on initial subscribe
66
+ - **Dev mode** deep-freezes object/array values written to signals
67
+
68
+ ## Related packages
69
+
70
+ | Package | Role |
71
+ |---------|------|
72
+ | [`@echojs-ecosystem/hyperdom`](https://www.npmjs.com/package/@echojs-ecosystem/hyperdom) | DOM views wired to signals |
73
+ | [`@echojs-ecosystem/store`](https://www.npmjs.com/package/@echojs-ecosystem/store) | Structured app state on top of signals |
74
+ | [`@echojs-ecosystem/framework`](https://www.npmjs.com/package/@echojs-ecosystem/framework) | Meta-package — entire ecosystem via subpath imports |
75
+
76
+ ## Documentation
77
+
78
+ [echojs.dev/docs/packages/reactivity](https://echojs.dev/docs/packages/reactivity)
@@ -0,0 +1,34 @@
1
+ declare const batch: <T>(fn: () => T) => T;
2
+
3
+ type CleanupFn = () => void;
4
+ declare const cleanup: (fn: CleanupFn) => void;
5
+
6
+ type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? readonly DeepReadonly<U>[] : T extends object ? {
7
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
8
+ } : T;
9
+ type ReadValue<T> = T extends object ? DeepReadonly<T> : T;
10
+ interface ReadonlySignal<T> {
11
+ value(): ReadValue<T>;
12
+ peek(): ReadValue<T>;
13
+ subscribe(fn: () => void): () => void;
14
+ }
15
+ interface Signal<T> extends ReadonlySignal<T> {
16
+ set(next: T): void;
17
+ update(fn: (prev: T) => T): void;
18
+ readonly(): ReadonlySignal<T>;
19
+ }
20
+
21
+ declare const computed: <T>(getter: () => T) => ReadonlySignal<T>;
22
+
23
+ declare const effect: (fn: () => void) => (() => void);
24
+
25
+ declare const readonly: <T>(sig: Signal<T> | ReadonlySignal<T>) => ReadonlySignal<T>;
26
+
27
+ declare const scope: (fn: () => void) => (() => void);
28
+
29
+ declare const signal: <T>(initial: T) => Signal<T>;
30
+
31
+ declare const isSignal: (value: unknown) => value is Signal<unknown> | ReadonlySignal<unknown>;
32
+ declare const isReadonlySignal: (value: unknown) => value is ReadonlySignal<unknown>;
33
+
34
+ export { type DeepReadonly, type ReadValue, type ReadonlySignal, type Signal, batch, cleanup, computed, effect, isReadonlySignal, isSignal, readonly, scope, signal };
package/dist/index.js ADDED
@@ -0,0 +1,250 @@
1
+ import { startBatch, endBatch, computed as computed$1, effect as effect$1, effectScope, signal as signal$1, setActiveSub } from 'alien-signals';
2
+
3
+ // src/utils.ts
4
+ var isObjectLike = (value) => {
5
+ return typeof value === "object" && value !== null;
6
+ };
7
+ var isFunction = (value) => {
8
+ return typeof value === "function";
9
+ };
10
+ var createAlienSignal = (initial) => {
11
+ return signal$1(initial);
12
+ };
13
+ var createAlienComputed = (getter) => {
14
+ return computed$1(getter);
15
+ };
16
+ var createAlienEffect = (fn) => {
17
+ return effect$1(fn);
18
+ };
19
+ var createAlienScope = (fn) => {
20
+ return effectScope(fn);
21
+ };
22
+ var batch = (fn) => {
23
+ startBatch();
24
+ try {
25
+ return fn();
26
+ } finally {
27
+ endBatch();
28
+ }
29
+ };
30
+ var untrack = (fn) => {
31
+ const prev = setActiveSub(void 0);
32
+ try {
33
+ return fn();
34
+ } finally {
35
+ setActiveSub(prev);
36
+ }
37
+ };
38
+
39
+ // src/batch.ts
40
+ var batch2 = (fn) => {
41
+ if (!isFunction(fn)) {
42
+ throw new TypeError("batch(fn) expects a function");
43
+ }
44
+ return batch(fn);
45
+ };
46
+
47
+ // src/cleanup.ts
48
+ var currentBucket;
49
+ var __withCleanupBucket = (bucket, fn) => {
50
+ const prev = currentBucket;
51
+ currentBucket = bucket;
52
+ try {
53
+ return fn();
54
+ } finally {
55
+ currentBucket = prev;
56
+ }
57
+ };
58
+ var __runCleanupBucket = (bucket) => {
59
+ for (let i = bucket.length - 1; i >= 0; i--) {
60
+ try {
61
+ bucket[i]?.();
62
+ } catch {
63
+ }
64
+ }
65
+ bucket.length = 0;
66
+ };
67
+ var cleanup = (fn) => {
68
+ if (!isFunction(fn)) {
69
+ throw new TypeError("cleanup(fn) expects a function");
70
+ }
71
+ if (!currentBucket) {
72
+ throw new Error("cleanup(fn) must be called inside scope()");
73
+ }
74
+ currentBucket.push(fn);
75
+ };
76
+ var __wrapDisposerWithCleanup = (dispose, bucket) => {
77
+ let disposed = false;
78
+ return () => {
79
+ if (disposed) return;
80
+ disposed = true;
81
+ try {
82
+ dispose();
83
+ } finally {
84
+ __runCleanupBucket(bucket);
85
+ }
86
+ };
87
+ };
88
+
89
+ // src/subscribe.ts
90
+ var createSubscribe = (readTracked) => {
91
+ return (fn) => {
92
+ if (!isFunction(fn)) {
93
+ throw new TypeError("subscribe(fn) expects a function");
94
+ }
95
+ let inited = false;
96
+ let prev;
97
+ const stop = createAlienEffect(() => {
98
+ const next = readTracked();
99
+ if (!inited) {
100
+ inited = true;
101
+ prev = next;
102
+ return;
103
+ }
104
+ if (Object.is(prev, next)) return;
105
+ prev = next;
106
+ fn();
107
+ });
108
+ return stop;
109
+ };
110
+ };
111
+
112
+ // src/internals/guards.ts
113
+ var kSignalBrand = /* @__PURE__ */ Symbol.for("echojs-ecosystem.reactivity.signal");
114
+ var kReadonlyBrand = /* @__PURE__ */ Symbol.for("echojs-ecosystem.reactivity.readonlySignal");
115
+ var brandWritable = (obj) => {
116
+ Object.defineProperty(obj, kSignalBrand, { value: true });
117
+ return obj;
118
+ };
119
+ var brandReadonly = (obj) => {
120
+ Object.defineProperty(obj, kSignalBrand, { value: true });
121
+ Object.defineProperty(obj, kReadonlyBrand, { value: true });
122
+ return obj;
123
+ };
124
+ var isBrandedSignal = (value) => {
125
+ return typeof value === "object" && value !== null && value[kSignalBrand] === true;
126
+ };
127
+ var isBrandedReadonlySignal = (value) => {
128
+ return typeof value === "object" && value !== null && value[kSignalBrand] === true && value[kReadonlyBrand] === true;
129
+ };
130
+
131
+ // src/computed.ts
132
+ var computed = (getter) => {
133
+ if (!isFunction(getter)) {
134
+ throw new TypeError("computed(getter) expects a function");
135
+ }
136
+ const engine = createAlienComputed(() => getter());
137
+ const readTracked = () => engine();
138
+ const readUntracked = () => untrack(() => engine());
139
+ const subscribe = createSubscribe(readTracked);
140
+ return brandReadonly({
141
+ value: () => readTracked(),
142
+ peek: () => readUntracked(),
143
+ subscribe
144
+ });
145
+ };
146
+
147
+ // src/effect.ts
148
+ var effect = (fn) => {
149
+ if (!isFunction(fn)) {
150
+ throw new TypeError("effect(fn) expects a function");
151
+ }
152
+ return createAlienEffect(fn);
153
+ };
154
+
155
+ // src/readonly.ts
156
+ var readonly = (sig) => {
157
+ if (!isFunction(sig.set)) return sig;
158
+ if (!isFunction(sig.readonly)) return sig.readonly();
159
+ return brandReadonly({
160
+ value: () => sig.value(),
161
+ peek: () => sig.peek(),
162
+ subscribe: (fn) => sig.subscribe(fn)
163
+ });
164
+ };
165
+
166
+ // src/scope.ts
167
+ var scope = (fn) => {
168
+ if (!isFunction(fn)) {
169
+ throw new TypeError("scope(fn) expects a function");
170
+ }
171
+ const bucket = [];
172
+ const dispose = createAlienScope(() => __withCleanupBucket(bucket, fn));
173
+ return __wrapDisposerWithCleanup(dispose, bucket);
174
+ };
175
+
176
+ // src/freeze.ts
177
+ var DEV = (() => {
178
+ const env = globalThis?.process?.env;
179
+ const nodeEnv = env?.NODE_ENV;
180
+ return nodeEnv !== "production";
181
+ })();
182
+ var deepFreezeImpl = (value, seen) => {
183
+ if (!isObjectLike(value)) return;
184
+ if (Object.isFrozen(value)) return;
185
+ if (seen.has(value)) return;
186
+ seen.add(value);
187
+ const keys = Object.keys(value);
188
+ for (const k of keys) {
189
+ deepFreezeImpl(value[k], seen);
190
+ }
191
+ Object.freeze(value);
192
+ };
193
+ var freezeIfDev = (value) => {
194
+ if (!DEV) return value;
195
+ if (!isObjectLike(value)) return value;
196
+ deepFreezeImpl(value, /* @__PURE__ */ new WeakSet());
197
+ return value;
198
+ };
199
+
200
+ // src/signal.ts
201
+ var createReadonlySignalFacade = (impl) => {
202
+ return brandReadonly({
203
+ value: impl.value,
204
+ peek: impl.peek,
205
+ subscribe: impl.subscribe
206
+ });
207
+ };
208
+ var signalImpl = (...args) => {
209
+ if (args.length === 0) {
210
+ throw new TypeError("signal(initial) expects 1 argument");
211
+ }
212
+ const initial = args[0];
213
+ const engine = createAlienSignal(freezeIfDev(initial));
214
+ const readTracked = () => engine();
215
+ const readUntracked = () => untrack(() => engine());
216
+ const subscribe = createSubscribe(readTracked);
217
+ const readonlyFacade = createReadonlySignalFacade({
218
+ value: () => readTracked(),
219
+ peek: () => readUntracked(),
220
+ subscribe
221
+ });
222
+ const writable = brandWritable({
223
+ value: () => readTracked(),
224
+ peek: () => readUntracked(),
225
+ subscribe,
226
+ set: (next) => {
227
+ engine(freezeIfDev(next));
228
+ },
229
+ update: (fn) => {
230
+ const prev = readUntracked();
231
+ const next = fn(prev);
232
+ engine(freezeIfDev(next));
233
+ },
234
+ readonly: () => readonlyFacade
235
+ });
236
+ return writable;
237
+ };
238
+ var signal = signalImpl;
239
+
240
+ // src/public-guards.ts
241
+ var isSignal = (value) => {
242
+ return isBrandedSignal(value);
243
+ };
244
+ var isReadonlySignal = (value) => {
245
+ return isBrandedReadonlySignal(value);
246
+ };
247
+
248
+ export { batch2 as batch, cleanup, computed, effect, isReadonlySignal, isSignal, readonly, scope, signal };
249
+ //# sourceMappingURL=index.js.map
250
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/internals/alien.ts","../src/batch.ts","../src/cleanup.ts","../src/subscribe.ts","../src/internals/guards.ts","../src/computed.ts","../src/effect.ts","../src/readonly.ts","../src/scope.ts","../src/freeze.ts","../src/signal.ts","../src/public-guards.ts"],"names":["alienSignal","alienComputed","alienEffect","alienEffectScope","alienStartBatch","alienEndBatch","alienSetActiveSub","batch"],"mappings":"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","file":"index.js","sourcesContent":["export type AnyFn = (...args: any[]) => any;\n\nexport const isObjectLike = (value: unknown): value is Record<string, unknown> => {\n return typeof value === \"object\" && value !== null;\n};\n\nexport const isPlainObject = (value: unknown): value is Record<string, unknown> => {\n if (!isObjectLike(value)) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n};\n\nexport const isFunction = (value: unknown): value is Function => {\n return typeof value === \"function\";\n};\n","import {\n computed as alienComputed,\n effect as alienEffect,\n effectScope as alienEffectScope,\n endBatch as alienEndBatch,\n setActiveSub as alienSetActiveSub,\n signal as alienSignal,\n startBatch as alienStartBatch,\n} from \"alien-signals\";\n\nexport type Disposer = () => void;\n\nexport const createAlienSignal = <T>(initial: T) => {\n return alienSignal(initial);\n};\n\nexport const createAlienComputed = <T>(getter: (prev?: T) => T) => {\n return alienComputed(getter);\n};\n\nexport const createAlienEffect = (fn: () => void): Disposer => {\n return alienEffect(fn);\n};\n\nexport const createAlienScope = (fn: () => void): Disposer => {\n return alienEffectScope(fn);\n};\n\nexport const batch = <T>(fn: () => T): T => {\n alienStartBatch();\n try {\n return fn();\n } finally {\n alienEndBatch();\n }\n};\n\nexport const untrack = <T>(fn: () => T): T => {\n const prev = alienSetActiveSub(undefined);\n try {\n return fn();\n } finally {\n alienSetActiveSub(prev);\n }\n};\n","import { isFunction } from \"./utils\";\nimport { batch as alienBatch } from \"./internals/alien.js\";\n\nexport const batch = <T>(fn: () => T): T => {\n if (!isFunction(fn)) {\n throw new TypeError(\"batch(fn) expects a function\");\n }\n return alienBatch(fn);\n};\n","import { isFunction } from \"./utils\";\nimport type { Disposer } from \"./internals/alien.js\";\n\ntype CleanupFn = () => void;\n\nlet currentBucket: CleanupFn[] | undefined;\n\nexport const __withCleanupBucket = <T>(bucket: CleanupFn[], fn: () => T): T => {\n const prev = currentBucket;\n currentBucket = bucket;\n try {\n return fn();\n } finally {\n currentBucket = prev;\n }\n};\n\nexport const __runCleanupBucket = (bucket: CleanupFn[]): void => {\n for (let i = bucket.length - 1; i >= 0; i--) {\n try {\n bucket[i]?.();\n } catch {\n // best-effort cleanup: userland errors shouldn't break disposal\n }\n }\n bucket.length = 0;\n};\n\nexport const cleanup = (fn: CleanupFn): void => {\n if (!isFunction(fn)) {\n throw new TypeError(\"cleanup(fn) expects a function\");\n }\n if (!currentBucket) {\n throw new Error(\"cleanup(fn) must be called inside scope()\");\n }\n currentBucket.push(fn);\n};\n\nexport const __wrapDisposerWithCleanup = (dispose: Disposer, bucket: CleanupFn[]): Disposer => {\n let disposed = false;\n return () => {\n if (disposed) return;\n disposed = true;\n try {\n dispose();\n } finally {\n __runCleanupBucket(bucket);\n }\n };\n};\n","import { createAlienEffect, type Disposer } from \"./internals/alien.js\";\nimport { isFunction } from \"./utils\";\n\nexport const createSubscribe = (readTracked: () => unknown): ((fn: () => void) => Disposer) => {\n return (fn) => {\n if (!isFunction(fn)) {\n throw new TypeError(\"subscribe(fn) expects a function\");\n }\n\n let inited = false;\n let prev: unknown;\n\n const stop = createAlienEffect(() => {\n const next = readTracked();\n\n if (!inited) {\n inited = true;\n prev = next;\n return;\n }\n\n if (Object.is(prev, next)) return;\n prev = next;\n fn();\n });\n\n return stop;\n };\n};\n","const kSignalBrand: unique symbol = Symbol.for(\"echojs-ecosystem.reactivity.signal\");\nconst kReadonlyBrand: unique symbol = Symbol.for(\"echojs-ecosystem.reactivity.readonlySignal\");\n\nexport type BrandedSignal = {\n [kSignalBrand]: true;\n [kReadonlyBrand]?: true;\n};\n\nexport const brandWritable = <T extends object>(obj: T): T & BrandedSignal => {\n Object.defineProperty(obj, kSignalBrand, { value: true });\n return obj as T & BrandedSignal;\n};\n\nexport const brandReadonly = <T extends object>(obj: T): T & BrandedSignal => {\n Object.defineProperty(obj, kSignalBrand, { value: true });\n Object.defineProperty(obj, kReadonlyBrand, { value: true });\n return obj as T & BrandedSignal;\n};\n\nexport const isBrandedSignal = (value: unknown): value is BrandedSignal => {\n return typeof value === \"object\" && value !== null && (value as any)[kSignalBrand] === true;\n};\n\nexport const isBrandedReadonlySignal = (value: unknown): value is BrandedSignal => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as any)[kSignalBrand] === true &&\n (value as any)[kReadonlyBrand] === true\n );\n};\n","import { createSubscribe } from \"./subscribe\";\nimport { createAlienComputed, untrack } from \"./internals/alien.js\";\nimport { brandReadonly } from \"./internals/guards.js\";\nimport type { ReadonlySignal, ReadValue } from \"./types\";\nimport { isFunction } from \"./utils\";\n\nexport const computed = <T>(getter: () => T): ReadonlySignal<T> => {\n if (!isFunction(getter)) {\n throw new TypeError(\"computed(getter) expects a function\");\n }\n\n const engine = createAlienComputed<T>(() => getter());\n\n const readTracked = (): T => engine();\n const readUntracked = (): T => untrack(() => engine());\n\n const subscribe = createSubscribe(readTracked);\n\n return brandReadonly({\n value: () => readTracked() as ReadValue<T>,\n peek: () => readUntracked() as ReadValue<T>,\n subscribe,\n }) as ReadonlySignal<T>;\n};\n","import { isFunction } from \"./utils\";\nimport { createAlienEffect } from \"./internals/alien.js\";\n\nexport const effect = (fn: () => void): (() => void) => {\n if (!isFunction(fn)) {\n throw new TypeError(\"effect(fn) expects a function\");\n }\n return createAlienEffect(fn);\n};\n","import { brandReadonly } from \"./internals/guards.js\";\nimport type { ReadonlySignal, Signal } from \"./types\";\nimport { isFunction } from \"./utils\";\n\nexport const readonly = <T>(sig: Signal<T> | ReadonlySignal<T>): ReadonlySignal<T> => {\n if (!isFunction((sig as any).set)) return sig as ReadonlySignal<T>;\n if (!isFunction((sig as any).readonly)) return (sig as Signal<T>).readonly();\n\n return brandReadonly({\n value: () => sig.value(),\n peek: () => sig.peek(),\n subscribe: (fn: () => void) => sig.subscribe(fn),\n }) as ReadonlySignal<T>;\n};\n","import { createAlienScope } from \"./internals/alien.js\";\nimport { __withCleanupBucket, __wrapDisposerWithCleanup } from \"./cleanup\";\nimport { isFunction } from \"./utils\";\n\nexport const scope = (fn: () => void): (() => void) => {\n if (!isFunction(fn)) {\n throw new TypeError(\"scope(fn) expects a function\");\n }\n\n const bucket: Array<() => void> = [];\n const dispose = createAlienScope(() => __withCleanupBucket(bucket, fn));\n return __wrapDisposerWithCleanup(dispose, bucket);\n};\n","import { isObjectLike } from \"./utils\";\n\nconst DEV = ((): boolean => {\n const env = (globalThis as any)?.process?.env;\n const nodeEnv = env?.NODE_ENV;\n return nodeEnv !== \"production\";\n})();\n\nconst deepFreezeImpl = (value: unknown, seen: WeakSet<object>): void => {\n if (!isObjectLike(value)) return;\n if (Object.isFrozen(value)) return;\n if (seen.has(value)) return;\n seen.add(value);\n\n const keys = Object.keys(value);\n for (const k of keys) {\n deepFreezeImpl((value as any)[k], seen);\n }\n\n Object.freeze(value);\n};\n\nexport const freezeIfDev = <T>(value: T): T => {\n if (!DEV) return value;\n if (!isObjectLike(value)) return value;\n\n deepFreezeImpl(value, new WeakSet());\n return value;\n};\n\nexport const __isDevModeForTests = (): boolean => {\n return DEV;\n};\n","import { freezeIfDev } from \"./freeze\";\nimport { createSubscribe } from \"./subscribe\";\nimport { createAlienSignal, untrack } from \"./internals/alien.js\";\nimport { brandReadonly, brandWritable } from \"./internals/guards.js\";\nimport type { ReadonlySignal, ReadValue, Signal } from \"./types\";\nimport { isFunction } from \"./utils\";\n\nexport const createReadonlySignalFacade = <T>(impl: {\n value(): ReadValue<T>;\n peek(): ReadValue<T>;\n subscribe(fn: () => void): () => void;\n}): ReadonlySignal<T> => {\n return brandReadonly({\n value: impl.value,\n peek: impl.peek,\n subscribe: impl.subscribe,\n });\n};\n\nconst signalImpl = <T>(...args: [initial: T] | []): Signal<T> => {\n if (args.length === 0) {\n throw new TypeError(\"signal(initial) expects 1 argument\");\n }\n\n const initial = args[0] as T;\n const engine = createAlienSignal(freezeIfDev(initial));\n\n const readTracked = (): T => engine();\n const readUntracked = (): T => untrack(() => engine());\n\n const subscribe = createSubscribe(readTracked);\n\n const readonlyFacade = createReadonlySignalFacade<T>({\n value: () => readTracked() as ReadValue<T>,\n peek: () => readUntracked() as ReadValue<T>,\n subscribe,\n });\n\n const writable: Signal<T> = brandWritable({\n value: () => readTracked() as ReadValue<T>,\n peek: () => readUntracked() as ReadValue<T>,\n subscribe,\n set: (next: T) => {\n engine(freezeIfDev(next));\n },\n update: (fn: (prev: T) => T) => {\n const prev = readUntracked();\n const next = fn(prev);\n engine(freezeIfDev(next));\n },\n readonly: () => readonlyFacade,\n });\n\n return writable;\n};\n\nexport const signal = signalImpl as <T>(initial: T) => Signal<T>;\n","import { isBrandedReadonlySignal, isBrandedSignal } from \"./internals/guards.js\";\nimport type { ReadonlySignal, Signal } from \"./types\";\n\nexport const isSignal = (value: unknown): value is Signal<unknown> | ReadonlySignal<unknown> => {\n return isBrandedSignal(value);\n};\n\nexport const isReadonlySignal = (value: unknown): value is ReadonlySignal<unknown> => {\n return isBrandedReadonlySignal(value);\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@echojs-ecosystem/reactivity",
3
+ "version": "0.1.0",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "check-types": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.typing.json --noEmit",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.typing.json --noEmit",
20
+ "test:types": "tsc -p tsconfig.typing.json --noEmit",
21
+ "lint": "oxlint .",
22
+ "lint:fix": "oxlint . --fix",
23
+ "format": "oxfmt --check .",
24
+ "format:fix": "oxfmt .",
25
+ "test": "vitest",
26
+ "test:run": "vitest run",
27
+ "test:coverage": "vitest run --coverage",
28
+ "test:ui": "vitest ui"
29
+ },
30
+ "dependencies": {
31
+ "alien-signals": "^3.1.2"
32
+ },
33
+ "devDependencies": {
34
+ "@echojs-ecosystem/oxc-config": "workspace:*",
35
+ "oxlint": "^1.60.0",
36
+ "typescript": "5.9.2",
37
+ "vitest": "^4.1.4"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/echojs-ecosystem/echojs-core.git",
45
+ "directory": "packages/reactivity"
46
+ }
47
+ }