ef.js

Declarative DOM helper experiment

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
---
status: accepted
---

# Use global context for building dependency graph

## Context and Problem Statement

The Signals architecture heavily relies on mutable global context variable to store computation scope for building dependency graph and ownership tree.
This is problematic in async and/or generator function, which run in other loops so the global variable had been changed at the time of a line inside the function body runs.

## Decision Drivers

- Need for work-around when using async, generator, or callback function makes API more complex.

## Considered Options

- Keep relying on mutable global context
- Algebraic Effect using generator function

Manual scope management is excluded as it is already considered in [Use Signals for reactive API](./0001-use-signals-for-reactive-api.md).

## Decision Outcome

Chosen option: "Keep relying on mutable global context", because one can't write useable and performant code using Algebraic Effect in the current state of JavaScript/TypeScript.
This decision may be re-considered once JavaScript introduces an Algebraic Effect into the language or both JavaScript and TypeScript improves generator function syntax.

## Pros and Cons of the Options

### Keep relying on mutable global context

This option does not change traditional Signals style with work-around API.

```ts
const $age = signal(10);

effect((ctx) => {
  (async () => {
    const isLegalToDrink = derived((ctx) => $age.get(ctx) >= 20);

    await somePromise;

    console.log($isLegalToDrink.get(ctx));
  })();
});
```

- Good, because it's simple function
- Bad, because it's error-prone (forget to pass `ctx`, using parent `ctx`, etc.)
- Bad, because this requires users to understand the concept of context

### Algebraic Effect using generator function

This option defines "lookup for dependant scope for the value" and "lookup for a scope owning this value or scope" as effects.

Effect handling is archived using generator function.
This can be easily adopted to async code.

```ts
const $age = signal(10);

effect(async function* () {
  const $isLegalToDrink = yield derived(function* () {
    // `.get()` or `.value` is still necessary in order to distinguish value retrieval from
    // ownership lookup.
    return (yield $age.get()) >= 20;
  });

  await somePromise;

  console.log(yield $isLegalToDrink.get());
});
```

- Good, because everything is explictly stated and visible
- Good, because it uses standard JavaScript syntax
- Good, because effects can be type-checked at compile time
- Bad, because Generator Function language feature is nearly abondand such as lack of usage on arrow function
- Bad, because TypeScript does not correctly model generator function (`yield`, `next()`)
- Bad, because using `yield*` to work-around TypeScript's `yield = any` bug introduces performance overhead on **runtime**
- Bad, because TypeScript is extreamly buggy on generator function even with `yield*` work-around
- Bad, because this is not a strictly effect function because the function is colored
- Bad, because every function that use `effect` or `derived` must be a generator function