@sylphx/lens-signals 1.0.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/src/signal.ts ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @sylphx/lens-client - Signal Implementation
3
+ *
4
+ * Re-exports Preact Signals with Lens-specific types and utilities.
5
+ * @see https://preactjs.com/guide/v10/signals/
6
+ */
7
+
8
+ import {
9
+ batch as preactBatch,
10
+ computed as preactComputed,
11
+ effect as preactEffect,
12
+ signal as preactSignal,
13
+ } from "@preact/signals-core";
14
+
15
+ // =============================================================================
16
+ // Types
17
+ // =============================================================================
18
+
19
+ /** Subscription callback */
20
+ export type Subscriber<T> = (value: T) => void;
21
+
22
+ /** Unsubscribe function */
23
+ export type Unsubscribe = () => void;
24
+
25
+ /** Read-only signal interface */
26
+ export interface Signal<T> {
27
+ /** Current value (read-only) */
28
+ readonly value: T;
29
+
30
+ /** Subscribe to value changes */
31
+ subscribe(fn: Subscriber<T>): Unsubscribe;
32
+
33
+ /** Get value without tracking */
34
+ peek(): T;
35
+ }
36
+
37
+ /** Writable signal interface */
38
+ export interface WritableSignal<T> extends Signal<T> {
39
+ /** Current value (read-write) */
40
+ value: T;
41
+ }
42
+
43
+ // =============================================================================
44
+ // Factory Functions (Re-exports with our types)
45
+ // =============================================================================
46
+
47
+ /**
48
+ * Create a writable signal
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const count = signal(0);
53
+ * console.log(count.value); // 0
54
+ *
55
+ * count.value = 1;
56
+ * console.log(count.value); // 1
57
+ * ```
58
+ */
59
+ export function signal<T>(initial: T): WritableSignal<T> {
60
+ return preactSignal(initial) as WritableSignal<T>;
61
+ }
62
+
63
+ /**
64
+ * Create a computed signal
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const count = signal(0);
69
+ * const doubled = computed(() => count.value * 2);
70
+ *
71
+ * console.log(doubled.value); // 0
72
+ *
73
+ * count.value = 5;
74
+ * console.log(doubled.value); // 10
75
+ * ```
76
+ */
77
+ export function computed<T>(compute: () => T): Signal<T> {
78
+ return preactComputed(compute) as Signal<T>;
79
+ }
80
+
81
+ /**
82
+ * Run a function whenever dependencies change
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * const count = signal(0);
87
+ * const dispose = effect(() => {
88
+ * console.log('Count:', count.value);
89
+ * });
90
+ *
91
+ * count.value = 1; // Logs: "Count: 1"
92
+ * dispose(); // Stop watching
93
+ * ```
94
+ */
95
+ export function effect(fn: () => void | (() => void)): Unsubscribe {
96
+ return preactEffect(fn);
97
+ }
98
+
99
+ /**
100
+ * Batch multiple signal updates
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const a = signal(1);
105
+ * const b = signal(2);
106
+ *
107
+ * batch(() => {
108
+ * a.value = 10;
109
+ * b.value = 20;
110
+ * });
111
+ * // Subscribers notified only once
112
+ * ```
113
+ */
114
+ export function batch<T>(fn: () => T): T {
115
+ return preactBatch(fn);
116
+ }
117
+
118
+ // =============================================================================
119
+ // Utilities
120
+ // =============================================================================
121
+
122
+ /**
123
+ * Check if a value is a signal
124
+ */
125
+ export function isSignal(value: unknown): value is Signal<unknown> {
126
+ return (
127
+ value !== null &&
128
+ typeof value === "object" &&
129
+ "value" in value &&
130
+ "peek" in value &&
131
+ "subscribe" in value
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Convert a signal to a promise that resolves when the signal changes
137
+ */
138
+ export function toPromise<T>(sig: Signal<T>): Promise<T> {
139
+ return new Promise((resolve) => {
140
+ let isFirst = true;
141
+ let unsub: Unsubscribe;
142
+ unsub = sig.subscribe((value) => {
143
+ // Skip the initial value (subscribe fires immediately)
144
+ if (isFirst) {
145
+ isFirst = false;
146
+ return;
147
+ }
148
+ unsub();
149
+ resolve(value);
150
+ });
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Create a signal that derives from multiple signals
156
+ */
157
+ export function derive<T, U>(signals: Signal<T>[], fn: (values: T[]) => U): Signal<U> {
158
+ return computed(() => fn(signals.map((s) => s.value)));
159
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @sylphx/lens-signals - Store Types
3
+ *
4
+ * Type definitions for ReactiveStore.
5
+ */
6
+
7
+ import type { PipelineResult } from "@sylphx/reify";
8
+
9
+ /** Entity state with metadata */
10
+ export interface EntityState<T = unknown> {
11
+ /** The entity data */
12
+ data: T | null;
13
+ /** Loading state */
14
+ loading: boolean;
15
+ /** Error state */
16
+ error: Error | null;
17
+ /** Whether data is stale */
18
+ stale: boolean;
19
+ /** Subscription reference count */
20
+ refCount: number;
21
+ /** Cache timestamp */
22
+ cachedAt?: number | undefined;
23
+ /** Cache tags for invalidation */
24
+ tags?: string[] | undefined;
25
+ }
26
+
27
+ /** Optimistic update entry */
28
+ export interface OptimisticEntry {
29
+ id: string;
30
+ entityName: string;
31
+ entityId: string;
32
+ type: "create" | "update" | "delete";
33
+ originalData: unknown;
34
+ optimisticData: unknown;
35
+ timestamp: number;
36
+ }
37
+
38
+ /** Multi-entity optimistic transaction */
39
+ export interface OptimisticTransaction {
40
+ id: string;
41
+ /** Pipeline results from Reify execution */
42
+ results: PipelineResult;
43
+ /** Original data for each entity (for rollback) */
44
+ originalData: Map<string, unknown>;
45
+ timestamp: number;
46
+ }
47
+
48
+ /** Store configuration */
49
+ export interface StoreConfig {
50
+ /** Enable optimistic updates (default: true) */
51
+ optimistic?: boolean;
52
+ /** Cache TTL in milliseconds (default: 5 minutes) */
53
+ cacheTTL?: number;
54
+ /** Maximum cache size (default: 1000) */
55
+ maxCacheSize?: number;
56
+ /** Cascade invalidation rules */
57
+ cascadeRules?: CascadeRule[];
58
+ }
59
+
60
+ /** Cascade invalidation rule */
61
+ export interface CascadeRule {
62
+ /** Source entity type that triggers invalidation */
63
+ source: string;
64
+ /** Operation types that trigger cascade (default: all) */
65
+ operations?: ("create" | "update" | "delete")[];
66
+ /** Target entities to invalidate */
67
+ targets: string[];
68
+ }
69
+
70
+ /** Invalidation options */
71
+ export interface InvalidationOptions {
72
+ /** Invalidate by tag */
73
+ tags?: string[];
74
+ /** Invalidate by entity type pattern (glob-like) */
75
+ pattern?: string;
76
+ /** Cascade to related entities */
77
+ cascade?: boolean;
78
+ }