@ibodr/state 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1169 -0
- package/dist/index.mjs +1311 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
import { registerDrawLibraryVersion, getFromLocalStorage, deleteFromLocalStorage, setInLocalStorage, assert } from '@ibodr/utils';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
|
|
5
|
+
// src/lib/helpers.ts
|
|
6
|
+
function isChild(x) {
|
|
7
|
+
return x && typeof x === "object" && "parents" in x;
|
|
8
|
+
}
|
|
9
|
+
function haveParentsChanged(child) {
|
|
10
|
+
for (let i = 0, n = child.parents.length; i < n; i++) {
|
|
11
|
+
child.parents[i].__unsafe__getWithoutCapture(true);
|
|
12
|
+
if (child.parents[i].lastChangedEpoch !== child.parentEpochs[i]) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
function detach(parent, child) {
|
|
19
|
+
if (!parent.children.remove(child)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (parent.children.isEmpty && isChild(parent)) {
|
|
23
|
+
for (let i = 0, n = parent.parents.length; i < n; i++) {
|
|
24
|
+
detach(parent.parents[i], parent);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function attach(parent, child) {
|
|
29
|
+
if (!parent.children.add(child)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (isChild(parent)) {
|
|
33
|
+
for (let i = 0, n = parent.parents.length; i < n; i++) {
|
|
34
|
+
attach(parent.parents[i], parent);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function equals(a, b) {
|
|
39
|
+
const shallowEquals = a === b || Object.is(a, b) || Boolean(a && b && typeof a.equals === "function" && a.equals(b));
|
|
40
|
+
return shallowEquals;
|
|
41
|
+
}
|
|
42
|
+
function singleton(key, init) {
|
|
43
|
+
const symbol = /* @__PURE__ */ Symbol.for(`com.draw.state/${key}`);
|
|
44
|
+
const global = globalThis;
|
|
45
|
+
global[symbol] ??= init();
|
|
46
|
+
return global[symbol];
|
|
47
|
+
}
|
|
48
|
+
var EMPTY_ARRAY = singleton("empty_array", () => Object.freeze([]));
|
|
49
|
+
|
|
50
|
+
// src/lib/ArraySet.ts
|
|
51
|
+
var ARRAY_SIZE_THRESHOLD = 8;
|
|
52
|
+
var ArraySet = class {
|
|
53
|
+
arraySize = 0;
|
|
54
|
+
array = Array(ARRAY_SIZE_THRESHOLD);
|
|
55
|
+
set = null;
|
|
56
|
+
/**
|
|
57
|
+
* Get whether this ArraySet has any elements.
|
|
58
|
+
*
|
|
59
|
+
* @returns True if this ArraySet has any elements, false otherwise.
|
|
60
|
+
*/
|
|
61
|
+
// eslint-disable-next-line tldraw/no-setter-getter
|
|
62
|
+
get isEmpty() {
|
|
63
|
+
if (this.array) {
|
|
64
|
+
return this.arraySize === 0;
|
|
65
|
+
}
|
|
66
|
+
if (this.set) {
|
|
67
|
+
return this.set.size === 0;
|
|
68
|
+
}
|
|
69
|
+
throw new Error("no set or array");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Add an element to the ArraySet if it is not already present.
|
|
73
|
+
*
|
|
74
|
+
* @param elem - The element to add to the set
|
|
75
|
+
* @returns `true` if the element was added, `false` if it was already present
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const arraySet = new ArraySet<string>()
|
|
79
|
+
*
|
|
80
|
+
* console.log(arraySet.add('hello')) // true
|
|
81
|
+
* console.log(arraySet.add('hello')) // false (already exists)
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
add(elem) {
|
|
85
|
+
if (this.array) {
|
|
86
|
+
const idx = this.array.indexOf(elem);
|
|
87
|
+
if (idx !== -1) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (this.arraySize < ARRAY_SIZE_THRESHOLD) {
|
|
91
|
+
this.array[this.arraySize] = elem;
|
|
92
|
+
this.arraySize++;
|
|
93
|
+
return true;
|
|
94
|
+
} else {
|
|
95
|
+
this.set = new Set(this.array);
|
|
96
|
+
this.array = null;
|
|
97
|
+
this.set.add(elem);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (this.set) {
|
|
102
|
+
if (this.set.has(elem)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
this.set.add(elem);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
throw new Error("no set or array");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Remove an element from the ArraySet if it is present.
|
|
112
|
+
*
|
|
113
|
+
* @param elem - The element to remove from the set
|
|
114
|
+
* @returns `true` if the element was removed, `false` if it was not present
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const arraySet = new ArraySet<string>()
|
|
118
|
+
* arraySet.add('hello')
|
|
119
|
+
*
|
|
120
|
+
* console.log(arraySet.remove('hello')) // true
|
|
121
|
+
* console.log(arraySet.remove('hello')) // false (not present)
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
remove(elem) {
|
|
125
|
+
if (this.array) {
|
|
126
|
+
const idx = this.array.indexOf(elem);
|
|
127
|
+
if (idx === -1) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
this.array[idx] = void 0;
|
|
131
|
+
this.arraySize--;
|
|
132
|
+
if (idx !== this.arraySize) {
|
|
133
|
+
this.array[idx] = this.array[this.arraySize];
|
|
134
|
+
this.array[this.arraySize] = void 0;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (this.set) {
|
|
139
|
+
if (!this.set.has(elem)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
this.set.delete(elem);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
throw new Error("no set or array");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Execute a callback function for each element in the ArraySet.
|
|
149
|
+
*
|
|
150
|
+
* @param visitor - A function to call for each element in the set
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const arraySet = new ArraySet<string>()
|
|
154
|
+
* arraySet.add('hello')
|
|
155
|
+
* arraySet.add('world')
|
|
156
|
+
*
|
|
157
|
+
* arraySet.visit((item) => {
|
|
158
|
+
* console.log(item) // 'hello', 'world'
|
|
159
|
+
* })
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
visit(visitor) {
|
|
163
|
+
if (this.array) {
|
|
164
|
+
for (let i = 0; i < this.arraySize; i++) {
|
|
165
|
+
const elem = this.array[i];
|
|
166
|
+
if (typeof elem !== "undefined") {
|
|
167
|
+
visitor(elem);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (this.set) {
|
|
173
|
+
this.set.forEach(visitor);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
throw new Error("no set or array");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Make the ArraySet iterable, allowing it to be used in for...of loops and with spread syntax.
|
|
180
|
+
*
|
|
181
|
+
* @returns An iterator that yields each element in the set
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* const arraySet = new ArraySet<number>()
|
|
185
|
+
* arraySet.add(1)
|
|
186
|
+
* arraySet.add(2)
|
|
187
|
+
*
|
|
188
|
+
* for (const item of arraySet) {
|
|
189
|
+
* console.log(item) // 1, 2
|
|
190
|
+
* }
|
|
191
|
+
*
|
|
192
|
+
* const items = [...arraySet] // [1, 2]
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
*[Symbol.iterator]() {
|
|
196
|
+
if (this.array) {
|
|
197
|
+
for (let i = 0; i < this.arraySize; i++) {
|
|
198
|
+
const elem = this.array[i];
|
|
199
|
+
if (typeof elem !== "undefined") {
|
|
200
|
+
yield elem;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else if (this.set) {
|
|
204
|
+
yield* this.set;
|
|
205
|
+
} else {
|
|
206
|
+
throw new Error("no set or array");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Check whether an element is present in the ArraySet.
|
|
211
|
+
*
|
|
212
|
+
* @param elem - The element to check for
|
|
213
|
+
* @returns `true` if the element is present, `false` otherwise
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* const arraySet = new ArraySet<string>()
|
|
217
|
+
* arraySet.add('hello')
|
|
218
|
+
*
|
|
219
|
+
* console.log(arraySet.has('hello')) // true
|
|
220
|
+
* console.log(arraySet.has('world')) // false
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
has(elem) {
|
|
224
|
+
if (this.array) {
|
|
225
|
+
return this.array.indexOf(elem) !== -1;
|
|
226
|
+
} else {
|
|
227
|
+
return this.set.has(elem);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Remove all elements from the ArraySet.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* const arraySet = new ArraySet<string>()
|
|
236
|
+
* arraySet.add('hello')
|
|
237
|
+
* arraySet.add('world')
|
|
238
|
+
*
|
|
239
|
+
* arraySet.clear()
|
|
240
|
+
* console.log(arraySet.size()) // 0
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
clear() {
|
|
244
|
+
if (this.set) {
|
|
245
|
+
this.set.clear();
|
|
246
|
+
} else {
|
|
247
|
+
this.arraySize = 0;
|
|
248
|
+
this.array = [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get the number of elements in the ArraySet.
|
|
253
|
+
*
|
|
254
|
+
* @returns The number of elements in the set
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* const arraySet = new ArraySet<string>()
|
|
258
|
+
* console.log(arraySet.size()) // 0
|
|
259
|
+
*
|
|
260
|
+
* arraySet.add('hello')
|
|
261
|
+
* console.log(arraySet.size()) // 1
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
size() {
|
|
265
|
+
if (this.set) {
|
|
266
|
+
return this.set.size;
|
|
267
|
+
} else {
|
|
268
|
+
return this.arraySize;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/lib/isComputed.ts
|
|
274
|
+
function isComputed(value) {
|
|
275
|
+
return !!(value && value.__isComputed === true);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/lib/capture.ts
|
|
279
|
+
var CaptureStackFrame = class {
|
|
280
|
+
constructor(below, child) {
|
|
281
|
+
this.below = below;
|
|
282
|
+
this.child = child;
|
|
283
|
+
}
|
|
284
|
+
below;
|
|
285
|
+
child;
|
|
286
|
+
offset = 0;
|
|
287
|
+
maybeRemoved;
|
|
288
|
+
};
|
|
289
|
+
var inst = singleton("capture", () => ({ stack: null }));
|
|
290
|
+
function unsafe__withoutCapture(fn) {
|
|
291
|
+
const oldStack = inst.stack;
|
|
292
|
+
inst.stack = null;
|
|
293
|
+
try {
|
|
294
|
+
return fn();
|
|
295
|
+
} finally {
|
|
296
|
+
inst.stack = oldStack;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function startCapturingParents(child) {
|
|
300
|
+
inst.stack = new CaptureStackFrame(inst.stack, child);
|
|
301
|
+
if (child.__debug_ancestor_epochs__) {
|
|
302
|
+
const previousAncestorEpochs = child.__debug_ancestor_epochs__;
|
|
303
|
+
child.__debug_ancestor_epochs__ = null;
|
|
304
|
+
for (const p of child.parents) {
|
|
305
|
+
p.__unsafe__getWithoutCapture(true);
|
|
306
|
+
}
|
|
307
|
+
logChangedAncestors(child, previousAncestorEpochs);
|
|
308
|
+
}
|
|
309
|
+
child.parentSet.clear();
|
|
310
|
+
}
|
|
311
|
+
function stopCapturingParents() {
|
|
312
|
+
const frame = inst.stack;
|
|
313
|
+
inst.stack = frame.below;
|
|
314
|
+
if (frame.offset < frame.child.parents.length) {
|
|
315
|
+
for (let i = frame.offset; i < frame.child.parents.length; i++) {
|
|
316
|
+
const maybeRemovedParent = frame.child.parents[i];
|
|
317
|
+
if (!frame.child.parentSet.has(maybeRemovedParent)) {
|
|
318
|
+
detach(maybeRemovedParent, frame.child);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
frame.child.parents.length = frame.offset;
|
|
322
|
+
frame.child.parentEpochs.length = frame.offset;
|
|
323
|
+
}
|
|
324
|
+
if (frame.maybeRemoved) {
|
|
325
|
+
for (let i = 0; i < frame.maybeRemoved.length; i++) {
|
|
326
|
+
const maybeRemovedParent = frame.maybeRemoved[i];
|
|
327
|
+
if (!frame.child.parentSet.has(maybeRemovedParent)) {
|
|
328
|
+
detach(maybeRemovedParent, frame.child);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (frame.child.__debug_ancestor_epochs__) {
|
|
333
|
+
captureAncestorEpochs(frame.child, frame.child.__debug_ancestor_epochs__);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function maybeCaptureParent(p) {
|
|
337
|
+
if (inst.stack) {
|
|
338
|
+
const wasCapturedAlready = inst.stack.child.parentSet.has(p);
|
|
339
|
+
if (wasCapturedAlready) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
inst.stack.child.parentSet.add(p);
|
|
343
|
+
if (inst.stack.child.isActivelyListening) {
|
|
344
|
+
attach(p, inst.stack.child);
|
|
345
|
+
}
|
|
346
|
+
if (inst.stack.offset < inst.stack.child.parents.length) {
|
|
347
|
+
const maybeRemovedParent = inst.stack.child.parents[inst.stack.offset];
|
|
348
|
+
if (maybeRemovedParent !== p) {
|
|
349
|
+
if (!inst.stack.maybeRemoved) {
|
|
350
|
+
inst.stack.maybeRemoved = [maybeRemovedParent];
|
|
351
|
+
} else {
|
|
352
|
+
inst.stack.maybeRemoved.push(maybeRemovedParent);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
inst.stack.child.parents[inst.stack.offset] = p;
|
|
357
|
+
inst.stack.child.parentEpochs[inst.stack.offset] = p.lastChangedEpoch;
|
|
358
|
+
inst.stack.offset++;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function whyAmIRunning() {
|
|
362
|
+
const child = inst.stack?.child;
|
|
363
|
+
if (!child) {
|
|
364
|
+
throw new Error("whyAmIRunning() called outside of a reactive context");
|
|
365
|
+
}
|
|
366
|
+
child.__debug_ancestor_epochs__ = /* @__PURE__ */ new Map();
|
|
367
|
+
}
|
|
368
|
+
function captureAncestorEpochs(child, ancestorEpochs) {
|
|
369
|
+
for (let i = 0; i < child.parents.length; i++) {
|
|
370
|
+
const parent = child.parents[i];
|
|
371
|
+
const epoch = child.parentEpochs[i];
|
|
372
|
+
ancestorEpochs.set(parent, epoch);
|
|
373
|
+
if (isComputed(parent)) {
|
|
374
|
+
captureAncestorEpochs(parent, ancestorEpochs);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return ancestorEpochs;
|
|
378
|
+
}
|
|
379
|
+
function collectChangedAncestors(child, ancestorEpochs) {
|
|
380
|
+
const changeTree = {};
|
|
381
|
+
for (let i = 0; i < child.parents.length; i++) {
|
|
382
|
+
const parent = child.parents[i];
|
|
383
|
+
if (!ancestorEpochs.has(parent)) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const prevEpoch = ancestorEpochs.get(parent);
|
|
387
|
+
const currentEpoch = parent.lastChangedEpoch;
|
|
388
|
+
if (currentEpoch !== prevEpoch) {
|
|
389
|
+
if (isComputed(parent)) {
|
|
390
|
+
changeTree[parent.name] = collectChangedAncestors(parent, ancestorEpochs);
|
|
391
|
+
} else {
|
|
392
|
+
changeTree[parent.name] = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return changeTree;
|
|
397
|
+
}
|
|
398
|
+
function logChangedAncestors(child, ancestorEpochs) {
|
|
399
|
+
const changeTree = collectChangedAncestors(child, ancestorEpochs);
|
|
400
|
+
if (Object.keys(changeTree).length === 0) {
|
|
401
|
+
console.log(`Effect(${child.name}) was executed manually.`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
let str = isComputed(child) ? `Computed(${child.name}) is recomputing because:` : `Effect(${child.name}) is executing because:`;
|
|
405
|
+
function logParent(tree, indent) {
|
|
406
|
+
const indentStr = "\n" + " ".repeat(indent) + "\u21B3 ";
|
|
407
|
+
for (const [name, val] of Object.entries(tree)) {
|
|
408
|
+
if (val) {
|
|
409
|
+
str += `${indentStr}Computed(${name}) changed`;
|
|
410
|
+
logParent(val, indent + 2);
|
|
411
|
+
} else {
|
|
412
|
+
str += `${indentStr}Atom(${name}) changed`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
logParent(changeTree, 1);
|
|
417
|
+
console.log(str);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/lib/types.ts
|
|
421
|
+
var RESET_VALUE = /* @__PURE__ */ Symbol.for("com.draw.state/RESET_VALUE");
|
|
422
|
+
|
|
423
|
+
// src/lib/HistoryBuffer.ts
|
|
424
|
+
var HistoryBuffer = class {
|
|
425
|
+
/**
|
|
426
|
+
* Creates a new HistoryBuffer with the specified capacity.
|
|
427
|
+
*
|
|
428
|
+
* capacity - Maximum number of diffs to store in the buffer
|
|
429
|
+
* @example
|
|
430
|
+
* ```ts
|
|
431
|
+
* const buffer = new HistoryBuffer<number>(10) // Store up to 10 diffs
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
constructor(capacity) {
|
|
435
|
+
this.capacity = capacity;
|
|
436
|
+
this.buffer = new Array(capacity);
|
|
437
|
+
}
|
|
438
|
+
capacity;
|
|
439
|
+
/**
|
|
440
|
+
* Current write position in the circular buffer.
|
|
441
|
+
* @internal
|
|
442
|
+
*/
|
|
443
|
+
index = 0;
|
|
444
|
+
/**
|
|
445
|
+
* Circular buffer storing range tuples. Uses undefined to represent empty slots.
|
|
446
|
+
* @internal
|
|
447
|
+
*/
|
|
448
|
+
buffer;
|
|
449
|
+
/**
|
|
450
|
+
* Adds a diff entry to the history buffer, representing a change between two epochs.
|
|
451
|
+
*
|
|
452
|
+
* If the diff is undefined, the operation is ignored. If the diff is RESET_VALUE,
|
|
453
|
+
* the entire buffer is cleared to indicate that historical tracking should restart.
|
|
454
|
+
*
|
|
455
|
+
* @param lastComputedEpoch - The epoch when the previous value was computed
|
|
456
|
+
* @param currentEpoch - The epoch when the current value was computed
|
|
457
|
+
* @param diff - The diff representing the change, or RESET_VALUE to clear history
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* const buffer = new HistoryBuffer<string>(5)
|
|
461
|
+
* buffer.pushEntry(0, 1, 'added text')
|
|
462
|
+
* buffer.pushEntry(1, 2, RESET_VALUE) // Clears the buffer
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
pushEntry(lastComputedEpoch, currentEpoch, diff) {
|
|
466
|
+
if (diff === void 0) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (diff === RESET_VALUE) {
|
|
470
|
+
this.clear();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
this.buffer[this.index] = [lastComputedEpoch, currentEpoch, diff];
|
|
474
|
+
this.index = (this.index + 1) % this.capacity;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Clears all entries from the history buffer and resets the write position.
|
|
478
|
+
* This is called when a RESET_VALUE diff is encountered.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* const buffer = new HistoryBuffer<string>(5)
|
|
483
|
+
* buffer.pushEntry(0, 1, 'change')
|
|
484
|
+
* buffer.clear()
|
|
485
|
+
* console.log(buffer.getChangesSince(0)) // RESET_VALUE
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
clear() {
|
|
489
|
+
this.index = 0;
|
|
490
|
+
this.buffer.fill(void 0);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Retrieves all diffs that occurred since the specified epoch.
|
|
494
|
+
*
|
|
495
|
+
* The method searches backwards through the circular buffer to find changes
|
|
496
|
+
* that occurred after the given epoch. If insufficient history is available
|
|
497
|
+
* or the requested epoch is too old, returns RESET_VALUE indicating that
|
|
498
|
+
* a complete state rebuild is required.
|
|
499
|
+
*
|
|
500
|
+
* @param sinceEpoch - The epoch from which to retrieve changes
|
|
501
|
+
* @returns Array of diffs since the epoch, or RESET_VALUE if history is insufficient
|
|
502
|
+
* @example
|
|
503
|
+
* ```ts
|
|
504
|
+
* const buffer = new HistoryBuffer<string>(5)
|
|
505
|
+
* buffer.pushEntry(0, 1, 'first')
|
|
506
|
+
* buffer.pushEntry(1, 2, 'second')
|
|
507
|
+
* const changes = buffer.getChangesSince(0) // ['first', 'second']
|
|
508
|
+
* const recentChanges = buffer.getChangesSince(1) // ['second']
|
|
509
|
+
* const tooOld = buffer.getChangesSince(-100) // RESET_VALUE
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
512
|
+
getChangesSince(sinceEpoch) {
|
|
513
|
+
const { index, capacity, buffer } = this;
|
|
514
|
+
for (let i = 0; i < capacity; i++) {
|
|
515
|
+
const offset = (index - 1 + capacity - i) % capacity;
|
|
516
|
+
const elem = buffer[offset];
|
|
517
|
+
if (!elem) {
|
|
518
|
+
return RESET_VALUE;
|
|
519
|
+
}
|
|
520
|
+
const [fromEpoch, toEpoch] = elem;
|
|
521
|
+
if (i === 0 && sinceEpoch >= toEpoch) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
if (fromEpoch <= sinceEpoch && sinceEpoch < toEpoch) {
|
|
525
|
+
const len = i + 1;
|
|
526
|
+
const result = new Array(len);
|
|
527
|
+
for (let j = 0; j < len; j++) {
|
|
528
|
+
result[j] = buffer[(offset + j) % capacity][2];
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return RESET_VALUE;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/lib/constants.ts
|
|
538
|
+
var GLOBAL_START_EPOCH = -1;
|
|
539
|
+
|
|
540
|
+
// src/lib/transactions.ts
|
|
541
|
+
var Transaction = class {
|
|
542
|
+
constructor(parent, isSync) {
|
|
543
|
+
this.parent = parent;
|
|
544
|
+
this.isSync = isSync;
|
|
545
|
+
}
|
|
546
|
+
parent;
|
|
547
|
+
isSync;
|
|
548
|
+
asyncProcessCount = 0;
|
|
549
|
+
initialAtomValues = /* @__PURE__ */ new Map();
|
|
550
|
+
/**
|
|
551
|
+
* Get whether this transaction is a root (no parents).
|
|
552
|
+
*
|
|
553
|
+
* @public
|
|
554
|
+
*/
|
|
555
|
+
// eslint-disable-next-line tldraw/no-setter-getter
|
|
556
|
+
get isRoot() {
|
|
557
|
+
return this.parent === null;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Commit the transaction's changes.
|
|
561
|
+
*
|
|
562
|
+
* @public
|
|
563
|
+
*/
|
|
564
|
+
commit() {
|
|
565
|
+
if (inst2.globalIsReacting) {
|
|
566
|
+
for (const atom2 of this.initialAtomValues.keys()) {
|
|
567
|
+
traverseAtomForCleanup(atom2);
|
|
568
|
+
}
|
|
569
|
+
} else if (this.isRoot) {
|
|
570
|
+
flushChanges(this.initialAtomValues.keys());
|
|
571
|
+
} else {
|
|
572
|
+
this.initialAtomValues.forEach((value, atom2) => {
|
|
573
|
+
if (!this.parent.initialAtomValues.has(atom2)) {
|
|
574
|
+
this.parent.initialAtomValues.set(atom2, value);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Abort the transaction.
|
|
581
|
+
*
|
|
582
|
+
* @public
|
|
583
|
+
*/
|
|
584
|
+
abort() {
|
|
585
|
+
inst2.globalEpoch++;
|
|
586
|
+
this.initialAtomValues.forEach((value, atom2) => {
|
|
587
|
+
atom2.set(value);
|
|
588
|
+
atom2.historyBuffer?.clear();
|
|
589
|
+
});
|
|
590
|
+
this.commit();
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
var inst2 = singleton("transactions", () => ({
|
|
594
|
+
// The current epoch (global to all atoms).
|
|
595
|
+
globalEpoch: GLOBAL_START_EPOCH + 1,
|
|
596
|
+
// Whether any transaction is reacting.
|
|
597
|
+
globalIsReacting: false,
|
|
598
|
+
currentTransaction: null,
|
|
599
|
+
cleanupReactors: null,
|
|
600
|
+
reactionEpoch: GLOBAL_START_EPOCH + 1
|
|
601
|
+
}));
|
|
602
|
+
function getReactionEpoch() {
|
|
603
|
+
return inst2.reactionEpoch;
|
|
604
|
+
}
|
|
605
|
+
function getGlobalEpoch() {
|
|
606
|
+
return inst2.globalEpoch;
|
|
607
|
+
}
|
|
608
|
+
function getIsReacting() {
|
|
609
|
+
return inst2.globalIsReacting;
|
|
610
|
+
}
|
|
611
|
+
var traverseReactors;
|
|
612
|
+
function traverseChild(child) {
|
|
613
|
+
if (child.lastTraversedEpoch === inst2.globalEpoch) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
child.lastTraversedEpoch = inst2.globalEpoch;
|
|
617
|
+
if ("__isEffectScheduler" in child) {
|
|
618
|
+
traverseReactors.add(child);
|
|
619
|
+
} else {
|
|
620
|
+
child.children.visit(traverseChild);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function traverse(reactors, child) {
|
|
624
|
+
traverseReactors = reactors;
|
|
625
|
+
traverseChild(child);
|
|
626
|
+
}
|
|
627
|
+
function flushChanges(atoms) {
|
|
628
|
+
if (inst2.globalIsReacting) {
|
|
629
|
+
throw new Error("flushChanges cannot be called during a reaction");
|
|
630
|
+
}
|
|
631
|
+
const outerTxn = inst2.currentTransaction;
|
|
632
|
+
try {
|
|
633
|
+
inst2.currentTransaction = null;
|
|
634
|
+
inst2.globalIsReacting = true;
|
|
635
|
+
inst2.reactionEpoch = inst2.globalEpoch;
|
|
636
|
+
const reactors = /* @__PURE__ */ new Set();
|
|
637
|
+
for (const atom2 of atoms) {
|
|
638
|
+
atom2.children.visit((child) => traverse(reactors, child));
|
|
639
|
+
}
|
|
640
|
+
for (const r of reactors) {
|
|
641
|
+
r.maybeScheduleEffect();
|
|
642
|
+
}
|
|
643
|
+
let updateDepth = 0;
|
|
644
|
+
while (inst2.cleanupReactors?.size) {
|
|
645
|
+
if (updateDepth++ > 1e3) {
|
|
646
|
+
throw new Error("Reaction update depth limit exceeded");
|
|
647
|
+
}
|
|
648
|
+
const reactors2 = inst2.cleanupReactors;
|
|
649
|
+
inst2.cleanupReactors = null;
|
|
650
|
+
for (const r of reactors2) {
|
|
651
|
+
r.maybeScheduleEffect();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} finally {
|
|
655
|
+
inst2.cleanupReactors = null;
|
|
656
|
+
inst2.globalIsReacting = false;
|
|
657
|
+
inst2.currentTransaction = outerTxn;
|
|
658
|
+
traverseReactors = void 0;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function atomDidChange(atom2, previousValue) {
|
|
662
|
+
if (inst2.currentTransaction) {
|
|
663
|
+
if (!inst2.currentTransaction.initialAtomValues.has(atom2)) {
|
|
664
|
+
inst2.currentTransaction.initialAtomValues.set(atom2, previousValue);
|
|
665
|
+
}
|
|
666
|
+
} else if (inst2.globalIsReacting) {
|
|
667
|
+
traverseAtomForCleanup(atom2);
|
|
668
|
+
} else {
|
|
669
|
+
flushChanges([atom2]);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function traverseAtomForCleanup(atom2) {
|
|
673
|
+
const rs = inst2.cleanupReactors ??= /* @__PURE__ */ new Set();
|
|
674
|
+
atom2.children.visit((child) => traverse(rs, child));
|
|
675
|
+
}
|
|
676
|
+
function advanceGlobalEpoch() {
|
|
677
|
+
inst2.globalEpoch++;
|
|
678
|
+
}
|
|
679
|
+
function transaction(fn) {
|
|
680
|
+
const txn = new Transaction(inst2.currentTransaction, true);
|
|
681
|
+
inst2.currentTransaction = txn;
|
|
682
|
+
try {
|
|
683
|
+
let result = void 0;
|
|
684
|
+
let rollback = false;
|
|
685
|
+
try {
|
|
686
|
+
result = fn(() => rollback = true);
|
|
687
|
+
} catch (e) {
|
|
688
|
+
txn.abort();
|
|
689
|
+
throw e;
|
|
690
|
+
}
|
|
691
|
+
if (inst2.currentTransaction !== txn) {
|
|
692
|
+
throw new Error("Transaction boundaries overlap");
|
|
693
|
+
}
|
|
694
|
+
if (rollback) {
|
|
695
|
+
txn.abort();
|
|
696
|
+
} else {
|
|
697
|
+
txn.commit();
|
|
698
|
+
}
|
|
699
|
+
return result;
|
|
700
|
+
} finally {
|
|
701
|
+
inst2.currentTransaction = txn.parent;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function transact(fn) {
|
|
705
|
+
if (inst2.currentTransaction) {
|
|
706
|
+
return fn();
|
|
707
|
+
}
|
|
708
|
+
return transaction(fn);
|
|
709
|
+
}
|
|
710
|
+
async function deferAsyncEffects(fn) {
|
|
711
|
+
if (inst2.currentTransaction?.isSync) {
|
|
712
|
+
throw new Error("deferAsyncEffects cannot be called during a sync transaction");
|
|
713
|
+
}
|
|
714
|
+
while (inst2.globalIsReacting) {
|
|
715
|
+
await new Promise((r) => queueMicrotask(() => r(null)));
|
|
716
|
+
}
|
|
717
|
+
const txn = inst2.currentTransaction ?? new Transaction(null, false);
|
|
718
|
+
if (txn.isSync) throw new Error("deferAsyncEffects cannot be called during a sync transaction");
|
|
719
|
+
inst2.currentTransaction = txn;
|
|
720
|
+
txn.asyncProcessCount++;
|
|
721
|
+
let result = void 0;
|
|
722
|
+
let error = void 0;
|
|
723
|
+
try {
|
|
724
|
+
result = await fn();
|
|
725
|
+
} catch (e) {
|
|
726
|
+
error = e ?? null;
|
|
727
|
+
}
|
|
728
|
+
if (--txn.asyncProcessCount > 0) {
|
|
729
|
+
if (typeof error !== "undefined") {
|
|
730
|
+
throw error;
|
|
731
|
+
} else {
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
inst2.currentTransaction = null;
|
|
736
|
+
if (typeof error !== "undefined") {
|
|
737
|
+
txn.abort();
|
|
738
|
+
throw error;
|
|
739
|
+
} else {
|
|
740
|
+
txn.commit();
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/lib/Atom.ts
|
|
746
|
+
var __Atom__ = class {
|
|
747
|
+
constructor(name, current, options) {
|
|
748
|
+
this.name = name;
|
|
749
|
+
this.current = current;
|
|
750
|
+
this.isEqual = options?.isEqual ?? null;
|
|
751
|
+
if (!options) return;
|
|
752
|
+
if (options.historyLength) {
|
|
753
|
+
this.historyBuffer = new HistoryBuffer(options.historyLength);
|
|
754
|
+
}
|
|
755
|
+
this.computeDiff = options.computeDiff;
|
|
756
|
+
}
|
|
757
|
+
name;
|
|
758
|
+
current;
|
|
759
|
+
/**
|
|
760
|
+
* Custom equality function for comparing values, or null to use default equality.
|
|
761
|
+
* @internal
|
|
762
|
+
*/
|
|
763
|
+
isEqual;
|
|
764
|
+
/**
|
|
765
|
+
* Optional function to compute diffs between old and new values.
|
|
766
|
+
* @internal
|
|
767
|
+
*/
|
|
768
|
+
computeDiff;
|
|
769
|
+
/**
|
|
770
|
+
* The global epoch when this atom was last changed.
|
|
771
|
+
* @internal
|
|
772
|
+
*/
|
|
773
|
+
lastChangedEpoch = getGlobalEpoch();
|
|
774
|
+
/**
|
|
775
|
+
* Set of child signals that depend on this atom.
|
|
776
|
+
* @internal
|
|
777
|
+
*/
|
|
778
|
+
children = new ArraySet();
|
|
779
|
+
/**
|
|
780
|
+
* Optional history buffer for tracking changes over time.
|
|
781
|
+
* @internal
|
|
782
|
+
*/
|
|
783
|
+
historyBuffer;
|
|
784
|
+
/**
|
|
785
|
+
* Gets the current value without capturing it as a dependency in the current reactive context.
|
|
786
|
+
* This is unsafe because it breaks the reactivity chain - use with caution.
|
|
787
|
+
*
|
|
788
|
+
* @param _ignoreErrors - Unused parameter for API compatibility
|
|
789
|
+
* @returns The current value
|
|
790
|
+
* @internal
|
|
791
|
+
*/
|
|
792
|
+
__unsafe__getWithoutCapture(_ignoreErrors) {
|
|
793
|
+
return this.current;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Gets the current value of this atom. When called within a computed signal or reaction,
|
|
797
|
+
* this atom will be automatically captured as a dependency.
|
|
798
|
+
*
|
|
799
|
+
* @returns The current value
|
|
800
|
+
* @example
|
|
801
|
+
* ```ts
|
|
802
|
+
* const count = atom('count', 5)
|
|
803
|
+
* console.log(count.get()) // 5
|
|
804
|
+
* ```
|
|
805
|
+
*/
|
|
806
|
+
get() {
|
|
807
|
+
maybeCaptureParent(this);
|
|
808
|
+
return this.current;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* 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.
|
|
812
|
+
*
|
|
813
|
+
* @param value - The new value to set
|
|
814
|
+
* @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}
|
|
815
|
+
* @returns The new value
|
|
816
|
+
* @example
|
|
817
|
+
* ```ts
|
|
818
|
+
* const count = atom('count', 0)
|
|
819
|
+
* count.set(5) // count.get() is now 5
|
|
820
|
+
* ```
|
|
821
|
+
*/
|
|
822
|
+
set(value, diff) {
|
|
823
|
+
if (this.isEqual?.(this.current, value) ?? equals(this.current, value)) {
|
|
824
|
+
return this.current;
|
|
825
|
+
}
|
|
826
|
+
advanceGlobalEpoch();
|
|
827
|
+
if (this.historyBuffer) {
|
|
828
|
+
this.historyBuffer.pushEntry(
|
|
829
|
+
this.lastChangedEpoch,
|
|
830
|
+
getGlobalEpoch(),
|
|
831
|
+
diff ?? this.computeDiff?.(this.current, value, this.lastChangedEpoch, getGlobalEpoch()) ?? RESET_VALUE
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
this.lastChangedEpoch = getGlobalEpoch();
|
|
835
|
+
const oldValue = this.current;
|
|
836
|
+
this.current = value;
|
|
837
|
+
atomDidChange(this, oldValue);
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* 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.
|
|
842
|
+
*
|
|
843
|
+
* @param updater - A function that takes the current value and returns the new value
|
|
844
|
+
* @returns The new value
|
|
845
|
+
* @example
|
|
846
|
+
* ```ts
|
|
847
|
+
* const count = atom('count', 5)
|
|
848
|
+
* count.update(n => n + 1) // count.get() is now 6
|
|
849
|
+
* ```
|
|
850
|
+
*/
|
|
851
|
+
update(updater) {
|
|
852
|
+
return this.set(updater(this.current));
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Gets all the diffs that have occurred since the given epoch. When called within a computed
|
|
856
|
+
* signal or reaction, this atom will be automatically captured as a dependency.
|
|
857
|
+
*
|
|
858
|
+
* @param epoch - The epoch to get changes since
|
|
859
|
+
* @returns An array of diffs, or RESET_VALUE if history is insufficient
|
|
860
|
+
* @internal
|
|
861
|
+
*/
|
|
862
|
+
getDiffSince(epoch) {
|
|
863
|
+
maybeCaptureParent(this);
|
|
864
|
+
if (epoch >= this.lastChangedEpoch) {
|
|
865
|
+
return EMPTY_ARRAY;
|
|
866
|
+
}
|
|
867
|
+
return this.historyBuffer?.getChangesSince(epoch) ?? RESET_VALUE;
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
var _Atom = singleton("Atom", () => __Atom__);
|
|
871
|
+
function atom(name, initialValue, options) {
|
|
872
|
+
return new _Atom(name, initialValue, options);
|
|
873
|
+
}
|
|
874
|
+
function isAtom(value) {
|
|
875
|
+
return value instanceof _Atom;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/lib/warnings.ts
|
|
879
|
+
var didWarnComputedGetter = false;
|
|
880
|
+
function logComputedGetterWarning() {
|
|
881
|
+
if (didWarnComputedGetter) return;
|
|
882
|
+
didWarnComputedGetter = true;
|
|
883
|
+
console.warn(
|
|
884
|
+
`Using \`@computed\` as a decorator for getters is deprecated and will be removed in the near future. Please refactor to use \`@computed\` as a decorator for methods.
|
|
885
|
+
|
|
886
|
+
// Before
|
|
887
|
+
@computed
|
|
888
|
+
get foo() {
|
|
889
|
+
return 'foo'
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// After
|
|
893
|
+
@computed
|
|
894
|
+
getFoo() {
|
|
895
|
+
return 'foo'
|
|
896
|
+
}
|
|
897
|
+
`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/lib/Computed.ts
|
|
902
|
+
var UNINITIALIZED = /* @__PURE__ */ Symbol.for("com.draw.state/UNINITIALIZED");
|
|
903
|
+
function isUninitialized(value) {
|
|
904
|
+
return value === UNINITIALIZED;
|
|
905
|
+
}
|
|
906
|
+
var WithDiff = singleton(
|
|
907
|
+
"WithDiff",
|
|
908
|
+
() => class WithDiff {
|
|
909
|
+
constructor(value, diff) {
|
|
910
|
+
this.value = value;
|
|
911
|
+
this.diff = diff;
|
|
912
|
+
}
|
|
913
|
+
value;
|
|
914
|
+
diff;
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
function withDiff(value, diff) {
|
|
918
|
+
return new WithDiff(value, diff);
|
|
919
|
+
}
|
|
920
|
+
var __UNSAFE__Computed = class {
|
|
921
|
+
constructor(name, derive, options) {
|
|
922
|
+
this.name = name;
|
|
923
|
+
this.derive = derive;
|
|
924
|
+
if (options?.historyLength) {
|
|
925
|
+
this.historyBuffer = new HistoryBuffer(options.historyLength);
|
|
926
|
+
}
|
|
927
|
+
this.computeDiff = options?.computeDiff;
|
|
928
|
+
this.isEqual = options?.isEqual ?? equals;
|
|
929
|
+
}
|
|
930
|
+
name;
|
|
931
|
+
derive;
|
|
932
|
+
__isComputed = true;
|
|
933
|
+
lastChangedEpoch = GLOBAL_START_EPOCH;
|
|
934
|
+
lastTraversedEpoch = GLOBAL_START_EPOCH;
|
|
935
|
+
__debug_ancestor_epochs__ = null;
|
|
936
|
+
/**
|
|
937
|
+
* The epoch when the reactor was last checked.
|
|
938
|
+
*/
|
|
939
|
+
lastCheckedEpoch = GLOBAL_START_EPOCH;
|
|
940
|
+
parentSet = new ArraySet();
|
|
941
|
+
parents = [];
|
|
942
|
+
parentEpochs = [];
|
|
943
|
+
children = new ArraySet();
|
|
944
|
+
// eslint-disable-next-line tldraw/no-setter-getter
|
|
945
|
+
get isActivelyListening() {
|
|
946
|
+
return !this.children.isEmpty;
|
|
947
|
+
}
|
|
948
|
+
historyBuffer;
|
|
949
|
+
// The last-computed value of this signal.
|
|
950
|
+
state = UNINITIALIZED;
|
|
951
|
+
// If the signal throws an error we stash it so we can rethrow it on the next get()
|
|
952
|
+
error = null;
|
|
953
|
+
computeDiff;
|
|
954
|
+
isEqual;
|
|
955
|
+
__unsafe__getWithoutCapture(ignoreErrors) {
|
|
956
|
+
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH;
|
|
957
|
+
const globalEpoch = getGlobalEpoch();
|
|
958
|
+
if (!isNew && (this.lastCheckedEpoch === globalEpoch || this.isActivelyListening && getIsReacting() && this.lastTraversedEpoch < getReactionEpoch() || !haveParentsChanged(this))) {
|
|
959
|
+
this.lastCheckedEpoch = globalEpoch;
|
|
960
|
+
if (this.error) {
|
|
961
|
+
if (!ignoreErrors) {
|
|
962
|
+
throw this.error.thrownValue;
|
|
963
|
+
} else {
|
|
964
|
+
return this.state;
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
return this.state;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
startCapturingParents(this);
|
|
972
|
+
const result = this.derive(this.state, this.lastCheckedEpoch);
|
|
973
|
+
const newState = result instanceof WithDiff ? result.value : result;
|
|
974
|
+
const isUninitialized2 = this.state === UNINITIALIZED;
|
|
975
|
+
if (isUninitialized2 || !this.isEqual(newState, this.state)) {
|
|
976
|
+
if (this.historyBuffer && !isUninitialized2) {
|
|
977
|
+
const diff = result instanceof WithDiff ? result.diff : void 0;
|
|
978
|
+
this.historyBuffer.pushEntry(
|
|
979
|
+
this.lastChangedEpoch,
|
|
980
|
+
getGlobalEpoch(),
|
|
981
|
+
diff ?? this.computeDiff?.(this.state, newState, this.lastCheckedEpoch, getGlobalEpoch()) ?? RESET_VALUE
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
this.lastChangedEpoch = getGlobalEpoch();
|
|
985
|
+
this.state = newState;
|
|
986
|
+
}
|
|
987
|
+
this.error = null;
|
|
988
|
+
this.lastCheckedEpoch = getGlobalEpoch();
|
|
989
|
+
return this.state;
|
|
990
|
+
} catch (e) {
|
|
991
|
+
if (this.state !== UNINITIALIZED) {
|
|
992
|
+
this.state = UNINITIALIZED;
|
|
993
|
+
this.lastChangedEpoch = getGlobalEpoch();
|
|
994
|
+
}
|
|
995
|
+
this.lastCheckedEpoch = getGlobalEpoch();
|
|
996
|
+
if (this.historyBuffer) {
|
|
997
|
+
this.historyBuffer.clear();
|
|
998
|
+
}
|
|
999
|
+
this.error = { thrownValue: e };
|
|
1000
|
+
if (!ignoreErrors) throw e;
|
|
1001
|
+
return this.state;
|
|
1002
|
+
} finally {
|
|
1003
|
+
stopCapturingParents();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
get() {
|
|
1007
|
+
try {
|
|
1008
|
+
return this.__unsafe__getWithoutCapture();
|
|
1009
|
+
} finally {
|
|
1010
|
+
maybeCaptureParent(this);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
getDiffSince(epoch) {
|
|
1014
|
+
this.__unsafe__getWithoutCapture(true);
|
|
1015
|
+
maybeCaptureParent(this);
|
|
1016
|
+
if (epoch >= this.lastChangedEpoch) {
|
|
1017
|
+
return EMPTY_ARRAY;
|
|
1018
|
+
}
|
|
1019
|
+
return this.historyBuffer?.getChangesSince(epoch) ?? RESET_VALUE;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
var _Computed = singleton("Computed", () => __UNSAFE__Computed);
|
|
1023
|
+
function computedMethodLegacyDecorator(options = {}, _target, key, descriptor) {
|
|
1024
|
+
const originalMethod = descriptor.value;
|
|
1025
|
+
const derivationKey = /* @__PURE__ */ Symbol.for("__@ibodr/state__computed__" + key);
|
|
1026
|
+
descriptor.value = function() {
|
|
1027
|
+
let d = this[derivationKey];
|
|
1028
|
+
if (!d) {
|
|
1029
|
+
d = new _Computed(key, originalMethod.bind(this), options);
|
|
1030
|
+
Object.defineProperty(this, derivationKey, {
|
|
1031
|
+
enumerable: false,
|
|
1032
|
+
configurable: false,
|
|
1033
|
+
writable: false,
|
|
1034
|
+
value: d
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return d.get();
|
|
1038
|
+
};
|
|
1039
|
+
descriptor.value[isComputedMethodKey] = true;
|
|
1040
|
+
return descriptor;
|
|
1041
|
+
}
|
|
1042
|
+
function computedGetterLegacyDecorator(options = {}, _target, key, descriptor) {
|
|
1043
|
+
const originalMethod = descriptor.get;
|
|
1044
|
+
const derivationKey = /* @__PURE__ */ Symbol.for("__@ibodr/state__computed__" + key);
|
|
1045
|
+
descriptor.get = function() {
|
|
1046
|
+
let d = this[derivationKey];
|
|
1047
|
+
if (!d) {
|
|
1048
|
+
d = new _Computed(key, originalMethod.bind(this), options);
|
|
1049
|
+
Object.defineProperty(this, derivationKey, {
|
|
1050
|
+
enumerable: false,
|
|
1051
|
+
configurable: false,
|
|
1052
|
+
writable: false,
|
|
1053
|
+
value: d
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
return d.get();
|
|
1057
|
+
};
|
|
1058
|
+
return descriptor;
|
|
1059
|
+
}
|
|
1060
|
+
function computedMethodTc39Decorator(options, compute, context) {
|
|
1061
|
+
assert(context.kind === "method", "@computed can only be used on methods");
|
|
1062
|
+
const derivationKey = /* @__PURE__ */ Symbol.for("__@ibodr/state__computed__" + String(context.name));
|
|
1063
|
+
const fn = function() {
|
|
1064
|
+
let d = this[derivationKey];
|
|
1065
|
+
if (!d) {
|
|
1066
|
+
d = new _Computed(String(context.name), compute.bind(this), options);
|
|
1067
|
+
Object.defineProperty(this, derivationKey, {
|
|
1068
|
+
enumerable: false,
|
|
1069
|
+
configurable: false,
|
|
1070
|
+
writable: false,
|
|
1071
|
+
value: d
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
return d.get();
|
|
1075
|
+
};
|
|
1076
|
+
fn[isComputedMethodKey] = true;
|
|
1077
|
+
return fn;
|
|
1078
|
+
}
|
|
1079
|
+
function computedDecorator(options = {}, args) {
|
|
1080
|
+
if (args.length === 2) {
|
|
1081
|
+
const [originalMethod, context] = args;
|
|
1082
|
+
return computedMethodTc39Decorator(options, originalMethod, context);
|
|
1083
|
+
} else {
|
|
1084
|
+
const [_target, key, descriptor] = args;
|
|
1085
|
+
if (descriptor.get) {
|
|
1086
|
+
logComputedGetterWarning();
|
|
1087
|
+
return computedGetterLegacyDecorator(options, _target, key, descriptor);
|
|
1088
|
+
} else {
|
|
1089
|
+
return computedMethodLegacyDecorator(options, _target, key, descriptor);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
var isComputedMethodKey = "@@__isComputedMethod__@@";
|
|
1094
|
+
function getComputedInstance(obj, propertyName) {
|
|
1095
|
+
const key = /* @__PURE__ */ Symbol.for("__@ibodr/state__computed__" + propertyName.toString());
|
|
1096
|
+
let inst3 = obj[key];
|
|
1097
|
+
if (!inst3) {
|
|
1098
|
+
const val = obj[propertyName];
|
|
1099
|
+
if (typeof val === "function" && val[isComputedMethodKey]) {
|
|
1100
|
+
val.call(obj);
|
|
1101
|
+
}
|
|
1102
|
+
inst3 = obj[key];
|
|
1103
|
+
}
|
|
1104
|
+
return inst3;
|
|
1105
|
+
}
|
|
1106
|
+
function computed() {
|
|
1107
|
+
if (arguments.length === 1) {
|
|
1108
|
+
const options = arguments[0];
|
|
1109
|
+
return (...args) => computedDecorator(options, args);
|
|
1110
|
+
} else if (typeof arguments[0] === "string") {
|
|
1111
|
+
return new _Computed(arguments[0], arguments[1], arguments[2]);
|
|
1112
|
+
} else {
|
|
1113
|
+
return computedDecorator(void 0, arguments);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/lib/EffectScheduler.ts
|
|
1118
|
+
var __EffectScheduler__ = class {
|
|
1119
|
+
constructor(name, runEffect, options) {
|
|
1120
|
+
this.name = name;
|
|
1121
|
+
this.runEffect = runEffect;
|
|
1122
|
+
this._scheduleEffect = options?.scheduleEffect;
|
|
1123
|
+
}
|
|
1124
|
+
name;
|
|
1125
|
+
runEffect;
|
|
1126
|
+
__isEffectScheduler = true;
|
|
1127
|
+
/** @internal */
|
|
1128
|
+
_isActivelyListening = false;
|
|
1129
|
+
/**
|
|
1130
|
+
* Whether this scheduler is attached and actively listening to its parents.
|
|
1131
|
+
* @public
|
|
1132
|
+
*/
|
|
1133
|
+
// eslint-disable-next-line tldraw/no-setter-getter
|
|
1134
|
+
get isActivelyListening() {
|
|
1135
|
+
return this._isActivelyListening;
|
|
1136
|
+
}
|
|
1137
|
+
/** @internal */
|
|
1138
|
+
lastTraversedEpoch = GLOBAL_START_EPOCH;
|
|
1139
|
+
/** @internal */
|
|
1140
|
+
lastReactedEpoch = GLOBAL_START_EPOCH;
|
|
1141
|
+
/** @internal */
|
|
1142
|
+
_scheduleCount = 0;
|
|
1143
|
+
/** @internal */
|
|
1144
|
+
__debug_ancestor_epochs__ = null;
|
|
1145
|
+
/**
|
|
1146
|
+
* The number of times this effect has been scheduled.
|
|
1147
|
+
* @public
|
|
1148
|
+
*/
|
|
1149
|
+
// eslint-disable-next-line tldraw/no-setter-getter
|
|
1150
|
+
get scheduleCount() {
|
|
1151
|
+
return this._scheduleCount;
|
|
1152
|
+
}
|
|
1153
|
+
/** @internal */
|
|
1154
|
+
parentSet = new ArraySet();
|
|
1155
|
+
/** @internal */
|
|
1156
|
+
parentEpochs = [];
|
|
1157
|
+
/** @internal */
|
|
1158
|
+
parents = [];
|
|
1159
|
+
/** @internal */
|
|
1160
|
+
_scheduleEffect;
|
|
1161
|
+
/** @internal */
|
|
1162
|
+
maybeScheduleEffect() {
|
|
1163
|
+
if (!this._isActivelyListening) return;
|
|
1164
|
+
if (this.lastReactedEpoch === getGlobalEpoch()) return;
|
|
1165
|
+
if (this.parents.length && !haveParentsChanged(this)) {
|
|
1166
|
+
this.lastReactedEpoch = getGlobalEpoch();
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
this.scheduleEffect();
|
|
1170
|
+
}
|
|
1171
|
+
/** @internal */
|
|
1172
|
+
scheduleEffect() {
|
|
1173
|
+
this._scheduleCount++;
|
|
1174
|
+
if (this._scheduleEffect) {
|
|
1175
|
+
this._scheduleEffect(this.maybeExecute);
|
|
1176
|
+
} else {
|
|
1177
|
+
this.execute();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
/** @internal */
|
|
1181
|
+
// eslint-disable-next-line tldraw/prefer-class-methods
|
|
1182
|
+
maybeExecute = () => {
|
|
1183
|
+
if (!this._isActivelyListening) return;
|
|
1184
|
+
this.execute();
|
|
1185
|
+
};
|
|
1186
|
+
/**
|
|
1187
|
+
* Makes this scheduler become 'actively listening' to its parents.
|
|
1188
|
+
* If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls.
|
|
1189
|
+
* If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling `EffectScheduler.execute`.
|
|
1190
|
+
* @public
|
|
1191
|
+
*/
|
|
1192
|
+
attach() {
|
|
1193
|
+
this._isActivelyListening = true;
|
|
1194
|
+
for (let i = 0, n = this.parents.length; i < n; i++) {
|
|
1195
|
+
attach(this.parents[i], this);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Makes this scheduler stop 'actively listening' to its parents.
|
|
1200
|
+
* It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again.
|
|
1201
|
+
* @public
|
|
1202
|
+
*/
|
|
1203
|
+
detach() {
|
|
1204
|
+
this._isActivelyListening = false;
|
|
1205
|
+
for (let i = 0, n = this.parents.length; i < n; i++) {
|
|
1206
|
+
detach(this.parents[i], this);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Executes the effect immediately and returns the result.
|
|
1211
|
+
* @returns The result of the effect.
|
|
1212
|
+
* @public
|
|
1213
|
+
*/
|
|
1214
|
+
execute() {
|
|
1215
|
+
try {
|
|
1216
|
+
startCapturingParents(this);
|
|
1217
|
+
const currentEpoch = getGlobalEpoch();
|
|
1218
|
+
const result = this.runEffect(this.lastReactedEpoch);
|
|
1219
|
+
this.lastReactedEpoch = currentEpoch;
|
|
1220
|
+
return result;
|
|
1221
|
+
} finally {
|
|
1222
|
+
stopCapturingParents();
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
var EffectScheduler = singleton(
|
|
1227
|
+
"EffectScheduler",
|
|
1228
|
+
() => __EffectScheduler__
|
|
1229
|
+
);
|
|
1230
|
+
function react(name, fn, options) {
|
|
1231
|
+
const scheduler = new EffectScheduler(name, fn, options);
|
|
1232
|
+
scheduler.attach();
|
|
1233
|
+
scheduler.scheduleEffect();
|
|
1234
|
+
return () => {
|
|
1235
|
+
scheduler.detach();
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function reactor(name, fn, options) {
|
|
1239
|
+
const scheduler = new EffectScheduler(name, fn, options);
|
|
1240
|
+
return {
|
|
1241
|
+
scheduler,
|
|
1242
|
+
start: (options2) => {
|
|
1243
|
+
const force = options2?.force ?? false;
|
|
1244
|
+
scheduler.attach();
|
|
1245
|
+
if (force) {
|
|
1246
|
+
scheduler.scheduleEffect();
|
|
1247
|
+
} else {
|
|
1248
|
+
scheduler.maybeScheduleEffect();
|
|
1249
|
+
}
|
|
1250
|
+
},
|
|
1251
|
+
stop: () => {
|
|
1252
|
+
scheduler.detach();
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// src/lib/isSignal.ts
|
|
1258
|
+
function isSignal(value) {
|
|
1259
|
+
return value instanceof _Atom || value instanceof _Computed;
|
|
1260
|
+
}
|
|
1261
|
+
function localStorageAtom(name, initialValue, options) {
|
|
1262
|
+
let _initialValue = initialValue;
|
|
1263
|
+
try {
|
|
1264
|
+
const value = getFromLocalStorage(name);
|
|
1265
|
+
if (value) {
|
|
1266
|
+
_initialValue = JSON.parse(value);
|
|
1267
|
+
}
|
|
1268
|
+
} catch {
|
|
1269
|
+
deleteFromLocalStorage(name);
|
|
1270
|
+
}
|
|
1271
|
+
const outAtom = atom(name, _initialValue, options);
|
|
1272
|
+
const reactCleanup = react(`save ${name} to localStorage`, () => {
|
|
1273
|
+
setInLocalStorage(name, JSON.stringify(outAtom.get()));
|
|
1274
|
+
});
|
|
1275
|
+
const handleStorageEvent = (event) => {
|
|
1276
|
+
if (event.key !== name) return;
|
|
1277
|
+
if (event.newValue === null) {
|
|
1278
|
+
outAtom.set(initialValue);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
const newValue = JSON.parse(event.newValue);
|
|
1283
|
+
outAtom.set(newValue);
|
|
1284
|
+
} catch {
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
window.addEventListener("storage", handleStorageEvent);
|
|
1288
|
+
const cleanup = () => {
|
|
1289
|
+
reactCleanup();
|
|
1290
|
+
window.removeEventListener("storage", handleStorageEvent);
|
|
1291
|
+
};
|
|
1292
|
+
return [outAtom, cleanup];
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/index.ts
|
|
1296
|
+
var currentApiVersion = 1;
|
|
1297
|
+
var actualApiVersion = singleton("apiVersion", () => currentApiVersion);
|
|
1298
|
+
if (actualApiVersion !== currentApiVersion) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
`You have multiple incompatible versions of @ibodr/state in your app. Please deduplicate the package.`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
registerDrawLibraryVersion(
|
|
1304
|
+
"@ibodr/state",
|
|
1305
|
+
"0.0.0",
|
|
1306
|
+
"esm"
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1309
|
+
export { ArraySet, EMPTY_ARRAY, EffectScheduler, RESET_VALUE, UNINITIALIZED, atom, computed, deferAsyncEffects, getComputedInstance, isAtom, isSignal, isUninitialized, localStorageAtom, react, reactor, transact, transaction, unsafe__withoutCapture, whyAmIRunning, withDiff };
|
|
1310
|
+
//# sourceMappingURL=index.mjs.map
|
|
1311
|
+
//# sourceMappingURL=index.mjs.map
|