@tldraw/state 5.3.2 → 5.4.0-canary.02cd0bd3b597

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.
Files changed (71) hide show
  1. package/DOCS.md +64 -63
  2. package/README.md +35 -36
  3. package/dist-cjs/index.d.ts +26 -27
  4. package/dist-cjs/index.js +1 -1
  5. package/dist-cjs/lib/ArraySet.js +47 -144
  6. package/dist-cjs/lib/ArraySet.js.map +2 -2
  7. package/dist-cjs/lib/Atom.js +12 -26
  8. package/dist-cjs/lib/Atom.js.map +2 -2
  9. package/dist-cjs/lib/Computed.js +36 -64
  10. package/dist-cjs/lib/Computed.js.map +2 -2
  11. package/dist-cjs/lib/EffectScheduler.js +1 -1
  12. package/dist-cjs/lib/EffectScheduler.js.map +2 -2
  13. package/dist-cjs/lib/HistoryBuffer.js +8 -8
  14. package/dist-cjs/lib/HistoryBuffer.js.map +2 -2
  15. package/dist-cjs/lib/capture.js +1 -3
  16. package/dist-cjs/lib/capture.js.map +2 -2
  17. package/dist-cjs/lib/constants.js.map +2 -2
  18. package/dist-cjs/lib/helpers.js +3 -11
  19. package/dist-cjs/lib/helpers.js.map +2 -2
  20. package/dist-cjs/lib/localStorageAtom.js +7 -2
  21. package/dist-cjs/lib/localStorageAtom.js.map +2 -2
  22. package/dist-cjs/lib/transactions.js +11 -19
  23. package/dist-cjs/lib/transactions.js.map +2 -2
  24. package/dist-cjs/lib/types.js.map +1 -1
  25. package/dist-cjs/lib/warnings.js +2 -4
  26. package/dist-cjs/lib/warnings.js.map +2 -2
  27. package/dist-esm/index.d.mts +26 -27
  28. package/dist-esm/index.mjs +1 -1
  29. package/dist-esm/lib/ArraySet.mjs +47 -144
  30. package/dist-esm/lib/ArraySet.mjs.map +2 -2
  31. package/dist-esm/lib/Atom.mjs +12 -26
  32. package/dist-esm/lib/Atom.mjs.map +2 -2
  33. package/dist-esm/lib/Computed.mjs +36 -64
  34. package/dist-esm/lib/Computed.mjs.map +2 -2
  35. package/dist-esm/lib/EffectScheduler.mjs +1 -1
  36. package/dist-esm/lib/EffectScheduler.mjs.map +2 -2
  37. package/dist-esm/lib/HistoryBuffer.mjs +8 -8
  38. package/dist-esm/lib/HistoryBuffer.mjs.map +2 -2
  39. package/dist-esm/lib/capture.mjs +1 -3
  40. package/dist-esm/lib/capture.mjs.map +2 -2
  41. package/dist-esm/lib/constants.mjs.map +2 -2
  42. package/dist-esm/lib/helpers.mjs +3 -11
  43. package/dist-esm/lib/helpers.mjs.map +2 -2
  44. package/dist-esm/lib/localStorageAtom.mjs +7 -2
  45. package/dist-esm/lib/localStorageAtom.mjs.map +2 -2
  46. package/dist-esm/lib/transactions.mjs +11 -19
  47. package/dist-esm/lib/transactions.mjs.map +2 -2
  48. package/dist-esm/lib/types.mjs.map +1 -1
  49. package/dist-esm/lib/warnings.mjs +2 -4
  50. package/dist-esm/lib/warnings.mjs.map +2 -2
  51. package/package.json +2 -2
  52. package/src/lib/ArraySet.ts +68 -176
  53. package/src/lib/Atom.ts +27 -31
  54. package/src/lib/Computed.ts +68 -96
  55. package/src/lib/EffectScheduler.ts +9 -8
  56. package/src/lib/HistoryBuffer.ts +12 -10
  57. package/src/lib/__tests__/ArraySet.test.ts +39 -13
  58. package/src/lib/__tests__/EffectScheduler.test.ts +18 -0
  59. package/src/lib/__tests__/HistoryBuffer.test.ts +6 -3
  60. package/src/lib/__tests__/computed.test.ts +75 -0
  61. package/src/lib/__tests__/errors.test.ts +24 -0
  62. package/src/lib/__tests__/helpers.test.ts +7 -11
  63. package/src/lib/__tests__/history.test.ts +32 -2
  64. package/src/lib/__tests__/localStorageAtom.test.ts +15 -0
  65. package/src/lib/capture.ts +13 -13
  66. package/src/lib/constants.ts +3 -22
  67. package/src/lib/helpers.ts +15 -140
  68. package/src/lib/localStorageAtom.ts +9 -2
  69. package/src/lib/transactions.ts +23 -47
  70. package/src/lib/types.ts +7 -7
  71. package/src/lib/warnings.ts +2 -10
@@ -1,53 +1,26 @@
1
1
  const ARRAY_SIZE_THRESHOLD = 8;
2
2
  class ArraySet {
3
- arraySize = 0;
4
- array = Array(ARRAY_SIZE_THRESHOLD);
5
3
  set = null;
4
+ // Slots [0, arraySize) hold the items; slots beyond are undefined. `add`/`has` scan the whole
5
+ // array with indexOf, which is why `clear` and `remove` must blank vacated slots.
6
+ array = null;
7
+ arraySize = 0;
6
8
  /**
7
9
  * Get whether this ArraySet has any elements.
8
- *
9
- * @returns True if this ArraySet has any elements, false otherwise.
10
10
  */
11
11
  // eslint-disable-next-line tldraw/no-setter-getter
12
12
  get isEmpty() {
13
- if (this.array) {
14
- return this.arraySize === 0;
15
- }
16
13
  if (this.set) {
17
14
  return this.set.size === 0;
18
15
  }
19
- throw new Error("no set or array");
16
+ return this.arraySize === 0;
20
17
  }
21
18
  /**
22
19
  * Add an element to the ArraySet if it is not already present.
23
20
  *
24
- * @param elem - The element to add to the set
25
21
  * @returns `true` if the element was added, `false` if it was already present
26
- * @example
27
- * ```ts
28
- * const arraySet = new ArraySet<string>()
29
- *
30
- * console.log(arraySet.add('hello')) // true
31
- * console.log(arraySet.add('hello')) // false (already exists)
32
- * ```
33
22
  */
34
23
  add(elem) {
35
- if (this.array) {
36
- const idx = this.array.indexOf(elem);
37
- if (idx !== -1) {
38
- return false;
39
- }
40
- if (this.arraySize < ARRAY_SIZE_THRESHOLD) {
41
- this.array[this.arraySize] = elem;
42
- this.arraySize++;
43
- return true;
44
- } else {
45
- this.set = new Set(this.array);
46
- this.array = null;
47
- this.set.add(elem);
48
- return true;
49
- }
50
- }
51
24
  if (this.set) {
52
25
  if (this.set.has(elem)) {
53
26
  return false;
@@ -55,168 +28,98 @@ class ArraySet {
55
28
  this.set.add(elem);
56
29
  return true;
57
30
  }
58
- throw new Error("no set or array");
31
+ if (!this.array) {
32
+ this.array = Array(ARRAY_SIZE_THRESHOLD);
33
+ } else if (this.array.indexOf(elem) !== -1) {
34
+ return false;
35
+ }
36
+ if (this.arraySize < ARRAY_SIZE_THRESHOLD) {
37
+ this.array[this.arraySize] = elem;
38
+ this.arraySize++;
39
+ return true;
40
+ }
41
+ this.set = new Set(this.array);
42
+ this.set.add(elem);
43
+ this.array = null;
44
+ this.arraySize = 0;
45
+ return true;
59
46
  }
60
47
  /**
61
48
  * Remove an element from the ArraySet if it is present.
62
49
  *
63
- * @param elem - The element to remove from the set
64
50
  * @returns `true` if the element was removed, `false` if it was not present
65
- * @example
66
- * ```ts
67
- * const arraySet = new ArraySet<string>()
68
- * arraySet.add('hello')
69
- *
70
- * console.log(arraySet.remove('hello')) // true
71
- * console.log(arraySet.remove('hello')) // false (not present)
72
- * ```
73
51
  */
74
52
  remove(elem) {
75
- if (this.array) {
76
- const idx = this.array.indexOf(elem);
77
- if (idx === -1) {
78
- return false;
79
- }
80
- this.array[idx] = void 0;
81
- this.arraySize--;
82
- if (idx !== this.arraySize) {
83
- this.array[idx] = this.array[this.arraySize];
84
- this.array[this.arraySize] = void 0;
85
- }
86
- return true;
87
- }
88
53
  if (this.set) {
89
- if (!this.set.has(elem)) {
90
- return false;
91
- }
92
- this.set.delete(elem);
93
- return true;
54
+ return this.set.delete(elem);
55
+ }
56
+ if (!this.array) {
57
+ return false;
58
+ }
59
+ const idx = this.array.indexOf(elem);
60
+ if (idx === -1) {
61
+ return false;
94
62
  }
95
- throw new Error("no set or array");
63
+ this.arraySize--;
64
+ this.array[idx] = this.array[this.arraySize];
65
+ this.array[this.arraySize] = void 0;
66
+ return true;
96
67
  }
97
68
  /**
98
69
  * Execute a callback function for each element in the ArraySet.
99
- *
100
- * @param visitor - A function to call for each element in the set
101
- * @example
102
- * ```ts
103
- * const arraySet = new ArraySet<string>()
104
- * arraySet.add('hello')
105
- * arraySet.add('world')
106
- *
107
- * arraySet.visit((item) => {
108
- * console.log(item) // 'hello', 'world'
109
- * })
110
- * ```
111
70
  */
112
71
  visit(visitor) {
113
- if (this.array) {
114
- for (let i = 0; i < this.arraySize; i++) {
115
- const elem = this.array[i];
116
- if (typeof elem !== "undefined") {
117
- visitor(elem);
118
- }
119
- }
120
- return;
121
- }
122
72
  if (this.set) {
123
73
  this.set.forEach(visitor);
124
74
  return;
125
75
  }
126
- throw new Error("no set or array");
76
+ if (!this.array) {
77
+ return;
78
+ }
79
+ for (let i = 0; i < this.arraySize; i++) {
80
+ visitor(this.array[i]);
81
+ }
127
82
  }
128
83
  /**
129
84
  * Make the ArraySet iterable, allowing it to be used in for...of loops and with spread syntax.
130
- *
131
- * @returns An iterator that yields each element in the set
132
- * @example
133
- * ```ts
134
- * const arraySet = new ArraySet<number>()
135
- * arraySet.add(1)
136
- * arraySet.add(2)
137
- *
138
- * for (const item of arraySet) {
139
- * console.log(item) // 1, 2
140
- * }
141
- *
142
- * const items = [...arraySet] // [1, 2]
143
- * ```
144
85
  */
145
86
  *[Symbol.iterator]() {
146
- if (this.array) {
87
+ if (this.set) {
88
+ yield* this.set;
89
+ } else if (this.array) {
147
90
  for (let i = 0; i < this.arraySize; i++) {
148
- const elem = this.array[i];
149
- if (typeof elem !== "undefined") {
150
- yield elem;
151
- }
91
+ yield this.array[i];
152
92
  }
153
- } else if (this.set) {
154
- yield* this.set;
155
- } else {
156
- throw new Error("no set or array");
157
93
  }
158
94
  }
159
95
  /**
160
96
  * Check whether an element is present in the ArraySet.
161
- *
162
- * @param elem - The element to check for
163
- * @returns `true` if the element is present, `false` otherwise
164
- * @example
165
- * ```ts
166
- * const arraySet = new ArraySet<string>()
167
- * arraySet.add('hello')
168
- *
169
- * console.log(arraySet.has('hello')) // true
170
- * console.log(arraySet.has('world')) // false
171
- * ```
172
97
  */
173
98
  has(elem) {
174
- if (this.array) {
175
- return this.array.indexOf(elem) !== -1;
176
- } else {
99
+ if (this.set) {
177
100
  return this.set.has(elem);
178
101
  }
102
+ return this.array ? this.array.indexOf(elem) !== -1 : false;
179
103
  }
180
104
  /**
181
105
  * Remove all elements from the ArraySet.
182
- *
183
- * @example
184
- * ```ts
185
- * const arraySet = new ArraySet<string>()
186
- * arraySet.add('hello')
187
- * arraySet.add('world')
188
- *
189
- * arraySet.clear()
190
- * console.log(arraySet.size()) // 0
191
- * ```
192
106
  */
193
107
  clear() {
194
108
  if (this.set) {
195
109
  this.set.clear();
196
- } else {
110
+ } else if (this.array) {
111
+ this.array.fill(void 0, 0, this.arraySize);
197
112
  this.arraySize = 0;
198
- this.array = [];
199
113
  }
200
114
  }
201
115
  /**
202
116
  * Get the number of elements in the ArraySet.
203
- *
204
- * @returns The number of elements in the set
205
- * @example
206
- * ```ts
207
- * const arraySet = new ArraySet<string>()
208
- * console.log(arraySet.size()) // 0
209
- *
210
- * arraySet.add('hello')
211
- * console.log(arraySet.size()) // 1
212
- * ```
213
117
  */
214
118
  size() {
215
119
  if (this.set) {
216
120
  return this.set.size;
217
- } else {
218
- return this.arraySize;
219
121
  }
122
+ return this.arraySize;
220
123
  }
221
124
  }
222
125
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/ArraySet.ts"],
4
- "sourcesContent": ["/**\n * The maximum number of items that can be stored in an ArraySet in array mode before switching to Set mode.\n *\n * @public\n * @example\n * ```ts\n * import { ARRAY_SIZE_THRESHOLD } from '@tldraw/state'\n *\n * console.log(ARRAY_SIZE_THRESHOLD) // 8\n * ```\n */\nexport const ARRAY_SIZE_THRESHOLD = 8\n\n/**\n * An ArraySet operates as an array until it reaches a certain size, after which a Set is used\n * instead. In either case, the same methods are used to get, set, remove, and visit the items.\n * @internal\n */\nexport class ArraySet<T> {\n\tprivate arraySize = 0\n\n\tprivate array: (T | undefined)[] | null = Array(ARRAY_SIZE_THRESHOLD)\n\n\tprivate set: Set<T> | null = null\n\n\t/**\n\t * Get whether this ArraySet has any elements.\n\t *\n\t * @returns True if this ArraySet has any elements, false otherwise.\n\t */\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget isEmpty() {\n\t\tif (this.array) {\n\t\t\treturn this.arraySize === 0\n\t\t}\n\n\t\tif (this.set) {\n\t\t\treturn this.set.size === 0\n\t\t}\n\n\t\tthrow new Error('no set or array')\n\t}\n\n\t/**\n\t * Add an element to the ArraySet if it is not already present.\n\t *\n\t * @param elem - The element to add to the set\n\t * @returns `true` if the element was added, `false` if it was already present\n\t * @example\n\t * ```ts\n\t * const arraySet = new ArraySet<string>()\n\t *\n\t * console.log(arraySet.add('hello')) // true\n\t * console.log(arraySet.add('hello')) // false (already exists)\n\t * ```\n\t */\n\tadd(elem: T) {\n\t\tif (this.array) {\n\t\t\tconst idx = this.array.indexOf(elem)\n\n\t\t\t// Return false if the element is already in the array.\n\t\t\tif (idx !== -1) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tif (this.arraySize < ARRAY_SIZE_THRESHOLD) {\n\t\t\t\t// If the array is below the size threshold, push items into the array.\n\n\t\t\t\t// Insert the element into the array's next available slot.\n\t\t\t\tthis.array[this.arraySize] = elem\n\t\t\t\tthis.arraySize++\n\n\t\t\t\treturn true\n\t\t\t} else {\n\t\t\t\t// If the array is full, convert it to a set and remove the array.\n\t\t\t\tthis.set = new Set(this.array as any)\n\t\t\t\tthis.array = null\n\t\t\t\tthis.set.add(elem)\n\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\tif (this.set) {\n\t\t\t// Return false if the element is already in the set.\n\t\t\tif (this.set.has(elem)) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tthis.set.add(elem)\n\t\t\treturn true\n\t\t}\n\n\t\tthrow new Error('no set or array')\n\t}\n\n\t/**\n\t * Remove an element from the ArraySet if it is present.\n\t *\n\t * @param elem - The element to remove from the set\n\t * @returns `true` if the element was removed, `false` if it was not present\n\t * @example\n\t * ```ts\n\t * const arraySet = new ArraySet<string>()\n\t * arraySet.add('hello')\n\t *\n\t * console.log(arraySet.remove('hello')) // true\n\t * console.log(arraySet.remove('hello')) // false (not present)\n\t * ```\n\t */\n\tremove(elem: T) {\n\t\tif (this.array) {\n\t\t\tconst idx = this.array.indexOf(elem)\n\n\t\t\t// If the item is not in the array, return false.\n\t\t\tif (idx === -1) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tthis.array[idx] = undefined\n\t\t\tthis.arraySize--\n\n\t\t\tif (idx !== this.arraySize) {\n\t\t\t\t// If the item is not the last item in the array, move the last item into the\n\t\t\t\t// removed item's slot.\n\t\t\t\tthis.array[idx] = this.array[this.arraySize]\n\t\t\t\tthis.array[this.arraySize] = undefined\n\t\t\t}\n\n\t\t\treturn true\n\t\t}\n\n\t\tif (this.set) {\n\t\t\t// If the item is not in the set, return false.\n\t\t\tif (!this.set.has(elem)) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tthis.set.delete(elem)\n\n\t\t\treturn true\n\t\t}\n\n\t\tthrow new Error('no set or array')\n\t}\n\n\t/**\n\t * Execute a callback function for each element in the ArraySet.\n\t *\n\t * @param visitor - A function to call for each element in the set\n\t * @example\n\t * ```ts\n\t * const arraySet = new ArraySet<string>()\n\t * arraySet.add('hello')\n\t * arraySet.add('world')\n\t *\n\t * arraySet.visit((item) => {\n\t * console.log(item) // 'hello', 'world'\n\t * })\n\t * ```\n\t */\n\tvisit(visitor: (item: T) => void) {\n\t\tif (this.array) {\n\t\t\tfor (let i = 0; i < this.arraySize; i++) {\n\t\t\t\tconst elem = this.array[i]\n\n\t\t\t\tif (typeof elem !== 'undefined') {\n\t\t\t\t\tvisitor(elem)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif (this.set) {\n\t\t\tthis.set.forEach(visitor)\n\n\t\t\treturn\n\t\t}\n\n\t\tthrow new Error('no set or array')\n\t}\n\n\t/**\n\t * Make the ArraySet iterable, allowing it to be used in for...of loops and with spread syntax.\n\t *\n\t * @returns An iterator that yields each element in the set\n\t * @example\n\t * ```ts\n\t * const arraySet = new ArraySet<number>()\n\t * arraySet.add(1)\n\t * arraySet.add(2)\n\t *\n\t * for (const item of arraySet) {\n\t * console.log(item) // 1, 2\n\t * }\n\t *\n\t * const items = [...arraySet] // [1, 2]\n\t * ```\n\t */\n\t*[Symbol.iterator]() {\n\t\tif (this.array) {\n\t\t\tfor (let i = 0; i < this.arraySize; i++) {\n\t\t\t\tconst elem = this.array[i]\n\n\t\t\t\tif (typeof elem !== 'undefined') {\n\t\t\t\t\tyield elem\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (this.set) {\n\t\t\tyield* this.set\n\t\t} else {\n\t\t\tthrow new Error('no set or array')\n\t\t}\n\t}\n\n\t/**\n\t * Check whether an element is present in the ArraySet.\n\t *\n\t * @param elem - The element to check for\n\t * @returns `true` if the element is present, `false` otherwise\n\t * @example\n\t * ```ts\n\t * const arraySet = new ArraySet<string>()\n\t * arraySet.add('hello')\n\t *\n\t * console.log(arraySet.has('hello')) // true\n\t * console.log(arraySet.has('world')) // false\n\t * ```\n\t */\n\thas(elem: T) {\n\t\tif (this.array) {\n\t\t\treturn this.array.indexOf(elem) !== -1\n\t\t} else {\n\t\t\treturn this.set!.has(elem)\n\t\t}\n\t}\n\n\t/**\n\t * Remove all elements from the ArraySet.\n\t *\n\t * @example\n\t * ```ts\n\t * const arraySet = new ArraySet<string>()\n\t * arraySet.add('hello')\n\t * arraySet.add('world')\n\t *\n\t * arraySet.clear()\n\t * console.log(arraySet.size()) // 0\n\t * ```\n\t */\n\tclear() {\n\t\tif (this.set) {\n\t\t\tthis.set.clear()\n\t\t} else {\n\t\t\tthis.arraySize = 0\n\t\t\tthis.array = []\n\t\t}\n\t}\n\n\t/**\n\t * Get the number of elements in the ArraySet.\n\t *\n\t * @returns The number of elements in the set\n\t * @example\n\t * ```ts\n\t * const arraySet = new ArraySet<string>()\n\t * console.log(arraySet.size()) // 0\n\t *\n\t * arraySet.add('hello')\n\t * console.log(arraySet.size()) // 1\n\t * ```\n\t */\n\tsize() {\n\t\tif (this.set) {\n\t\t\treturn this.set.size\n\t\t} else {\n\t\t\treturn this.arraySize\n\t\t}\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * The number of items an ArraySet holds in array mode before switching to a Set.\n * Exported only for tests.\n * @internal\n */\nexport const ARRAY_SIZE_THRESHOLD = 8\n\n/**\n * An ArraySet operates as an array until it reaches a certain size, after which a Set is used\n * instead. In either case, the same methods are used to get, set, remove, and visit the items.\n *\n * `set` and `array` are never both non-null. `set` being null means array mode, but the array\n * itself is only allocated on the first `add` (most signals never get a child, and an empty\n * ArraySet is created for every atom and effect, and two for every computed), so array-mode code\n * must handle `array === null`; `arraySize` is 0 in that state. Once promoted to a set, an\n * ArraySet never goes back.\n * @internal\n */\nexport class ArraySet<T> {\n\tprivate set: Set<T> | null = null\n\n\t// Slots [0, arraySize) hold the items; slots beyond are undefined. `add`/`has` scan the whole\n\t// array with indexOf, which is why `clear` and `remove` must blank vacated slots.\n\tprivate array: (T | undefined)[] | null = null\n\n\tprivate arraySize = 0\n\n\t/**\n\t * Get whether this ArraySet has any elements.\n\t */\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget isEmpty() {\n\t\tif (this.set) {\n\t\t\treturn this.set.size === 0\n\t\t}\n\n\t\treturn this.arraySize === 0\n\t}\n\n\t/**\n\t * Add an element to the ArraySet if it is not already present.\n\t *\n\t * @returns `true` if the element was added, `false` if it was already present\n\t */\n\tadd(elem: T) {\n\t\tif (this.set) {\n\t\t\tif (this.set.has(elem)) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tthis.set.add(elem)\n\t\t\treturn true\n\t\t}\n\n\t\tif (!this.array) {\n\t\t\tthis.array = Array(ARRAY_SIZE_THRESHOLD)\n\t\t} else if (this.array.indexOf(elem) !== -1) {\n\t\t\treturn false\n\t\t}\n\n\t\tif (this.arraySize < ARRAY_SIZE_THRESHOLD) {\n\t\t\tthis.array[this.arraySize] = elem\n\t\t\tthis.arraySize++\n\n\t\t\treturn true\n\t\t}\n\n\t\t// The array is full: promote to a set.\n\t\tthis.set = new Set(this.array as T[])\n\t\tthis.set.add(elem)\n\t\tthis.array = null\n\t\tthis.arraySize = 0\n\n\t\treturn true\n\t}\n\n\t/**\n\t * Remove an element from the ArraySet if it is present.\n\t *\n\t * @returns `true` if the element was removed, `false` if it was not present\n\t */\n\tremove(elem: T) {\n\t\tif (this.set) {\n\t\t\treturn this.set.delete(elem)\n\t\t}\n\n\t\tif (!this.array) {\n\t\t\treturn false\n\t\t}\n\n\t\tconst idx = this.array.indexOf(elem)\n\n\t\tif (idx === -1) {\n\t\t\treturn false\n\t\t}\n\n\t\tthis.arraySize--\n\n\t\t// Move the last item into the vacated slot so the items stay dense.\n\t\tthis.array[idx] = this.array[this.arraySize]\n\t\tthis.array[this.arraySize] = undefined\n\n\t\treturn true\n\t}\n\n\t/**\n\t * Execute a callback function for each element in the ArraySet.\n\t */\n\tvisit(visitor: (item: T) => void) {\n\t\tif (this.set) {\n\t\t\tthis.set.forEach(visitor)\n\n\t\t\treturn\n\t\t}\n\n\t\tif (!this.array) {\n\t\t\treturn\n\t\t}\n\n\t\tfor (let i = 0; i < this.arraySize; i++) {\n\t\t\tvisitor(this.array[i]!)\n\t\t}\n\t}\n\n\t/**\n\t * Make the ArraySet iterable, allowing it to be used in for...of loops and with spread syntax.\n\t */\n\t*[Symbol.iterator]() {\n\t\tif (this.set) {\n\t\t\tyield* this.set\n\t\t} else if (this.array) {\n\t\t\tfor (let i = 0; i < this.arraySize; i++) {\n\t\t\t\tyield this.array[i]!\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Check whether an element is present in the ArraySet.\n\t */\n\thas(elem: T) {\n\t\tif (this.set) {\n\t\t\treturn this.set.has(elem)\n\t\t}\n\n\t\treturn this.array ? this.array.indexOf(elem) !== -1 : false\n\t}\n\n\t/**\n\t * Remove all elements from the ArraySet.\n\t */\n\tclear() {\n\t\tif (this.set) {\n\t\t\tthis.set.clear()\n\t\t} else if (this.array) {\n\t\t\t// Blank the used slots in place rather than allocating a new array: this runs on every\n\t\t\t// computed derive and effect run.\n\t\t\tthis.array.fill(undefined, 0, this.arraySize)\n\t\t\tthis.arraySize = 0\n\t\t}\n\t}\n\n\t/**\n\t * Get the number of elements in the ArraySet.\n\t */\n\tsize() {\n\t\tif (this.set) {\n\t\t\treturn this.set.size\n\t\t}\n\n\t\treturn this.arraySize\n\t}\n}\n"],
5
+ "mappings": "AAKO,MAAM,uBAAuB;AAa7B,MAAM,SAAY;AAAA,EAChB,MAAqB;AAAA;AAAA;AAAA,EAIrB,QAAkC;AAAA,EAElC,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAMpB,IAAI,UAAU;AACb,QAAI,KAAK,KAAK;AACb,aAAO,KAAK,IAAI,SAAS;AAAA,IAC1B;AAEA,WAAO,KAAK,cAAc;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,MAAS;AACZ,QAAI,KAAK,KAAK;AACb,UAAI,KAAK,IAAI,IAAI,IAAI,GAAG;AACvB,eAAO;AAAA,MACR;AAEA,WAAK,IAAI,IAAI,IAAI;AACjB,aAAO;AAAA,IACR;AAEA,QAAI,CAAC,KAAK,OAAO;AAChB,WAAK,QAAQ,MAAM,oBAAoB;AAAA,IACxC,WAAW,KAAK,MAAM,QAAQ,IAAI,MAAM,IAAI;AAC3C,aAAO;AAAA,IACR;AAEA,QAAI,KAAK,YAAY,sBAAsB;AAC1C,WAAK,MAAM,KAAK,SAAS,IAAI;AAC7B,WAAK;AAEL,aAAO;AAAA,IACR;AAGA,SAAK,MAAM,IAAI,IAAI,KAAK,KAAY;AACpC,SAAK,IAAI,IAAI,IAAI;AACjB,SAAK,QAAQ;AACb,SAAK,YAAY;AAEjB,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,MAAS;AACf,QAAI,KAAK,KAAK;AACb,aAAO,KAAK,IAAI,OAAO,IAAI;AAAA,IAC5B;AAEA,QAAI,CAAC,KAAK,OAAO;AAChB,aAAO;AAAA,IACR;AAEA,UAAM,MAAM,KAAK,MAAM,QAAQ,IAAI;AAEnC,QAAI,QAAQ,IAAI;AACf,aAAO;AAAA,IACR;AAEA,SAAK;AAGL,SAAK,MAAM,GAAG,IAAI,KAAK,MAAM,KAAK,SAAS;AAC3C,SAAK,MAAM,KAAK,SAAS,IAAI;AAE7B,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAA4B;AACjC,QAAI,KAAK,KAAK;AACb,WAAK,IAAI,QAAQ,OAAO;AAExB;AAAA,IACD;AAEA,QAAI,CAAC,KAAK,OAAO;AAChB;AAAA,IACD;AAEA,aAAS,IAAI,GAAG,IAAI,KAAK,WAAW,KAAK;AACxC,cAAQ,KAAK,MAAM,CAAC,CAAE;AAAA,IACvB;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,EAAE,OAAO,QAAQ,IAAI;AACpB,QAAI,KAAK,KAAK;AACb,aAAO,KAAK;AAAA,IACb,WAAW,KAAK,OAAO;AACtB,eAAS,IAAI,GAAG,IAAI,KAAK,WAAW,KAAK;AACxC,cAAM,KAAK,MAAM,CAAC;AAAA,MACnB;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,MAAS;AACZ,QAAI,KAAK,KAAK;AACb,aAAO,KAAK,IAAI,IAAI,IAAI;AAAA,IACzB;AAEA,WAAO,KAAK,QAAQ,KAAK,MAAM,QAAQ,IAAI,MAAM,KAAK;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ;AACP,QAAI,KAAK,KAAK;AACb,WAAK,IAAI,MAAM;AAAA,IAChB,WAAW,KAAK,OAAO;AAGtB,WAAK,MAAM,KAAK,QAAW,GAAG,KAAK,SAAS;AAC5C,WAAK,YAAY;AAAA,IAClB;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO;AACN,QAAI,KAAK,KAAK;AACb,aAAO,KAAK,IAAI;AAAA,IACjB;AAEA,WAAO,KAAK;AAAA,EACb;AACD;",
6
6
  "names": []
7
7
  }
@@ -17,30 +17,15 @@ class __Atom__ {
17
17
  }
18
18
  name;
19
19
  current;
20
- /**
21
- * Custom equality function for comparing values, or null to use default equality.
22
- * @internal
23
- */
20
+ /** @internal */
24
21
  isEqual;
25
- /**
26
- * Optional function to compute diffs between old and new values.
27
- * @internal
28
- */
22
+ /** @internal */
29
23
  computeDiff;
30
- /**
31
- * The global epoch when this atom was last changed.
32
- * @internal
33
- */
24
+ /** @internal */
34
25
  lastChangedEpoch = getGlobalEpoch();
35
- /**
36
- * Set of child signals that depend on this atom.
37
- * @internal
38
- */
26
+ /** @internal */
39
27
  children = new ArraySet();
40
- /**
41
- * Optional history buffer for tracking changes over time.
42
- * @internal
43
- */
28
+ /** @internal */
44
29
  historyBuffer;
45
30
  /**
46
31
  * Gets the current value without capturing it as a dependency in the current reactive context.
@@ -84,15 +69,16 @@ class __Atom__ {
84
69
  if (this.isEqual?.(this.current, value) ?? equals(this.current, value)) {
85
70
  return this.current;
86
71
  }
72
+ let historyDiff;
73
+ if (this.historyBuffer) {
74
+ historyDiff = diff !== void 0 ? diff : this.computeDiff ? this.computeDiff(this.current, value, this.lastChangedEpoch, getGlobalEpoch() + 1) : RESET_VALUE;
75
+ }
87
76
  advanceGlobalEpoch();
77
+ const epoch = getGlobalEpoch();
88
78
  if (this.historyBuffer) {
89
- this.historyBuffer.pushEntry(
90
- this.lastChangedEpoch,
91
- getGlobalEpoch(),
92
- diff ?? this.computeDiff?.(this.current, value, this.lastChangedEpoch, getGlobalEpoch()) ?? RESET_VALUE
93
- );
79
+ this.historyBuffer.pushEntry(this.lastChangedEpoch, epoch, historyDiff);
94
80
  }
95
- this.lastChangedEpoch = getGlobalEpoch();
81
+ this.lastChangedEpoch = epoch;
96
82
  const oldValue = this.current;
97
83
  this.current = value;
98
84
  atomDidChange(this, oldValue);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/Atom.ts"],
4
- "sourcesContent": ["import { ArraySet } from './ArraySet'\nimport { maybeCaptureParent } from './capture'\nimport { EMPTY_ARRAY, equals, singleton } from './helpers'\nimport { HistoryBuffer } from './HistoryBuffer'\nimport { advanceGlobalEpoch, atomDidChange, getGlobalEpoch } from './transactions'\nimport { Child, ComputeDiff, RESET_VALUE, Signal } from './types'\n\n/**\n * The options to configure an atom, passed into the {@link atom} function.\n * @public\n */\nexport interface AtomOptions<Value, Diff> {\n\t/**\n\t * The maximum number of diffs to keep in the history buffer.\n\t *\n\t * If you don't need to compute diffs, or if you will supply diffs manually via {@link Atom.set}, you can leave this as `undefined` and no history buffer will be created.\n\t *\n\t * If you expect the value to be part of an active effect subscription all the time, and to not change multiple times inside of a single transaction, you can set this to a relatively low number (e.g. 10).\n\t *\n\t * Otherwise, set this to a higher number based on your usage pattern and memory constraints.\n\t *\n\t */\n\thistoryLength?: number\n\t/**\n\t * A method used to compute a diff between the atom's old and new values. If provided, it will not be used unless you also specify {@link AtomOptions.historyLength}.\n\t */\n\tcomputeDiff?: ComputeDiff<Value, Diff>\n\t/**\n\t * If provided, this will be used to compare the old and new values of the atom to determine if the value has changed.\n\t * By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain.\n\t * @param a - The old value\n\t * @param b - The new value\n\t * @returns True if the values are equal, false otherwise.\n\t */\n\tisEqual?(a: any, b: any): boolean\n}\n\n/**\n * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.\n *\n * Atoms are created using the {@link atom} function.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * print(name.get()) // 'John'\n * ```\n *\n * @public\n */\nexport interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {\n\t/**\n\t * Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.\n\t *\n\t * @param value - The new value to set.\n\t * @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}.\n\t */\n\tset(value: Value, diff?: Diff): Value\n\t/**\n\t * Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.\n\t *\n\t * @param updater - A function that takes the current value and returns the new value.\n\t */\n\tupdate(updater: (value: Value) => Value): Value\n}\n\n/**\n * Internal implementation of the Atom interface. This class should not be used directly - use the {@link atom} function instead.\n *\n * @internal\n */\nclass __Atom__<Value, Diff = unknown> implements Atom<Value, Diff> {\n\tconstructor(\n\t\tpublic readonly name: string,\n\t\tprivate current: Value,\n\t\toptions?: AtomOptions<Value, Diff>\n\t) {\n\t\tthis.isEqual = options?.isEqual ?? null\n\n\t\tif (!options) return\n\n\t\tif (options.historyLength) {\n\t\t\tthis.historyBuffer = new HistoryBuffer(options.historyLength)\n\t\t}\n\n\t\tthis.computeDiff = options.computeDiff\n\t}\n\n\t/**\n\t * Custom equality function for comparing values, or null to use default equality.\n\t * @internal\n\t */\n\treadonly isEqual: null | ((a: any, b: any) => boolean)\n\n\t/**\n\t * Optional function to compute diffs between old and new values.\n\t * @internal\n\t */\n\tcomputeDiff?: ComputeDiff<Value, Diff>\n\n\t/**\n\t * The global epoch when this atom was last changed.\n\t * @internal\n\t */\n\tlastChangedEpoch = getGlobalEpoch()\n\n\t/**\n\t * Set of child signals that depend on this atom.\n\t * @internal\n\t */\n\tchildren = new ArraySet<Child>()\n\n\t/**\n\t * Optional history buffer for tracking changes over time.\n\t * @internal\n\t */\n\thistoryBuffer?: HistoryBuffer<Diff>\n\n\t/**\n\t * Gets the current value without capturing it as a dependency in the current reactive context.\n\t * This is unsafe because it breaks the reactivity chain - use with caution.\n\t *\n\t * @param _ignoreErrors - Unused parameter for API compatibility\n\t * @returns The current value\n\t * @internal\n\t */\n\t__unsafe__getWithoutCapture(_ignoreErrors?: boolean): Value {\n\t\treturn this.current\n\t}\n\n\t/**\n\t * Gets the current value of this atom. When called within a computed signal or reaction,\n\t * this atom will be automatically captured as a dependency.\n\t *\n\t * @returns The current value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 5)\n\t * console.log(count.get()) // 5\n\t * ```\n\t */\n\tget() {\n\t\tmaybeCaptureParent(this)\n\t\treturn this.current\n\t}\n\n\t/**\n\t * Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.\n\t *\n\t * @param value - The new value to set\n\t * @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}\n\t * @returns The new value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 0)\n\t * count.set(5) // count.get() is now 5\n\t * ```\n\t */\n\tset(value: Value, diff?: Diff): Value {\n\t\t// If the value has not changed, do nothing.\n\t\tif (this.isEqual?.(this.current, value) ?? equals(this.current, value)) {\n\t\t\treturn this.current\n\t\t}\n\n\t\t// Tick forward the global epoch\n\t\tadvanceGlobalEpoch()\n\n\t\t// Add the diff to the history buffer.\n\t\tif (this.historyBuffer) {\n\t\t\tthis.historyBuffer.pushEntry(\n\t\t\t\tthis.lastChangedEpoch,\n\t\t\t\tgetGlobalEpoch(),\n\t\t\t\tdiff ??\n\t\t\t\t\tthis.computeDiff?.(this.current, value, this.lastChangedEpoch, getGlobalEpoch()) ??\n\t\t\t\t\tRESET_VALUE\n\t\t\t)\n\t\t}\n\n\t\t// Update the atom's record of the epoch when last changed.\n\t\tthis.lastChangedEpoch = getGlobalEpoch()\n\n\t\tconst oldValue = this.current\n\t\tthis.current = value\n\n\t\t// Notify all children that this atom has changed.\n\t\tatomDidChange(this as any, oldValue)\n\n\t\treturn value\n\t}\n\n\t/**\n\t * Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.\n\t *\n\t * @param updater - A function that takes the current value and returns the new value\n\t * @returns The new value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 5)\n\t * count.update(n => n + 1) // count.get() is now 6\n\t * ```\n\t */\n\tupdate(updater: (value: Value) => Value): Value {\n\t\treturn this.set(updater(this.current))\n\t}\n\n\t/**\n\t * Gets all the diffs that have occurred since the given epoch. When called within a computed\n\t * signal or reaction, this atom will be automatically captured as a dependency.\n\t *\n\t * @param epoch - The epoch to get changes since\n\t * @returns An array of diffs, or RESET_VALUE if history is insufficient\n\t * @internal\n\t */\n\tgetDiffSince(epoch: number): RESET_VALUE | Diff[] {\n\t\tmaybeCaptureParent(this)\n\n\t\t// If no changes have occurred since the given epoch, return an empty array.\n\t\tif (epoch >= this.lastChangedEpoch) {\n\t\t\treturn EMPTY_ARRAY\n\t\t}\n\n\t\treturn this.historyBuffer?.getChangesSince(epoch) ?? RESET_VALUE\n\t}\n}\n\n/**\n * Singleton reference to the Atom constructor. Used internally to create atom instances.\n * @internal\n */\nexport const _Atom = singleton('Atom', () => __Atom__)\n\n/**\n * Type alias for instances of the internal Atom class.\n * @internal\n */\nexport type _Atom = InstanceType<typeof _Atom>\n\n/**\n * Creates a new {@link Atom}.\n *\n * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * name.get() // 'John'\n *\n * name.set('Jane')\n *\n * name.get() // 'Jane'\n * ```\n *\n * @public\n */\nexport function atom<Value, Diff = unknown>(\n\t/**\n\t * A name for the signal. This is used for debugging and profiling purposes, it does not need to be unique.\n\t */\n\tname: string,\n\t/**\n\t * The initial value of the signal.\n\t */\n\tinitialValue: Value,\n\t/**\n\t * The options to configure the atom. See {@link AtomOptions}.\n\t */\n\toptions?: AtomOptions<Value, Diff>\n): Atom<Value, Diff> {\n\treturn new _Atom(name, initialValue, options)\n}\n\n/**\n * Returns true if the given value is an {@link Atom}.\n *\n * @param value - The value to check\n * @returns True if the value is an Atom, false otherwise\n * @example\n * ```ts\n * const myAtom = atom('test', 42)\n * const notAtom = 'hello'\n *\n * console.log(isAtom(myAtom)) // true\n * console.log(isAtom(notAtom)) // false\n * ```\n * @public\n */\nexport function isAtom(value: unknown): value is Atom<unknown> {\n\treturn value instanceof _Atom\n}\n"],
5
- "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,0BAA0B;AACnC,SAAS,aAAa,QAAQ,iBAAiB;AAC/C,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB,eAAe,sBAAsB;AAClE,SAA6B,mBAA2B;AAmExD,MAAM,SAA6D;AAAA,EAClE,YACiB,MACR,SACR,SACC;AAHe;AACR;AAGR,SAAK,UAAU,SAAS,WAAW;AAEnC,QAAI,CAAC,QAAS;AAEd,QAAI,QAAQ,eAAe;AAC1B,WAAK,gBAAgB,IAAI,cAAc,QAAQ,aAAa;AAAA,IAC7D;AAEA,SAAK,cAAc,QAAQ;AAAA,EAC5B;AAAA,EAbiB;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA,EAMlC,WAAW,IAAI,SAAgB;AAAA;AAAA;AAAA;AAAA;AAAA,EAM/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,4BAA4B,eAAgC;AAC3D,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM;AACL,uBAAmB,IAAI;AACvB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,IAAI,OAAc,MAAoB;AAErC,QAAI,KAAK,UAAU,KAAK,SAAS,KAAK,KAAK,OAAO,KAAK,SAAS,KAAK,GAAG;AACvE,aAAO,KAAK;AAAA,IACb;AAGA,uBAAmB;AAGnB,QAAI,KAAK,eAAe;AACvB,WAAK,cAAc;AAAA,QAClB,KAAK;AAAA,QACL,eAAe;AAAA,QACf,QACC,KAAK,cAAc,KAAK,SAAS,OAAO,KAAK,kBAAkB,eAAe,CAAC,KAC/E;AAAA,MACF;AAAA,IACD;AAGA,SAAK,mBAAmB,eAAe;AAEvC,UAAM,WAAW,KAAK;AACtB,SAAK,UAAU;AAGf,kBAAc,MAAa,QAAQ;AAEnC,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,OAAO,SAAyC;AAC/C,WAAO,KAAK,IAAI,QAAQ,KAAK,OAAO,CAAC;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,aAAa,OAAqC;AACjD,uBAAmB,IAAI;AAGvB,QAAI,SAAS,KAAK,kBAAkB;AACnC,aAAO;AAAA,IACR;AAEA,WAAO,KAAK,eAAe,gBAAgB,KAAK,KAAK;AAAA,EACtD;AACD;AAMO,MAAM,QAAQ,UAAU,QAAQ,MAAM,QAAQ;AA0B9C,SAAS,KAIf,MAIA,cAIA,SACoB;AACpB,SAAO,IAAI,MAAM,MAAM,cAAc,OAAO;AAC7C;AAiBO,SAAS,OAAO,OAAwC;AAC9D,SAAO,iBAAiB;AACzB;",
4
+ "sourcesContent": ["import { ArraySet } from './ArraySet'\nimport { maybeCaptureParent } from './capture'\nimport { EMPTY_ARRAY, equals, singleton } from './helpers'\nimport { HistoryBuffer } from './HistoryBuffer'\nimport { advanceGlobalEpoch, atomDidChange, getGlobalEpoch } from './transactions'\nimport { Child, ComputeDiff, RESET_VALUE, Signal } from './types'\n\n/**\n * The options to configure an atom, passed into the {@link atom} function.\n * @public\n */\nexport interface AtomOptions<Value, Diff> {\n\t/**\n\t * The maximum number of diffs to keep in the history buffer.\n\t *\n\t * If you don't need diffs, leave this as `undefined` and no history buffer will be created. Diffs passed to {@link Atom.set} or produced by {@link AtomOptions.computeDiff} are only recorded when this is set.\n\t *\n\t * If you expect the value to be part of an active effect subscription all the time, and to not change multiple times inside of a single transaction, you can set this to a relatively low number (e.g. 10).\n\t *\n\t * Otherwise, set this to a higher number based on your usage pattern and memory constraints.\n\t *\n\t */\n\thistoryLength?: number\n\t/**\n\t * A method used to compute a diff between the atom's old and new values. If provided, it will not be used unless you also specify {@link AtomOptions.historyLength}.\n\t */\n\tcomputeDiff?: ComputeDiff<Value, Diff>\n\t/**\n\t * If provided, this will be used to compare the old and new values of the atom to determine if the value has changed.\n\t * By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain.\n\t * @param a - The old value\n\t * @param b - The new value\n\t * @returns True if the values are equal, false otherwise.\n\t */\n\tisEqual?(a: any, b: any): boolean\n}\n\n/**\n * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.\n *\n * Atoms are created using the {@link atom} function.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * print(name.get()) // 'John'\n * ```\n *\n * @public\n */\nexport interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {\n\t/**\n\t * Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.\n\t *\n\t * @param value - The new value to set.\n\t * @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}.\n\t */\n\tset(value: Value, diff?: Diff): Value\n\t/**\n\t * Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.\n\t *\n\t * @param updater - A function that takes the current value and returns the new value.\n\t */\n\tupdate(updater: (value: Value) => Value): Value\n}\n\n/**\n * Internal implementation of the Atom interface. This class should not be used directly - use the {@link atom} function instead.\n *\n * @internal\n */\nclass __Atom__<Value, Diff = unknown> implements Atom<Value, Diff> {\n\tconstructor(\n\t\tpublic readonly name: string,\n\t\tprivate current: Value,\n\t\toptions?: AtomOptions<Value, Diff>\n\t) {\n\t\tthis.isEqual = options?.isEqual ?? null\n\n\t\tif (!options) return\n\n\t\tif (options.historyLength) {\n\t\t\tthis.historyBuffer = new HistoryBuffer(options.historyLength)\n\t\t}\n\n\t\tthis.computeDiff = options.computeDiff\n\t}\n\n\t/** @internal */\n\treadonly isEqual: null | ((a: any, b: any) => boolean)\n\n\t/** @internal */\n\tcomputeDiff?: ComputeDiff<Value, Diff>\n\n\t/** @internal */\n\tlastChangedEpoch = getGlobalEpoch()\n\n\t/** @internal */\n\tchildren = new ArraySet<Child>()\n\n\t/** @internal */\n\thistoryBuffer?: HistoryBuffer<Diff>\n\n\t/**\n\t * Gets the current value without capturing it as a dependency in the current reactive context.\n\t * This is unsafe because it breaks the reactivity chain - use with caution.\n\t *\n\t * @param _ignoreErrors - Unused parameter for API compatibility\n\t * @returns The current value\n\t * @internal\n\t */\n\t__unsafe__getWithoutCapture(_ignoreErrors?: boolean): Value {\n\t\treturn this.current\n\t}\n\n\t/**\n\t * Gets the current value of this atom. When called within a computed signal or reaction,\n\t * this atom will be automatically captured as a dependency.\n\t *\n\t * @returns The current value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 5)\n\t * console.log(count.get()) // 5\n\t * ```\n\t */\n\tget() {\n\t\tmaybeCaptureParent(this)\n\t\treturn this.current\n\t}\n\n\t/**\n\t * Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.\n\t *\n\t * @param value - The new value to set\n\t * @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}\n\t * @returns The new value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 0)\n\t * count.set(5) // count.get() is now 5\n\t * ```\n\t */\n\tset(value: Value, diff?: Diff): Value {\n\t\t// If the value has not changed, do nothing.\n\t\tif (this.isEqual?.(this.current, value) ?? equals(this.current, value)) {\n\t\t\treturn this.current\n\t\t}\n\n\t\t// `computeDiff` is user code: run it before ticking the epoch, so that any signal it reads is\n\t\t// checked against the epoch this write has not yet happened in. Reading a dependent computed\n\t\t// after the tick would stamp it as checked at the new epoch while the atom was still being\n\t\t// written, and it would then never see the change. Only `undefined` means \"no diff\n\t\t// supplied\"; `null` can be a legitimate diff.\n\t\tlet historyDiff: Diff | RESET_VALUE | undefined\n\t\tif (this.historyBuffer) {\n\t\t\thistoryDiff =\n\t\t\t\tdiff !== undefined\n\t\t\t\t\t? diff\n\t\t\t\t\t: this.computeDiff\n\t\t\t\t\t\t? this.computeDiff(this.current, value, this.lastChangedEpoch, getGlobalEpoch() + 1)\n\t\t\t\t\t\t: RESET_VALUE\n\t\t}\n\n\t\t// Tick forward the global epoch. This write belongs to that one epoch, so read it once and\n\t\t// use it everywhere below \u2014 otherwise a `computeDiff` that touches other atoms could leave\n\t\t// the history entry and `lastChangedEpoch` disagreeing.\n\t\tadvanceGlobalEpoch()\n\t\tconst epoch = getGlobalEpoch()\n\n\t\t// Add the diff to the history buffer.\n\t\tif (this.historyBuffer) {\n\t\t\tthis.historyBuffer.pushEntry(this.lastChangedEpoch, epoch, historyDiff)\n\t\t}\n\n\t\t// Update the atom's record of the epoch when last changed.\n\t\tthis.lastChangedEpoch = epoch\n\n\t\tconst oldValue = this.current\n\t\tthis.current = value\n\n\t\t// Notify all children that this atom has changed.\n\t\tatomDidChange(this as any, oldValue)\n\n\t\treturn value\n\t}\n\n\t/**\n\t * Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.\n\t *\n\t * @param updater - A function that takes the current value and returns the new value\n\t * @returns The new value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 5)\n\t * count.update(n => n + 1) // count.get() is now 6\n\t * ```\n\t */\n\tupdate(updater: (value: Value) => Value): Value {\n\t\treturn this.set(updater(this.current))\n\t}\n\n\t/**\n\t * Gets all the diffs that have occurred since the given epoch. When called within a computed\n\t * signal or reaction, this atom will be automatically captured as a dependency.\n\t *\n\t * @param epoch - The epoch to get changes since\n\t * @returns An array of diffs, or RESET_VALUE if history is insufficient\n\t * @internal\n\t */\n\tgetDiffSince(epoch: number): RESET_VALUE | Diff[] {\n\t\tmaybeCaptureParent(this)\n\n\t\tif (epoch >= this.lastChangedEpoch) {\n\t\t\treturn EMPTY_ARRAY\n\t\t}\n\n\t\treturn this.historyBuffer?.getChangesSince(epoch) ?? RESET_VALUE\n\t}\n}\n\n/**\n * Singleton reference to the Atom constructor. Used internally to create atom instances.\n * @internal\n */\nexport const _Atom = singleton('Atom', () => __Atom__)\n\n/**\n * Type alias for instances of the internal Atom class.\n * @internal\n */\nexport type _Atom = InstanceType<typeof _Atom>\n\n/**\n * Creates a new {@link Atom}.\n *\n * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * name.get() // 'John'\n *\n * name.set('Jane')\n *\n * name.get() // 'Jane'\n * ```\n *\n * @public\n */\nexport function atom<Value, Diff = unknown>(\n\t/**\n\t * A name for the signal. This is used for debugging and profiling purposes, it does not need to be unique.\n\t */\n\tname: string,\n\t/**\n\t * The initial value of the signal.\n\t */\n\tinitialValue: Value,\n\t/**\n\t * The options to configure the atom. See {@link AtomOptions}.\n\t */\n\toptions?: AtomOptions<Value, Diff>\n): Atom<Value, Diff> {\n\treturn new _Atom(name, initialValue, options)\n}\n\n/**\n * Returns true if the given value is an {@link Atom}.\n *\n * @param value - The value to check\n * @returns True if the value is an Atom, false otherwise\n * @example\n * ```ts\n * const myAtom = atom('test', 42)\n * const notAtom = 'hello'\n *\n * console.log(isAtom(myAtom)) // true\n * console.log(isAtom(notAtom)) // false\n * ```\n * @public\n */\nexport function isAtom(value: unknown): value is Atom<unknown> {\n\treturn value instanceof _Atom\n}\n"],
5
+ "mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,0BAA0B;AACnC,SAAS,aAAa,QAAQ,iBAAiB;AAC/C,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB,eAAe,sBAAsB;AAClE,SAA6B,mBAA2B;AAmExD,MAAM,SAA6D;AAAA,EAClE,YACiB,MACR,SACR,SACC;AAHe;AACR;AAGR,SAAK,UAAU,SAAS,WAAW;AAEnC,QAAI,CAAC,QAAS;AAEd,QAAI,QAAQ,eAAe;AAC1B,WAAK,gBAAgB,IAAI,cAAc,QAAQ,aAAa;AAAA,IAC7D;AAEA,SAAK,cAAc,QAAQ;AAAA,EAC5B;AAAA,EAbiB;AAAA,EACR;AAAA;AAAA,EAeA;AAAA;AAAA,EAGT;AAAA;AAAA,EAGA,mBAAmB,eAAe;AAAA;AAAA,EAGlC,WAAW,IAAI,SAAgB;AAAA;AAAA,EAG/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,4BAA4B,eAAgC;AAC3D,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM;AACL,uBAAmB,IAAI;AACvB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,IAAI,OAAc,MAAoB;AAErC,QAAI,KAAK,UAAU,KAAK,SAAS,KAAK,KAAK,OAAO,KAAK,SAAS,KAAK,GAAG;AACvE,aAAO,KAAK;AAAA,IACb;AAOA,QAAI;AACJ,QAAI,KAAK,eAAe;AACvB,oBACC,SAAS,SACN,OACA,KAAK,cACJ,KAAK,YAAY,KAAK,SAAS,OAAO,KAAK,kBAAkB,eAAe,IAAI,CAAC,IACjF;AAAA,IACN;AAKA,uBAAmB;AACnB,UAAM,QAAQ,eAAe;AAG7B,QAAI,KAAK,eAAe;AACvB,WAAK,cAAc,UAAU,KAAK,kBAAkB,OAAO,WAAW;AAAA,IACvE;AAGA,SAAK,mBAAmB;AAExB,UAAM,WAAW,KAAK;AACtB,SAAK,UAAU;AAGf,kBAAc,MAAa,QAAQ;AAEnC,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,OAAO,SAAyC;AAC/C,WAAO,KAAK,IAAI,QAAQ,KAAK,OAAO,CAAC;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,aAAa,OAAqC;AACjD,uBAAmB,IAAI;AAEvB,QAAI,SAAS,KAAK,kBAAkB;AACnC,aAAO;AAAA,IACR;AAEA,WAAO,KAAK,eAAe,gBAAgB,KAAK,KAAK;AAAA,EACtD;AACD;AAMO,MAAM,QAAQ,UAAU,QAAQ,MAAM,QAAQ;AA0B9C,SAAS,KAIf,MAIA,cAIA,SACoB;AACpB,SAAO,IAAI,MAAM,MAAM,cAAc,OAAO;AAC7C;AAiBO,SAAS,OAAO,OAAwC;AAC9D,SAAO,iBAAiB;AACzB;",
6
6
  "names": []
7
7
  }
@@ -80,27 +80,29 @@ class __UNSAFE__Computed {
80
80
  const result = this.derive(this.state, this.lastCheckedEpoch);
81
81
  const newState = result instanceof WithDiff ? result.value : result;
82
82
  const isUninitialized2 = this.state === UNINITIALIZED;
83
+ const epoch = getGlobalEpoch();
83
84
  if (isUninitialized2 || !this.isEqual(this.state, newState)) {
84
85
  if (this.historyBuffer && !isUninitialized2) {
85
86
  const diff = result instanceof WithDiff ? result.diff : void 0;
86
87
  this.historyBuffer.pushEntry(
87
88
  this.lastChangedEpoch,
88
- getGlobalEpoch(),
89
- diff ?? this.computeDiff?.(this.state, newState, this.lastCheckedEpoch, getGlobalEpoch()) ?? RESET_VALUE
89
+ epoch,
90
+ diff !== void 0 ? diff : this.computeDiff ? this.computeDiff(this.state, newState, this.lastCheckedEpoch, epoch) : RESET_VALUE
90
91
  );
91
92
  }
92
- this.lastChangedEpoch = getGlobalEpoch();
93
+ this.lastChangedEpoch = epoch;
93
94
  this.state = newState;
94
95
  }
95
96
  this.error = null;
96
- this.lastCheckedEpoch = getGlobalEpoch();
97
+ this.lastCheckedEpoch = epoch;
97
98
  return this.state;
98
99
  } catch (e) {
99
- if (this.state !== UNINITIALIZED) {
100
+ const epoch = getGlobalEpoch();
101
+ if (this.error === null) {
100
102
  this.state = UNINITIALIZED;
101
- this.lastChangedEpoch = getGlobalEpoch();
103
+ this.lastChangedEpoch = epoch;
102
104
  }
103
- this.lastCheckedEpoch = getGlobalEpoch();
105
+ this.lastCheckedEpoch = epoch;
104
106
  if (this.historyBuffer) {
105
107
  this.historyBuffer.clear();
106
108
  }
@@ -128,13 +130,13 @@ class __UNSAFE__Computed {
128
130
  }
129
131
  }
130
132
  const _Computed = singleton("Computed", () => __UNSAFE__Computed);
131
- function computedMethodLegacyDecorator(options = {}, _target, key, descriptor) {
132
- const originalMethod = descriptor.value;
133
- const derivationKey = /* @__PURE__ */ Symbol.for("__@tldraw/state__computed__" + key);
134
- descriptor.value = function() {
133
+ const derivationKeyKey = "@@__computedDerivationKey__@@";
134
+ function makeComputedWrapper(name, compute, options) {
135
+ const derivationKey = /* @__PURE__ */ Symbol("__@tldraw/state__computed__" + name);
136
+ const wrapper = function() {
135
137
  let d = this[derivationKey];
136
138
  if (!d) {
137
- d = new _Computed(key, originalMethod.bind(this), options);
139
+ d = new _Computed(name, compute.bind(this), options);
138
140
  Object.defineProperty(this, derivationKey, {
139
141
  enumerable: false,
140
142
  configurable: false,
@@ -144,75 +146,45 @@ function computedMethodLegacyDecorator(options = {}, _target, key, descriptor) {
144
146
  }
145
147
  return d.get();
146
148
  };
147
- descriptor.value[isComputedMethodKey] = true;
148
- return descriptor;
149
- }
150
- function computedGetterLegacyDecorator(options = {}, _target, key, descriptor) {
151
- const originalMethod = descriptor.get;
152
- const derivationKey = /* @__PURE__ */ Symbol.for("__@tldraw/state__computed__" + key);
153
- descriptor.get = function() {
154
- let d = this[derivationKey];
155
- if (!d) {
156
- d = new _Computed(key, originalMethod.bind(this), options);
157
- Object.defineProperty(this, derivationKey, {
158
- enumerable: false,
159
- configurable: false,
160
- writable: false,
161
- value: d
162
- });
163
- }
164
- return d.get();
165
- };
166
- return descriptor;
167
- }
168
- function computedMethodTc39Decorator(options, compute, context) {
169
- assert(context.kind === "method", "@computed can only be used on methods");
170
- const derivationKey = /* @__PURE__ */ Symbol.for("__@tldraw/state__computed__" + String(context.name));
171
- const fn = function() {
172
- let d = this[derivationKey];
173
- if (!d) {
174
- d = new _Computed(String(context.name), compute.bind(this), options);
175
- Object.defineProperty(this, derivationKey, {
176
- enumerable: false,
177
- configurable: false,
178
- writable: false,
179
- value: d
180
- });
181
- }
182
- return d.get();
183
- };
184
- fn[isComputedMethodKey] = true;
185
- return fn;
149
+ wrapper[derivationKeyKey] = derivationKey;
150
+ return wrapper;
186
151
  }
187
152
  function computedDecorator(options = {}, args) {
188
153
  if (args.length === 2) {
189
154
  const [originalMethod, context] = args;
190
- return computedMethodTc39Decorator(options, originalMethod, context);
155
+ assert(context.kind === "method", "@computed can only be used on methods");
156
+ return makeComputedWrapper(String(context.name), originalMethod, options);
191
157
  } else {
192
158
  const [_target, key, descriptor] = args;
193
159
  if (descriptor.get) {
194
160
  logComputedGetterWarning();
195
- return computedGetterLegacyDecorator(options, _target, key, descriptor);
161
+ descriptor.get = makeComputedWrapper(key, descriptor.get, options);
196
162
  } else {
197
- return computedMethodLegacyDecorator(options, _target, key, descriptor);
163
+ descriptor.value = makeComputedWrapper(key, descriptor.value, options);
198
164
  }
165
+ return descriptor;
199
166
  }
200
167
  }
201
- const isComputedMethodKey = "@@__isComputedMethod__@@";
202
168
  function getComputedInstance(obj, propertyName) {
203
- const key = /* @__PURE__ */ Symbol.for("__@tldraw/state__computed__" + propertyName.toString());
204
- let inst = obj[key];
205
- if (!inst) {
206
- const val = obj[propertyName];
207
- if (typeof val === "function" && val[isComputedMethodKey]) {
208
- val.call(obj);
169
+ for (let proto = obj; proto; proto = Object.getPrototypeOf(proto)) {
170
+ const descriptor = Object.getOwnPropertyDescriptor(proto, propertyName);
171
+ const wrapper = descriptor?.get ?? descriptor?.value;
172
+ const key = wrapper?.[derivationKeyKey];
173
+ if (!key) continue;
174
+ let inst = obj[key];
175
+ if (!inst) {
176
+ try {
177
+ wrapper.call(obj);
178
+ } catch {
179
+ }
180
+ inst = obj[key];
209
181
  }
210
- inst = obj[key];
182
+ return inst;
211
183
  }
212
- return inst;
184
+ return void 0;
213
185
  }
214
186
  function computed() {
215
- if (arguments.length === 1) {
187
+ if (arguments.length <= 1) {
216
188
  const options = arguments[0];
217
189
  return (...args) => computedDecorator(options, args);
218
190
  } else if (typeof arguments[0] === "string") {