fast reactive signals jsr.io/@mary/signals
typescript jsr
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

Mary da75d100

+759
+3
.vscode/settings.json
··· 1 + { 2 + "editor.defaultFormatter": "denoland.vscode-deno" 3 + }
+17
LICENSE
··· 1 + Permission is hereby granted, free of charge, to any person obtaining a copy 2 + of this software and associated documentation files (the "Software"), to deal 3 + in the Software without restriction, including without limitation the rights 4 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 + copies of the Software, and to permit persons to whom the Software is 6 + furnished to do so, subject to the following conditions: 7 + 8 + The above copyright notice and this permission notice shall be included in all 9 + copies or substantial portions of the Software. 10 + 11 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 + SOFTWARE.
+37
README.md
··· 1 + # signals 2 + 3 + Fast reactive signals. 4 + 5 + ```ts 6 + const name = signal(`Mary`); 7 + 8 + // Run a side effect that gets rerun on state changes... 9 + effect(() => { 10 + console.log(`Hello, ${name.value}!`); 11 + }); 12 + // logs `Hello, Mary!` 13 + 14 + // Combine multiple writes into a single update... 15 + batch(() => { 16 + name.value = `Elly`; 17 + name.value = `Alice!`; 18 + }); 19 + // logs `Hello, Alice!` 20 + 21 + // Run derivations that only gets updated as needed when not depended on... 22 + const doubled = computed(() => { 23 + console.log(`Computation ran!`); 24 + return name.repeat(2); 25 + }); 26 + 27 + doubled.value; 28 + // logs `Computation ran!` 29 + // -> `AliceAlice` 30 + 31 + name.value = `Alina`; 32 + // no logs as it's not being read under an effect yet! 33 + 34 + doubled.value; 35 + // logs `Computation ran!` 36 + // -> `AlinaAlina` 37 + ```
+20
deno.json
··· 1 + { 2 + "name": "@mary/signals", 3 + "version": "0.1.0", 4 + "exports": "./mod.ts", 5 + "fmt": { 6 + "useTabs": true, 7 + "indentWidth": 2, 8 + "lineWidth": 110, 9 + "semiColons": true, 10 + "singleQuote": true 11 + }, 12 + "lint": { 13 + "rules": { 14 + "exclude": ["no-explicit-any"] 15 + } 16 + }, 17 + "publish": { 18 + "exclude": [".vscode/"] 19 + } 20 + }
+682
mod.ts
··· 1 + const enum Flags { 2 + RUNNING = 1 << 0, 3 + DIRTY = 1 << 1, 4 + MAYBE_DIRTY = 1 << 2, 5 + TRACKING = 1 << 3, 6 + NOTIFIED = 1 << 4, 7 + DISPOSED = 1 << 5, 8 + HAS_ERROR = 1 << 6, 9 + } 10 + 11 + type Computation = Computed<any> | Effect<any>; 12 + 13 + /** currently evaluating listener */ 14 + export let eval_listener: Computation | undefined; 15 + 16 + /** pointer for checking existing dependencies in a context */ 17 + let eval_sources_index: number = 0; 18 + /** array of new dependencies to said context */ 19 + let eval_untracked_sources: Signal[] | undefined; 20 + 21 + /** effect scheduled for batching */ 22 + let batched_effect: Effect<any> | undefined; 23 + /** current batch depth */ 24 + let batch_depth: number = 0; 25 + /** current batch iteration */ 26 + let batch_iteration: number = 0; 27 + 28 + /** 29 + * Writing to a signal ticks this write clock forward and stores the new epoch 30 + * into that signal, this is used for contexts to know if they're stale by 31 + * comparing the epoch between it and its dependencies. 32 + */ 33 + let write_clock = 0; 34 + 35 + /** 36 + * Running a context ticks this read clock forward and stores the new epoch 37 + * into that context, this is used for duplicate-checking to see if the signal 38 + * has already been read by the context. 39 + * 40 + * Oversubscription isn't a big deal at all so really this isn't actually 41 + * needed, a duplicate dependency wouldn't cause any harm as we have a flag for 42 + * checking whether the context has been already been notified. 43 + */ 44 + let read_clock = 0; 45 + 46 + function start_batch(): void { 47 + batch_depth++; 48 + } 49 + 50 + function end_batch(): void { 51 + if (batch_depth > 1) { 52 + batch_depth--; 53 + return; 54 + } 55 + 56 + let error: unknown; 57 + let has_error = false; 58 + 59 + while (batched_effect !== undefined) { 60 + let effect: Effect | undefined = batched_effect; 61 + batched_effect = undefined; 62 + 63 + batch_iteration++; 64 + 65 + while (effect !== undefined) { 66 + const next: Effect | undefined = effect._next_batched_effect; 67 + const flags = (effect._flags &= ~Flags.NOTIFIED); 68 + effect._next_batched_effect = undefined; 69 + 70 + if (!(flags & Flags.DISPOSED) && is_stale(effect, flags)) { 71 + try { 72 + effect._refresh(); 73 + } catch (err) { 74 + if (!has_error) { 75 + has_error = true; 76 + error = err; 77 + } 78 + } 79 + } 80 + 81 + effect = next; 82 + } 83 + } 84 + 85 + batch_iteration = 0; 86 + batch_depth--; 87 + 88 + if (has_error) { 89 + throw error; 90 + } 91 + } 92 + 93 + function is_stale(target: Computation, flags: number): boolean { 94 + const dependencies = target._dependencies; 95 + 96 + if (flags & Flags.DIRTY) { 97 + return true; 98 + } 99 + 100 + if (flags & Flags.MAYBE_DIRTY) { 101 + for (let i = 0, ilen = dependencies.length; i < ilen; i++) { 102 + const source = dependencies[i]; 103 + 104 + if (source._epoch > target._epoch || source._refresh()) { 105 + return true; 106 + } 107 + } 108 + } 109 + 110 + return false; 111 + } 112 + 113 + function cleanup_context(): void { 114 + let dependencies = eval_listener!._dependencies; 115 + 116 + if (eval_untracked_sources) { 117 + // We have new dependencies, so let's unsubscribe from stale dependencies. 118 + prune_context_sources(); 119 + 120 + if (eval_sources_index > 0) { 121 + // We have existing dependencies still depended on, so let's expand the 122 + // existing array to make room for our new dependencies. 123 + const ilen = eval_untracked_sources.length; 124 + 125 + dependencies.length = eval_sources_index + ilen; 126 + 127 + // Override anything after the pointer with our new dependencies, this is 128 + // fine since we're no longer subscribed to them. 129 + for (let i = 0; i < ilen; i++) { 130 + dependencies[eval_sources_index + i] = eval_untracked_sources[i]; 131 + } 132 + } else { 133 + // There isn't any existing dependencies, so just replace the existing 134 + // array with the new one. 135 + dependencies = eval_listener!._dependencies = eval_untracked_sources; 136 + } 137 + 138 + // Now we subscribe to the new dependencies, but only if we're currently 139 + // configured as tracking. 140 + if (eval_listener!._flags & Flags.TRACKING) { 141 + for (let i = eval_sources_index, ilen = dependencies.length; i < ilen; i++) { 142 + const dep = dependencies[i]; 143 + 144 + dep._subscribe(eval_listener!); 145 + } 146 + } 147 + } else if (eval_sources_index < eval_listener!._dependencies.length) { 148 + // We don't have new dependencies, but the index pointer isn't pointing to 149 + // the end of the array, so we need to clean up the rest. 150 + prune_context_sources(); 151 + dependencies.length = eval_sources_index; 152 + } 153 + } 154 + 155 + function prune_context_sources(): void { 156 + const dependencies = eval_listener!._dependencies; 157 + 158 + for (let i = 0, ilen = dependencies.length; i < ilen; i++) { 159 + const dep = dependencies[i]; 160 + dep._unsubscribe(eval_listener!); 161 + } 162 + } 163 + 164 + function cleanup_effect(effect: Effect<any>): void { 165 + const cleanups = effect._cleanups; 166 + if (cleanups.length > 0) { 167 + const prev_listener = eval_listener; 168 + 169 + /*#__INLINE__*/ start_batch(); 170 + eval_listener = undefined; 171 + 172 + try { 173 + for (let i = 0, ilen = cleanups.length; i < ilen; i++) { 174 + (0, cleanups[i])(); 175 + } 176 + } catch (err) { 177 + // Failed to clean, so let's dispose of this effect. 178 + effect._flags = (effect._flags & ~Flags.RUNNING) | Flags.DISPOSED; 179 + dispose_effect(effect, false); 180 + 181 + throw err; 182 + } finally { 183 + cleanups.length = 0; 184 + 185 + eval_listener = prev_listener; 186 + 187 + end_batch(); 188 + } 189 + } 190 + } 191 + 192 + function dispose_effect(effect: Effect<any>, run_cleanup: boolean): void { 193 + const dependencies = effect._dependencies; 194 + 195 + for (let i = 0, ilen = dependencies.length; i < ilen; i++) { 196 + const dep = dependencies[i]; 197 + dep._unsubscribe(effect); 198 + } 199 + 200 + dependencies.length = 0; 201 + 202 + if (run_cleanup) { 203 + cleanup_effect(effect); 204 + } 205 + } 206 + 207 + export class Signal<T = unknown> { 208 + /** @internal Stored time of the write clock */ 209 + _epoch = -1; 210 + /** @internal Stored time of the read clock, used to detect dupe-reads */ 211 + _access_epoch = -1; 212 + /** @internal Contexts depending on it */ 213 + _dependants: Computation[] = []; 214 + 215 + /** @internal stored value */ 216 + _value: T; 217 + 218 + constructor(value: T) { 219 + this._value = value; 220 + } 221 + 222 + /** 223 + * @internal 224 + * Contexts will call this function to check if perhaps its dependencies are 225 + * stale, this is mostly needed for computed signals though, for a signal, 226 + * there's nothing new to be had once this method is called. 227 + */ 228 + _refresh(): boolean { 229 + // Returning `true` indicates that the dependant is now stale as a result, 230 + // e.g. computation has returned a different value. This just prevents the 231 + // need to check the epoch again so the epoch still needs to be incremented. 232 + return false; 233 + } 234 + 235 + /** @internal */ 236 + _subscribe(target: Computation): void { 237 + this._dependants.push(target); 238 + } 239 + 240 + /** @internal */ 241 + _unsubscribe(target: Computation): void { 242 + const dependants = this._dependants; 243 + const index = dependants.indexOf(target); 244 + 245 + dependants.splice(index, 1); 246 + } 247 + 248 + /** 249 + * Retrieves the value without being tracked as a dependant. 250 + */ 251 + peek(): T { 252 + return this._value; 253 + } 254 + 255 + /** 256 + * Retrieves the value, tracks the currently-running effect as a dependant. 257 + */ 258 + get value(): T { 259 + // Check if we're running under a context 260 + if (eval_listener !== undefined && eval_listener._context_epoch !== this._access_epoch) { 261 + // Store the read epoch, we don't need to recheck the dependencies of 262 + // this effect. 263 + this._access_epoch = eval_listener._context_epoch; 264 + 265 + // Dependency tracking is simple: does the index pointer point to us? 266 + // 267 + // - If so, then we're already depended on and we can increment the 268 + // pointer for the next signal. 269 + // 270 + // - If not, then we need to create a new dependency array and stop 271 + // incrementing the pointer, now that pointer acts as a dividing line 272 + // between signals that are still being depended, and are no longer 273 + // depended upon. The new dependencies can be concatenated afterwards. 274 + 275 + if (eval_untracked_sources !== undefined) { 276 + eval_untracked_sources.push(this); 277 + } else if (eval_listener._dependencies[eval_sources_index] === this) { 278 + eval_sources_index++; 279 + } else { 280 + eval_untracked_sources = [this]; 281 + } 282 + } 283 + 284 + return this._value; 285 + } 286 + set value(next: T) { 287 + if (this._value !== next) { 288 + // Tick the write clock forward 289 + this._epoch = ++write_clock; 290 + this._value = next; 291 + 292 + if (batch_iteration < 100) { 293 + const dependants = this._dependants; 294 + 295 + /*#__INLINE__*/ start_batch(); 296 + 297 + for (let i = 0, ilen = dependants.length; i < ilen; i++) { 298 + const dep = dependants[i]; 299 + 300 + // Source signal dependants are guaranteed to be dirty. 301 + dep._notify(Flags.DIRTY); 302 + } 303 + 304 + end_batch(); 305 + } 306 + } 307 + } 308 + } 309 + 310 + export interface ReadonlySignal<T> extends Signal<T> { 311 + readonly value: T; 312 + } 313 + 314 + export class Computed<T = unknown> extends Signal<T> { 315 + /** 316 + * @internal 317 + * Signals it's depending on 318 + */ 319 + _dependencies: Signal[] = []; 320 + /** 321 + * @internal 322 + * Context flags 323 + */ 324 + _flags: number = Flags.DIRTY; 325 + /** 326 + * @internal 327 + * This is mostly used for computed signals that currently aren't being 328 + * subscribed to, if nothing in the realm has changed, then it shouldn't be 329 + * possible for this computed signal to be stale from its dependencies. 330 + */ 331 + _realm_write_epoch = -1; 332 + /** 333 + * @internal 334 + * Stored time of the read clock 335 + */ 336 + _context_epoch = -1; 337 + 338 + /** 339 + * @internal 340 + * Compute function used to retrieve the value for this computed signal 341 + */ 342 + _compute: (prev: T) => T; 343 + 344 + constructor(compute: (prev: T) => T, initialValue: T) { 345 + super(initialValue); 346 + 347 + this._compute = compute; 348 + } 349 + 350 + /** @internal */ 351 + _refresh(): boolean { 352 + // Retrieve the current flags, make sure to unset NOTIFIED now that we're 353 + // running this refresh. 354 + const flags = (this._flags &= ~Flags.NOTIFIED); 355 + 356 + if ( 357 + // If our stored time matches current time then nothing in the realm has changed 358 + this._realm_write_epoch === write_clock || 359 + // If we're tracking, we can make use of DIRTY and MAYBE_DIRTY 360 + (flags & (Flags.TRACKING | Flags.DIRTY | Flags.MAYBE_DIRTY)) === Flags.TRACKING || 361 + // Prevent self-referential checks 362 + flags & Flags.RUNNING 363 + ) { 364 + return false; 365 + } 366 + 367 + this._flags = (flags & ~Flags.DIRTY & ~Flags.MAYBE_DIRTY) | Flags.RUNNING; 368 + this._realm_write_epoch = write_clock; 369 + 370 + if (this._epoch > -1 && !is_stale(this, flags)) { 371 + this._flags &= ~Flags.RUNNING; 372 + return false; 373 + } 374 + 375 + const prev_value = this._value; 376 + const prev_listener = eval_listener; 377 + const prev_sources = eval_untracked_sources; 378 + const prev_sources_index = eval_sources_index; 379 + 380 + let stale = false; 381 + let value: T; 382 + 383 + try { 384 + this._context_epoch = read_clock++; 385 + 386 + eval_listener = this; 387 + eval_untracked_sources = undefined; 388 + eval_sources_index = 0; 389 + 390 + value = (0, this._compute)(prev_value); 391 + 392 + if (flags & Flags.HAS_ERROR || prev_value !== value || this._epoch === -1) { 393 + stale = true; 394 + 395 + this._value = value; 396 + this._flags &= ~Flags.HAS_ERROR; 397 + this._epoch = this._realm_write_epoch = ++write_clock; 398 + } 399 + } catch (err) { 400 + // Always mark errors as stale 401 + stale = true; 402 + 403 + this._value = err as T; 404 + this._flags |= Flags.HAS_ERROR; 405 + this._epoch = this._realm_write_epoch = ++write_clock; 406 + } 407 + 408 + cleanup_context(); 409 + 410 + eval_listener = prev_listener; 411 + eval_untracked_sources = prev_sources; 412 + eval_sources_index = prev_sources_index; 413 + 414 + this._flags &= ~Flags.RUNNING; 415 + return stale; 416 + } 417 + 418 + /** @internal */ 419 + _subscribe(target: Computation): void { 420 + // Subscribe to our sources now that we have someone subscribing on us 421 + if (this._dependants.length < 1) { 422 + const dependencies = this._dependencies; 423 + this._flags |= Flags.TRACKING; 424 + 425 + for (let i = 0, ilen = dependencies.length; i < ilen; i++) { 426 + const dep = dependencies[i]; 427 + dep._subscribe(this); 428 + } 429 + } 430 + 431 + super._subscribe(target); 432 + } 433 + 434 + /** @internal */ 435 + _unsubscribe(target: Computation): void { 436 + super._unsubscribe(target); 437 + 438 + // Unsubscribe from our sources since there's no one subscribing to us 439 + if (this._dependants.length < 1) { 440 + const dependencies = this._dependencies; 441 + this._flags &= ~Flags.TRACKING; 442 + 443 + for (let i = 0, ilen = dependencies.length; i < ilen; i++) { 444 + const dep = dependencies[i]; 445 + dep._unsubscribe(this); 446 + } 447 + } 448 + } 449 + 450 + /** @internal */ 451 + _notify(flag: Flags.DIRTY | Flags.MAYBE_DIRTY): void { 452 + if (!(this._flags & (Flags.NOTIFIED | Flags.RUNNING))) { 453 + const dependants = this._dependants; 454 + 455 + this._flags |= flag | Flags.NOTIFIED; 456 + 457 + for (let i = 0, ilen = dependants.length; i < ilen; i++) { 458 + const dep = dependants[i]; 459 + 460 + // Computed signal dependants aren't guaranteed to be dirty. 461 + dep._notify(Flags.MAYBE_DIRTY); 462 + } 463 + } 464 + } 465 + 466 + /** 467 + * Retrieves the value without being tracked as a dependant 468 + */ 469 + peek(): T { 470 + this._refresh(); 471 + 472 + if (this._flags & Flags.HAS_ERROR) { 473 + throw this._value; 474 + } 475 + 476 + return this._value; 477 + } 478 + 479 + /** 480 + * Retrieves the value, tracks the currently-running effect as a dependant 481 + */ 482 + get value(): T { 483 + this._refresh(); 484 + 485 + if (this._flags & Flags.HAS_ERROR) { 486 + throw super.value; 487 + } 488 + 489 + return super.value; 490 + } 491 + } 492 + 493 + type CleanupFunction = () => void; 494 + 495 + export class Effect<T = void> { 496 + /** @internal Stored time of the write clock */ 497 + _epoch = -1; 498 + /** @internal Stored time of the read clock */ 499 + _context_epoch = -1; 500 + /** @internal Signals it's depending on */ 501 + _dependencies: Signal[] = []; 502 + /** @internal Context flags */ 503 + _flags = Flags.TRACKING; 504 + 505 + /** @internal Registered cleanup functions */ 506 + _cleanups: CleanupFunction[] = []; 507 + /** @internal Compute function for this effect */ 508 + _compute: (prev: T) => T; 509 + /** @internal Stored value from this compute */ 510 + _value!: T; 511 + 512 + /** @internal Batched effects are queued by a linked list on itself */ 513 + _next_batched_effect: Effect | undefined; 514 + 515 + constructor(compute: (prev: T) => T, initialValue: T) { 516 + this._compute = compute; 517 + this._value = initialValue; 518 + } 519 + 520 + /** @internal */ 521 + _refresh() { 522 + const flags = this._flags; 523 + 524 + if (flags & Flags.RUNNING) { 525 + return; 526 + } 527 + 528 + const prev_listener = eval_listener; 529 + const prev_sources = eval_untracked_sources; 530 + const prev_sources_index = eval_sources_index; 531 + 532 + try { 533 + /*#__INLINE__*/ start_batch(); 534 + 535 + eval_listener = this; 536 + eval_untracked_sources = undefined; 537 + eval_sources_index = 0; 538 + 539 + this._epoch = write_clock; 540 + this._context_epoch = read_clock++; 541 + this._flags = (flags & ~Flags.DIRTY & ~Flags.MAYBE_DIRTY) | Flags.RUNNING; 542 + 543 + this._value = (0, this._compute)(this._value); 544 + } finally { 545 + cleanup_context(); 546 + 547 + eval_listener = prev_listener; 548 + eval_untracked_sources = prev_sources; 549 + eval_sources_index = prev_sources_index; 550 + 551 + if ((this._flags &= ~Flags.RUNNING) & Flags.DISPOSED) { 552 + dispose_effect(this, true); 553 + } 554 + 555 + end_batch(); 556 + } 557 + } 558 + 559 + /** @internal */ 560 + _notify(flag: Flags.DIRTY | Flags.MAYBE_DIRTY): void { 561 + if (!(this._flags & (Flags.NOTIFIED | Flags.RUNNING))) { 562 + this._flags |= flag | Flags.NOTIFIED; 563 + this._next_batched_effect = batched_effect; 564 + batched_effect = this; 565 + } 566 + } 567 + 568 + /** @internal */ 569 + _dispose(): void { 570 + if (!((this._flags |= Flags.DISPOSED) & Flags.RUNNING)) { 571 + dispose_effect(this, true); 572 + } 573 + } 574 + } 575 + 576 + /** 577 + * Create a new signal, a container that can change value and subscribed to at 578 + * any given time. 579 + */ 580 + export function signal<T>(): Signal<T | undefined>; 581 + export function signal<T>(value: T): Signal<T>; 582 + export function signal<T>(value?: T): Signal<T | undefined> { 583 + return new Signal(value); 584 + } 585 + 586 + type NoInfer<T extends any> = [T][T extends any ? 0 : never]; 587 + 588 + export type ComputedFunction<Prev, Next extends Prev = Prev> = (v: Prev) => Next; 589 + 590 + /** 591 + * Create derivations of signals. 592 + */ 593 + export function computed<Next extends Prev, Prev = Next>( 594 + fn: ComputedFunction<undefined | NoInfer<Prev>, Next>, 595 + ): ReadonlySignal<Next>; 596 + export function computed<Next extends Prev, Init = Next, Prev = Next>( 597 + fn: ComputedFunction<Init | Prev, Next>, 598 + value: Init, 599 + ): ReadonlySignal<Next>; 600 + export function computed<Next extends Prev, Init, Prev>( 601 + fn: ComputedFunction<Init | Prev, Next>, 602 + value?: Init, 603 + ): ReadonlySignal<Next> { 604 + // @ts-expect-error: messy overloads 605 + return new Computed(fn, value); 606 + } 607 + 608 + export type EffectFunction<Prev, Next extends Prev = Prev> = (v: Prev) => Next; 609 + 610 + /** 611 + * Run side-effects that get rerun when one of its signal dependencies change. 612 + */ 613 + export function effect<Next extends Prev, Prev = Next>( 614 + fn: EffectFunction<undefined | NoInfer<Prev>, Next>, 615 + ): void; 616 + export function effect<Next extends Prev, Init = Next, Prev = Next>( 617 + fn: EffectFunction<Init | Prev, Next>, 618 + value: Init, 619 + ): void; 620 + export function effect<Next extends Prev, Init, Prev>( 621 + fn: EffectFunction<Init | Prev, Next>, 622 + value?: Init, 623 + ): void { 624 + // @ts-expect-error - messy overloads 625 + const instance = new Effect(fn, value); 626 + 627 + try { 628 + instance._refresh(); 629 + } catch (err) { 630 + instance._dispose(); 631 + throw err; 632 + } 633 + } 634 + 635 + /** 636 + * Adds a cleanup function that gets run when an effect is rerun or destroyed 637 + * @param fn Cleanup function to run 638 + * @param throws Whether to throw if not under an effect, defaults to true 639 + */ 640 + export function cleanup(fn: () => void, throws = true) { 641 + if (eval_listener instanceof Effect) { 642 + eval_listener._cleanups.push(fn); 643 + } else if (throws) { 644 + throw new Error(`Cleanup function called outside of effect`); 645 + } 646 + } 647 + 648 + /** 649 + * Read signal values without being tracked as a dependency 650 + */ 651 + export function untrack<T>(callback: () => T): T { 652 + if (eval_listener === undefined) { 653 + return callback(); 654 + } 655 + 656 + const prev_listener = eval_listener; 657 + eval_listener = undefined; 658 + 659 + try { 660 + return callback(); 661 + } finally { 662 + eval_listener = prev_listener; 663 + } 664 + } 665 + 666 + /** 667 + * Combines multiple signal writes into one single update that gets triggered at 668 + * the end of the callback 669 + */ 670 + export function batch(callback: () => void): void { 671 + if (batch_depth > 0) { 672 + return callback(); 673 + } 674 + 675 + /* @__INLINE__ */ start_batch(); 676 + 677 + try { 678 + return callback(); 679 + } finally { 680 + end_batch(); 681 + } 682 + }