@ydant/reactive 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 cwd-k2
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # @ydant/reactive
2
+
3
+ Signal-based reactivity system for Ydant.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @ydant/reactive
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Signals
14
+
15
+ ```typescript
16
+ import { signal, computed, effect } from "@ydant/reactive";
17
+
18
+ // Create a signal
19
+ const count = signal(0);
20
+
21
+ // Read value
22
+ console.log(count()); // 0
23
+
24
+ // Update value
25
+ count.set(5);
26
+ count.update((n) => n + 1);
27
+
28
+ // Create computed value
29
+ const doubled = computed(() => count() * 2);
30
+ console.log(doubled()); // 12
31
+
32
+ // Run effects
33
+ const dispose = effect(() => {
34
+ console.log(`Count: ${count()}`);
35
+ });
36
+
37
+ count.set(10); // Logs: "Count: 10"
38
+ dispose(); // Stop tracking
39
+ ```
40
+
41
+ ### With DOM (reactive primitive)
42
+
43
+ ```typescript
44
+ import { div, button, text, on, type Component } from "@ydant/core";
45
+ import { mount } from "@ydant/dom";
46
+ import { signal, reactive, createReactivePlugin } from "@ydant/reactive";
47
+
48
+ const count = signal(0);
49
+
50
+ const Counter: Component = () =>
51
+ div(function* () {
52
+ // Auto-update on signal change
53
+ yield* reactive(() => [text(`Count: ${count()}`)]);
54
+
55
+ yield* button(() => [on("click", () => count.update((n) => n + 1)), text("Increment")]);
56
+ });
57
+
58
+ mount(Counter, document.getElementById("app")!, {
59
+ plugins: [createReactivePlugin()],
60
+ });
61
+ ```
62
+
63
+ ## API
64
+
65
+ ### signal
66
+
67
+ ```typescript
68
+ function signal<T>(initialValue: T): Signal<T>;
69
+
70
+ interface Signal<T> {
71
+ (): T; // Read
72
+ set(value: T): void; // Write
73
+ update(fn: (v: T) => T): void; // Update with function
74
+ }
75
+ ```
76
+
77
+ ### computed
78
+
79
+ ```typescript
80
+ function computed<T>(fn: () => T): Computed<T>;
81
+
82
+ interface Computed<T> {
83
+ (): T; // Read (automatically tracks dependencies)
84
+ }
85
+ ```
86
+
87
+ ### effect
88
+
89
+ ```typescript
90
+ function effect(fn: () => void | (() => void)): () => void;
91
+ ```
92
+
93
+ Runs `fn` immediately and re-runs when dependencies change. Returns a dispose function. If `fn` returns a cleanup function, it will be called before each re-run and on dispose.
94
+
95
+ ### reactive
96
+
97
+ ```typescript
98
+ function reactive(fn: () => Render): Reactive;
99
+ ```
100
+
101
+ Creates a reactive block that auto-updates DOM when signals change. Use with `yield*` in generator syntax.
102
+
103
+ ### batch
104
+
105
+ ```typescript
106
+ function batch(fn: () => void): void;
107
+ ```
108
+
109
+ Batches multiple signal updates to trigger effects only once:
110
+
111
+ ```typescript
112
+ const firstName = signal("John");
113
+ const lastName = signal("Doe");
114
+
115
+ effect(() => {
116
+ console.log(`${firstName()} ${lastName()}`);
117
+ });
118
+ // Logs: "John Doe"
119
+
120
+ batch(() => {
121
+ firstName.set("Jane");
122
+ lastName.set("Smith");
123
+ });
124
+ // Logs only once: "Jane Smith"
125
+ ```
126
+
127
+ Without `batch`, each `set()` call would trigger the effect immediately. With `batch`, updates are collected and the effect runs only once at the end with the final values.
128
+
129
+ ### createReactivePlugin
130
+
131
+ ```typescript
132
+ function createReactivePlugin(): DomPlugin;
133
+ ```
134
+
135
+ Creates a DOM plugin that handles `reactive` blocks. Must be passed to `mount()`.
136
+
137
+ ## Module Structure
138
+
139
+ - `types.ts` - Subscriber type
140
+ - `signal.ts` - Signal implementation
141
+ - `computed.ts` - Computed implementation
142
+ - `effect.ts` - Effect implementation
143
+ - `batch.ts` - Batch functionality
144
+ - `reactive.ts` - reactive primitive
145
+ - `plugin.ts` - DOM plugin
146
+ - `tracking.ts` - Subscription tracking (internal)
@@ -0,0 +1,43 @@
1
+ /**
2
+ * バッチ更新: 複数の Signal 更新を一度にまとめて通知する
3
+ *
4
+ * NOTE: batchDepth と pendingEffects はモジュールレベルのグローバル状態。
5
+ * テスト間での分離には __resetForTesting__() を使用。
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const firstName = signal("John");
10
+ * const lastName = signal("Doe");
11
+ *
12
+ * effect(() => {
13
+ * console.log(`${firstName()} ${lastName()}`);
14
+ * });
15
+ * // 出力: "John Doe"
16
+ *
17
+ * batch(() => {
18
+ * firstName.set("Jane");
19
+ * lastName.set("Smith");
20
+ * });
21
+ * // 出力: "Jane Smith" (1回だけ)
22
+ * ```
23
+ */
24
+ /**
25
+ * テスト用: 状態をリセット
26
+ * @internal
27
+ */
28
+ export declare function __resetForTesting__(): void;
29
+ /**
30
+ * バッチ更新を実行する
31
+ *
32
+ * バッチ内で行われた複数の Signal 更新は、バッチ終了時に
33
+ * まとめて1回だけ effect を実行する。
34
+ *
35
+ * @param fn - バッチ内で実行する関数
36
+ */
37
+ export declare function batch(fn: () => void): void;
38
+ /**
39
+ * 内部用: バッチ中かどうかを確認し、effect を遅延実行キューに追加
40
+ *
41
+ * @returns バッチ中で遅延された場合は true
42
+ */
43
+ export declare function scheduleEffect(effect: () => void): boolean;
@@ -0,0 +1,23 @@
1
+ import { Readable } from './types';
2
+ /** Computed インターフェース(読み取り専用) */
3
+ export interface Computed<T> extends Readable<T> {
4
+ }
5
+ /**
6
+ * Computed を作成する
7
+ *
8
+ * @param fn - 派生値を計算する関数
9
+ * @returns Computed オブジェクト
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const firstName = signal("John");
14
+ * const lastName = signal("Doe");
15
+ * const fullName = computed(() => `${firstName()} ${lastName()}`);
16
+ *
17
+ * console.log(fullName()); // "John Doe"
18
+ *
19
+ * firstName.set("Jane");
20
+ * console.log(fullName()); // "Jane Doe"
21
+ * ```
22
+ */
23
+ export declare function computed<T>(fn: () => T): Computed<T>;
@@ -0,0 +1,29 @@
1
+ import { CleanupFn } from '@ydant/core';
2
+ /**
3
+ * Effect を作成する
4
+ *
5
+ * @param fn - 副作用を実行する関数。クリーンアップ関数を返すことができる。
6
+ * @returns Effect を破棄するための関数
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const count = signal(0);
11
+ *
12
+ * // 基本的な使い方
13
+ * const dispose = effect(() => {
14
+ * console.log(`Count changed to: ${count()}`);
15
+ * });
16
+ *
17
+ * // クリーンアップ付き
18
+ * const disposeTimer = effect(() => {
19
+ * const value = count();
20
+ * const timer = setTimeout(() => {
21
+ * console.log(`Delayed log: ${value}`);
22
+ * }, 1000);
23
+ *
24
+ * // クリーンアップ: 次の実行前、または dispose 時に呼ばれる
25
+ * return () => clearTimeout(timer);
26
+ * });
27
+ * ```
28
+ */
29
+ export declare function effect(fn: () => void | CleanupFn): CleanupFn;
@@ -0,0 +1,6 @@
1
+ import { Reactive } from './reactive';
2
+ declare module "@ydant/core" {
3
+ interface PluginChildExtensions {
4
+ Reactive: Reactive;
5
+ }
6
+ }
@@ -0,0 +1,11 @@
1
+ /// <reference path="./global.d.ts" />
2
+ export type { Subscriber, Readable } from './types';
3
+ export type { Signal } from './signal';
4
+ export type { Computed } from './computed';
5
+ export type { Reactive } from './reactive';
6
+ export { signal } from './signal';
7
+ export { computed } from './computed';
8
+ export { effect } from './effect';
9
+ export { batch, scheduleEffect } from './batch';
10
+ export { reactive } from './reactive';
11
+ export { createReactivePlugin } from './plugin';
@@ -0,0 +1,121 @@
1
+ import { isTagged as d } from "@ydant/core";
2
+ let u = null;
3
+ function l() {
4
+ return u;
5
+ }
6
+ function a(r, e) {
7
+ const t = u;
8
+ u = r;
9
+ try {
10
+ return e();
11
+ } finally {
12
+ u = t;
13
+ }
14
+ }
15
+ let i = 0, f = /* @__PURE__ */ new Set();
16
+ function h(r) {
17
+ i++;
18
+ try {
19
+ r();
20
+ } finally {
21
+ if (i--, i === 0) {
22
+ const e = f;
23
+ f = /* @__PURE__ */ new Set();
24
+ for (const t of e)
25
+ t();
26
+ }
27
+ }
28
+ }
29
+ function b(r) {
30
+ return i > 0 ? (f.add(r), !0) : !1;
31
+ }
32
+ function y(r) {
33
+ let e = r;
34
+ const t = /* @__PURE__ */ new Set(), c = (() => {
35
+ const n = l();
36
+ return n && t.add(n), e;
37
+ });
38
+ return c.set = (n) => {
39
+ if (!Object.is(e, n)) {
40
+ e = n;
41
+ for (const s of t)
42
+ b(s) || s();
43
+ }
44
+ }, c.update = (n) => {
45
+ c.set(n(e));
46
+ }, c.peek = () => e, c;
47
+ }
48
+ function v(r) {
49
+ let e, t = !0;
50
+ const c = /* @__PURE__ */ new Set(), n = () => {
51
+ t = !0;
52
+ for (const o of c)
53
+ o();
54
+ }, s = (() => {
55
+ const o = l();
56
+ return o && c.add(o), t && (e = a(n, r), t = !1), e;
57
+ });
58
+ return s.peek = () => (t && (e = r(), t = !1), e), s;
59
+ }
60
+ function m(r) {
61
+ let e, t = !1;
62
+ const c = () => {
63
+ if (!t) {
64
+ if (e) {
65
+ try {
66
+ e();
67
+ } catch (n) {
68
+ console.error("[ydant] Effect cleanup threw an error:", n);
69
+ }
70
+ e = void 0;
71
+ }
72
+ e = a(c, r);
73
+ }
74
+ };
75
+ return c(), () => {
76
+ if (!t && (t = !0, e)) {
77
+ try {
78
+ e();
79
+ } catch (n) {
80
+ console.error("[ydant] Effect cleanup threw an error:", n);
81
+ }
82
+ e = void 0;
83
+ }
84
+ };
85
+ }
86
+ function* S(r) {
87
+ yield { type: "reactive", builder: r };
88
+ }
89
+ function g() {
90
+ return {
91
+ name: "reactive",
92
+ types: ["reactive"],
93
+ dependencies: ["base"],
94
+ process(r, e) {
95
+ if (!d(r, "reactive")) return {};
96
+ const t = r.builder, c = document.createElement("span");
97
+ c.setAttribute("data-reactive", ""), e.appendChild(c);
98
+ let n = [];
99
+ const s = () => {
100
+ for (const o of n)
101
+ o();
102
+ n = [], c.innerHTML = "", a(s, () => {
103
+ e.processChildren(t, { parent: c });
104
+ });
105
+ };
106
+ return s(), e.onUnmount(() => {
107
+ for (const o of n)
108
+ o();
109
+ }), {};
110
+ }
111
+ };
112
+ }
113
+ export {
114
+ h as batch,
115
+ v as computed,
116
+ g as createReactivePlugin,
117
+ m as effect,
118
+ S as reactive,
119
+ b as scheduleEffect,
120
+ y as signal
121
+ };
@@ -0,0 +1 @@
1
+ (function(i,f){typeof exports=="object"&&typeof module<"u"?f(exports,require("@ydant/core")):typeof define=="function"&&define.amd?define(["exports","@ydant/core"],f):(i=typeof globalThis<"u"?globalThis:i||self,f(i.YdantReactive={},i.YdantCore))})(this,(function(i,f){"use strict";let s=null;function b(){return s}function d(n,e){const t=s;s=n;try{return e()}finally{s=t}}let a=0,l=new Set;function h(n){a++;try{n()}finally{if(a--,a===0){const e=l;l=new Set;for(const t of e)t()}}}function p(n){return a>0?(l.add(n),!0):!1}function y(n){let e=n;const t=new Set,c=(()=>{const r=b();return r&&t.add(r),e});return c.set=r=>{if(!Object.is(e,r)){e=r;for(const o of t)p(o)||o()}},c.update=r=>{c.set(r(e))},c.peek=()=>e,c}function v(n){let e,t=!0;const c=new Set,r=()=>{t=!0;for(const u of c)u()},o=(()=>{const u=b();return u&&c.add(u),t&&(e=d(r,n),t=!1),e});return o.peek=()=>(t&&(e=n(),t=!1),e),o}function m(n){let e,t=!1;const c=()=>{if(!t){if(e){try{e()}catch(r){console.error("[ydant] Effect cleanup threw an error:",r)}e=void 0}e=d(c,n)}};return c(),()=>{if(!t&&(t=!0,e)){try{e()}catch(r){console.error("[ydant] Effect cleanup threw an error:",r)}e=void 0}}}function*g(n){yield{type:"reactive",builder:n}}function S(){return{name:"reactive",types:["reactive"],dependencies:["base"],process(n,e){if(!f.isTagged(n,"reactive"))return{};const t=n.builder,c=document.createElement("span");c.setAttribute("data-reactive",""),e.appendChild(c);let r=[];const o=()=>{for(const u of r)u();r=[],c.innerHTML="",d(o,()=>{e.processChildren(t,{parent:c})})};return o(),e.onUnmount(()=>{for(const u of r)u()}),{}}}}i.batch=h,i.computed=v,i.createReactivePlugin=S,i.effect=m,i.reactive=g,i.scheduleEffect=p,i.signal=y,Object.defineProperty(i,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,5 @@
1
+ import { Plugin } from '@ydant/core';
2
+ /**
3
+ * Reactive プラグインを作成する
4
+ */
5
+ export declare function createReactivePlugin(): Plugin;
@@ -0,0 +1,21 @@
1
+ import { Tagged, Builder, Primitive } from '@ydant/core';
2
+ /** リアクティブブロック - Signal の変更を追跡して自動更新 */
3
+ export type Reactive = Tagged<"reactive", {
4
+ builder: Builder;
5
+ }>;
6
+ /**
7
+ * Signal を追跡して自動的に再レンダリングするリアクティブブロックを作成
8
+ *
9
+ * @param builder - 子要素を生成する関数
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const count = signal(0);
14
+ * const doubled = computed(() => count() * 2);
15
+ *
16
+ * yield* reactive(() => [
17
+ * text(`Count: ${count()}, Doubled: ${doubled()}`),
18
+ * ]);
19
+ * ```
20
+ */
21
+ export declare function reactive(builder: Builder): Primitive<Reactive>;
@@ -0,0 +1,30 @@
1
+ import { Readable } from './types';
2
+ /** Signal インターフェース */
3
+ export interface Signal<T> extends Readable<T> {
4
+ /** 値を設定する */
5
+ set(value: T): void;
6
+ /** 関数で値を更新する */
7
+ update(fn: (prev: T) => T): void;
8
+ }
9
+ /**
10
+ * Signal を作成する
11
+ *
12
+ * @param initialValue - 初期値
13
+ * @returns Signal オブジェクト
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const count = signal(0);
18
+ *
19
+ * // 読み取り
20
+ * console.log(count()); // 0
21
+ *
22
+ * // 書き込み
23
+ * count.set(5);
24
+ * count.update(n => n * 2); // 10
25
+ *
26
+ * // 購読なしで読み取り
27
+ * console.log(count.peek()); // 10
28
+ * ```
29
+ */
30
+ export declare function signal<T>(initialValue: T): Signal<T>;
@@ -0,0 +1,10 @@
1
+ import { Subscriber } from './types';
2
+ /**
3
+ * テスト用: 状態をリセット
4
+ * @internal
5
+ */
6
+ export declare function __resetForTesting__(): void;
7
+ /** 現在の購読者を取得 */
8
+ export declare function getCurrentSubscriber(): Subscriber | null;
9
+ /** 購読者を設定して関数を実行(終了後に元に戻す) */
10
+ export declare function runWithSubscriber<T>(subscriber: Subscriber, fn: () => T): T;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Reactive パッケージの型定義
3
+ */
4
+ /** Signal/Effect の購読者(変更通知を受け取るコールバック) */
5
+ export type Subscriber = () => void;
6
+ /**
7
+ * 読み取り可能なリアクティブ値の共通インターフェース
8
+ *
9
+ * Signal, Computed など、値の読み取りと peek をサポートする型の基底インターフェース。
10
+ */
11
+ export interface Readable<T> {
12
+ /** 値を読み取る(依存関係を追跡) */
13
+ (): T;
14
+ /** 現在の値を取得(購読なし、依存関係を追跡しない) */
15
+ peek(): T;
16
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@ydant/reactive",
3
+ "version": "0.1.0",
4
+ "description": "Reactivity system for Ydant",
5
+ "keywords": [
6
+ "reactive",
7
+ "signal",
8
+ "state",
9
+ "ydant"
10
+ ],
11
+ "homepage": "https://github.com/cwd-k2/ydant#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/cwd-k2/ydant/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": "cwd-k2",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/cwd-k2/ydant.git",
20
+ "directory": "packages/reactive"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "LICENSE",
25
+ "README.md"
26
+ ],
27
+ "main": "./dist/index.umd.js",
28
+ "module": "./dist/index.es.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "@ydant/dev": {
34
+ "types": "./src/index.ts",
35
+ "default": "./src/index.ts"
36
+ },
37
+ "import": "./dist/index.es.js",
38
+ "require": "./dist/index.umd.js"
39
+ }
40
+ },
41
+ "peerDependencies": {
42
+ "@ydant/base": "0.1.0",
43
+ "@ydant/core": "0.1.0"
44
+ },
45
+ "scripts": {
46
+ "build": "vite build",
47
+ "typecheck": "tsc --noEmit"
48
+ }
49
+ }