@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.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