@nisli/core 0.43.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/dist/signal.js ADDED
@@ -0,0 +1,403 @@
1
+ /**
2
+ * signal.ts — Reactive primitives: signal(), computed(), effect()
3
+ *
4
+ * Push-pull hybrid model:
5
+ * - Writes push "dirty" flags up the dependency graph
6
+ * - Reads pull fresh values lazily (computed only recalculates when read)
7
+ * - Multiple synchronous writes coalesce into one microtask flush
8
+ *
9
+ * Dependency tracking is automatic: reading a signal inside a computed
10
+ * or effect registers it as a dependency. Dependencies are re-tracked
11
+ * on every execution (dynamic/conditional tracking).
12
+ */
13
+ // ── Late-bound context hook for auto-disposal ──────────────────────
14
+ // Avoids circular dependency: context.ts imports nothing from signal.ts,
15
+ // but signal.ts needs to check for component context to auto-dispose effects.
16
+ // component.ts wires this up at import time via `setContextHook()`.
17
+ let contextHook = null;
18
+ /**
19
+ * Register the context hook. Called by component.ts at module init.
20
+ * The hook returns an `addDisposer` function if inside setup context, null otherwise.
21
+ */
22
+ export function setContextHook(hook) {
23
+ contextHook = hook;
24
+ }
25
+ // ── Brand symbol for signal detection in templates ──────────────────
26
+ export const SIGNAL_BRAND = Symbol.for('backlog.signal');
27
+ // ── Internal tracking state ─────────────────────────────────────────
28
+ /**
29
+ * The currently executing reactive context (computed or effect).
30
+ * When non-null, any signal read during execution is recorded as a dependency.
31
+ */
32
+ let activeObserver = null;
33
+ /**
34
+ * Global epoch counter. Incremented on every signal write.
35
+ * Used to determine if a computed needs recalculation: if a dependency's
36
+ * lastChanged > this computed's lastChecked, the computed is stale.
37
+ */
38
+ let globalEpoch = 0;
39
+ // ── Effect scheduling ────────────────────────────────────────────────
40
+ const pendingEffects = new Set();
41
+ let flushScheduled = false;
42
+ function scheduleFlush() {
43
+ if (!flushScheduled) {
44
+ flushScheduled = true;
45
+ queueMicrotask(flushPendingEffects);
46
+ }
47
+ }
48
+ function flushPendingEffects() {
49
+ flushScheduled = false;
50
+ // Copy to avoid mutation during iteration
51
+ const effects = [...pendingEffects];
52
+ pendingEffects.clear();
53
+ for (const effect of effects) {
54
+ if (!effect.disposed) {
55
+ runEffect(effect);
56
+ }
57
+ }
58
+ }
59
+ // ── Dependency graph node types ─────────────────────────────────────
60
+ var NodeState;
61
+ (function (NodeState) {
62
+ NodeState[NodeState["Clean"] = 0] = "Clean";
63
+ NodeState[NodeState["MaybeDirty"] = 1] = "MaybeDirty";
64
+ NodeState[NodeState["Dirty"] = 2] = "Dirty";
65
+ })(NodeState || (NodeState = {}));
66
+ class SignalImpl {
67
+ [SIGNAL_BRAND] = true;
68
+ /** @internal */
69
+ _node;
70
+ constructor(initialValue) {
71
+ this._node = {
72
+ value: initialValue,
73
+ lastChanged: globalEpoch,
74
+ observers: new Set(),
75
+ };
76
+ }
77
+ get value() {
78
+ // Track dependency if inside a reactive context
79
+ if (activeObserver) {
80
+ activeObserver.sources.add(this._node);
81
+ this._node.observers.add(activeObserver);
82
+ }
83
+ return this._node.value;
84
+ }
85
+ set value(newValue) {
86
+ if (Object.is(this._node.value, newValue))
87
+ return;
88
+ this._node.value = newValue;
89
+ this._node.lastChanged = ++globalEpoch;
90
+ notifyObservers(this._node.observers);
91
+ }
92
+ subscribe(fn) {
93
+ // Immediately notify with current value
94
+ fn(this._node.value);
95
+ // Create a lightweight effect that calls fn
96
+ const dispose = effect(() => {
97
+ fn(this.value);
98
+ });
99
+ return dispose;
100
+ }
101
+ }
102
+ function notifyObservers(observers) {
103
+ for (const observer of observers) {
104
+ observer.notify();
105
+ }
106
+ }
107
+ class ComputedImpl {
108
+ [SIGNAL_BRAND] = true;
109
+ /** @internal */
110
+ _node;
111
+ compute;
112
+ state = NodeState.Dirty; // Start dirty so first read computes
113
+ sources = new Set();
114
+ computing = false;
115
+ constructor(fn) {
116
+ this.compute = fn;
117
+ this._node = {
118
+ value: undefined,
119
+ lastChanged: 0,
120
+ observers: new Set(),
121
+ };
122
+ }
123
+ get value() {
124
+ if (this.computing) {
125
+ throw new Error('Circular dependency detected in computed()');
126
+ }
127
+ // Track dependency if inside a reactive context
128
+ if (activeObserver) {
129
+ activeObserver.sources.add(this._node);
130
+ this._node.observers.add(activeObserver);
131
+ }
132
+ // Pull: recalculate if dirty
133
+ if (this.state !== NodeState.Clean) {
134
+ this.update();
135
+ }
136
+ return this._node.value;
137
+ }
138
+ update() {
139
+ // Unsubscribe from previous sources (for dynamic dependency tracking)
140
+ for (const source of this.sources) {
141
+ source.observers.delete(this);
142
+ }
143
+ this.sources.clear();
144
+ // Run compute with tracking
145
+ const prevObserver = activeObserver;
146
+ activeObserver = this;
147
+ this.computing = true;
148
+ try {
149
+ const newValue = this.compute();
150
+ if (!Object.is(this._node.value, newValue)) {
151
+ this._node.value = newValue;
152
+ this._node.lastChanged = ++globalEpoch;
153
+ // Notify downstream observers that our value changed
154
+ notifyObservers(this._node.observers);
155
+ }
156
+ }
157
+ finally {
158
+ this.computing = false;
159
+ activeObserver = prevObserver;
160
+ }
161
+ this.state = NodeState.Clean;
162
+ }
163
+ /** @internal — called by the ReactiveNode interface when a source changes */
164
+ notify() {
165
+ if (this.state === NodeState.Clean) {
166
+ this.state = NodeState.Dirty;
167
+ // Propagate dirty flags to downstream observers
168
+ // (they need to re-check if this computed's value actually changed)
169
+ notifyObservers(this._node.observers);
170
+ }
171
+ }
172
+ subscribe(fn) {
173
+ fn(this.value);
174
+ const dispose = effect(() => {
175
+ fn(this.value);
176
+ });
177
+ return dispose;
178
+ }
179
+ // ReactiveNode interface — used by dependency tracking
180
+ get sources_() {
181
+ return this.sources;
182
+ }
183
+ }
184
+ // Make ComputedImpl satisfy ReactiveNode for the dependency tracker
185
+ Object.defineProperty(ComputedImpl.prototype, 'sources', {
186
+ enumerable: false,
187
+ });
188
+ // Bridge ComputedImpl to ReactiveNode — the observer interface.
189
+ // We cast in notify() above; here we make the computed usable as a ReactiveNode
190
+ // by defining the properties the tracker expects.
191
+ const computedAsReactiveNode = (c) => ({
192
+ get state() { return c['state']; },
193
+ set state(v) { c['state'] = v; },
194
+ get sources() { return c['sources']; },
195
+ notify: () => c.notify(),
196
+ });
197
+ // ── Effect (side-effect, auto-tracks, auto-disposes) ────────────────
198
+ /**
199
+ * Maximum consecutive re-runs within a time window before we assume an infinite loop.
200
+ * An effect that writes to a signal it reads will re-trigger itself
201
+ * on every flush. This guard prevents silent UI freezes.
202
+ * See ADR 0008 Gap 2 / ADR 0009.
203
+ */
204
+ const MAX_EFFECT_RERUNS = 100;
205
+ /**
206
+ * Time window (ms) for counting consecutive re-runs.
207
+ * If an effect runs MAX_EFFECT_RERUNS times within this window, it's a loop.
208
+ * Resets when the effect hasn't been re-triggered for longer than this window.
209
+ */
210
+ const LOOP_WINDOW_MS = 2000;
211
+ function createEffectNode(fn) {
212
+ return {
213
+ state: NodeState.Dirty,
214
+ fn,
215
+ cleanup: null,
216
+ disposed: false,
217
+ sources: new Set(),
218
+ disposers: [],
219
+ runCount: 0,
220
+ windowStart: 0,
221
+ notify() {
222
+ if (this.disposed)
223
+ return;
224
+ this.state = NodeState.Dirty;
225
+ pendingEffects.add(this);
226
+ scheduleFlush();
227
+ },
228
+ };
229
+ }
230
+ function runEffect(node) {
231
+ if (node.disposed)
232
+ return;
233
+ // Loop detection: count consecutive re-runs within a time window.
234
+ // If the effect runs MAX_EFFECT_RERUNS times within LOOP_WINDOW_MS,
235
+ // it's almost certainly a write-to-own-dependency loop. Dispose it.
236
+ const now = Date.now();
237
+ if (now - node.windowStart > LOOP_WINDOW_MS) {
238
+ // New window — reset counter
239
+ node.runCount = 0;
240
+ node.windowStart = now;
241
+ }
242
+ node.runCount++;
243
+ if (node.runCount > MAX_EFFECT_RERUNS) {
244
+ console.error(`Effect exceeded maximum re-run limit (${MAX_EFFECT_RERUNS}). ` +
245
+ `This usually means the effect writes to a signal it reads. ` +
246
+ `The effect has been disposed to prevent a UI freeze.`);
247
+ node.disposed = true;
248
+ // Unsubscribe from all sources to stop further notifications
249
+ for (const source of node.sources) {
250
+ source.observers.delete(node);
251
+ }
252
+ node.sources.clear();
253
+ pendingEffects.delete(node);
254
+ return;
255
+ }
256
+ // Run cleanup from previous execution
257
+ if (node.cleanup) {
258
+ try {
259
+ node.cleanup();
260
+ }
261
+ catch (_) { /* cleanup errors are swallowed */ }
262
+ node.cleanup = null;
263
+ }
264
+ // Unsubscribe from previous sources
265
+ for (const source of node.sources) {
266
+ source.observers.delete(node);
267
+ }
268
+ node.sources.clear();
269
+ // Run effect with tracking
270
+ const prevObserver = activeObserver;
271
+ activeObserver = node;
272
+ try {
273
+ const result = node.fn();
274
+ if (typeof result === 'function') {
275
+ node.cleanup = result;
276
+ }
277
+ }
278
+ catch (err) {
279
+ // Effect errors: log but don't crash the system.
280
+ // The effect is NOT disposed — it may succeed on next signal change.
281
+ console.error('Effect error:', err);
282
+ }
283
+ finally {
284
+ activeObserver = prevObserver;
285
+ }
286
+ node.state = NodeState.Clean;
287
+ }
288
+ // ── Public API ──────────────────────────────────────────────────────
289
+ /**
290
+ * Create a writable reactive signal.
291
+ *
292
+ * ```ts
293
+ * const count = signal(0);
294
+ * count.value; // read (auto-tracks in reactive contexts)
295
+ * count.value = 5; // write (notifies dependents)
296
+ * ```
297
+ */
298
+ export function signal(initialValue) {
299
+ return new SignalImpl(initialValue);
300
+ }
301
+ /**
302
+ * Create a derived, lazy, cached computed signal.
303
+ * Re-evaluates only when dependencies change AND the value is read.
304
+ *
305
+ * ```ts
306
+ * const doubled = computed(() => count.value * 2);
307
+ * doubled.value; // lazy evaluation
308
+ * ```
309
+ */
310
+ export function computed(fn) {
311
+ return new ComputedImpl(fn);
312
+ }
313
+ /**
314
+ * Create a side-effect that re-runs when its dependencies change.
315
+ * Returns a dispose function to stop the effect.
316
+ *
317
+ * The effect function may return a cleanup callback that runs
318
+ * before each re-execution and on disposal.
319
+ *
320
+ * ```ts
321
+ * const dispose = effect(() => {
322
+ * console.log('count is', count.value);
323
+ * return () => { // cleanup
324
+ * console.log('cleaning up');
325
+ * };
326
+ * });
327
+ * dispose(); // stop the effect
328
+ * ```
329
+ */
330
+ export function effect(fn) {
331
+ const node = createEffectNode(fn);
332
+ // Run immediately to establish initial dependencies
333
+ runEffect(node);
334
+ const dispose = () => {
335
+ node.disposed = true;
336
+ // Run final cleanup
337
+ if (node.cleanup) {
338
+ try {
339
+ node.cleanup();
340
+ }
341
+ catch (_) { /* swallow */ }
342
+ node.cleanup = null;
343
+ }
344
+ // Unsubscribe from all sources
345
+ for (const source of node.sources) {
346
+ source.observers.delete(node);
347
+ }
348
+ node.sources.clear();
349
+ pendingEffects.delete(node);
350
+ };
351
+ // Auto-dispose when inside component setup context
352
+ if (contextHook) {
353
+ const addDisposer = contextHook();
354
+ if (addDisposer) {
355
+ addDisposer(dispose);
356
+ }
357
+ }
358
+ return dispose;
359
+ }
360
+ /**
361
+ * Check if a value is a signal (writable or computed).
362
+ * Used by the template engine to detect signals in expression slots.
363
+ */
364
+ export function isSignal(value) {
365
+ return (value !== null &&
366
+ typeof value === 'object' &&
367
+ SIGNAL_BRAND in value);
368
+ }
369
+ // ── Observer isolation ───────────────────────────────────────────────
370
+ /**
371
+ * Run a function with activeObserver set to null, preventing any
372
+ * signal reads inside `fn` from being tracked by an outer effect.
373
+ *
374
+ * Used by component.ts to isolate connectedCallback: child component
375
+ * setup signal reads must not leak into a parent effect's dependency
376
+ * set. See ADR 0008 Gap 1.
377
+ */
378
+ export function untrack(fn) {
379
+ const prev = activeObserver;
380
+ activeObserver = null;
381
+ try {
382
+ fn();
383
+ }
384
+ finally {
385
+ activeObserver = prev;
386
+ }
387
+ }
388
+ // ── Synchronous flush ───────────────────────────────────────────────
389
+ /**
390
+ * Synchronously execute all pending effects.
391
+ *
392
+ * In normal usage, effects run automatically on the next microtask.
393
+ * Use flush() only when you need effects to have executed before
394
+ * the next line — e.g., imperative DOM measurement after state change.
395
+ *
396
+ * flush() is idempotent: calling it with no pending effects is a no-op.
397
+ */
398
+ export function flush() {
399
+ flushPendingEffects();
400
+ }
401
+ /** Backward compat alias for tests. */
402
+ export { flush as flushEffects };
403
+ //# sourceMappingURL=signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.js","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * template.ts — Tagged template engine with targeted DOM binding
3
+ *
4
+ * The html tagged template literal parses HTML with expression slots,
5
+ * creates bindings for signals, and produces a TemplateResult that
6
+ * can be mounted into the DOM with surgical updates.
7
+ *
8
+ * Two phases:
9
+ * 1. First render: parse template, clone, create bindings, mount
10
+ * 2. Updates: signal changes trigger individual binding updates (O(1))
11
+ */
12
+ import { type ReadonlySignal } from './signal.js';
13
+ export interface TemplateResult {
14
+ /** Mount this template into a host element */
15
+ mount(host: HTMLElement): void;
16
+ /** Dispose all bindings and effects */
17
+ dispose(): void;
18
+ /** Brand for type checking */
19
+ __templateResult: true;
20
+ }
21
+ /**
22
+ * Tagged template literal for creating reactive DOM templates.
23
+ *
24
+ * ```ts
25
+ * const name = signal('World');
26
+ * const greeting = html`<h1>Hello, ${name}!</h1>`;
27
+ * ```
28
+ *
29
+ * Signals in expression slots are detected automatically and
30
+ * create fine-grained bindings. When the signal changes, only
31
+ * the specific text node or attribute updates — no diffing needed.
32
+ */
33
+ export declare function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;
34
+ /**
35
+ * Reactive list rendering with keyed reconciliation.
36
+ *
37
+ * Renders a list of items from a signal, tracking each item by key.
38
+ * When the array changes, only affected DOM nodes are added, removed,
39
+ * or reordered — existing items update in-place via their signals.
40
+ *
41
+ * ```ts
42
+ * const tasks = signal([{ id: '1', title: 'A' }, { id: '2', title: 'B' }]);
43
+ * html`<ul>${each(tasks, t => t.id, (task, index) =>
44
+ * html`<li>${computed(() => task.value.title)}</li>`
45
+ * )}</ul>`
46
+ * ```
47
+ */
48
+ export declare function each<T>(items: ReadonlySignal<T[]>, keyFn: (item: T, index: number) => string | number, templateFn: (item: ReadonlySignal<T>, index: ReadonlySignal<number>) => TemplateResult): TemplateResult;
49
+ export declare function when(condition: unknown, template: TemplateResult | (() => TemplateResult)): TemplateResult | ReadonlySignal<TemplateResult | null> | null;