@getforma/core 1.0.10 → 1.2.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.
Files changed (55) hide show
  1. package/dist/{chunk-T33QUD2Y.cjs → chunk-E3FV2VSU.cjs} +43 -43
  2. package/dist/{chunk-T33QUD2Y.cjs.map → chunk-E3FV2VSU.cjs.map} +1 -1
  3. package/dist/chunk-ETPJTPYD.js +103 -0
  4. package/dist/chunk-ETPJTPYD.js.map +1 -0
  5. package/dist/chunk-H7MDUHS5.cjs +110 -0
  6. package/dist/chunk-H7MDUHS5.cjs.map +1 -0
  7. package/dist/{chunk-6FW5E54W.cjs → chunk-KUXNZ5MG.cjs} +127 -103
  8. package/dist/chunk-KUXNZ5MG.cjs.map +1 -0
  9. package/dist/{chunk-3G7ET4O5.js → chunk-Y3A2EVVS.js} +121 -94
  10. package/dist/chunk-Y3A2EVVS.js.map +1 -0
  11. package/dist/{chunk-AFRFF7XL.js → chunk-YRNYOZF3.js} +4 -4
  12. package/dist/{chunk-AFRFF7XL.js.map → chunk-YRNYOZF3.js.map} +1 -1
  13. package/dist/forma-runtime-csp.js +52 -4
  14. package/dist/forma-runtime.js +98 -10
  15. package/dist/formajs-runtime-hardened.global.js +52 -4
  16. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  17. package/dist/formajs-runtime.global.js +98 -10
  18. package/dist/formajs-runtime.global.js.map +1 -1
  19. package/dist/formajs.global.js +368 -147
  20. package/dist/formajs.global.js.map +1 -1
  21. package/dist/http.cjs +11 -11
  22. package/dist/http.js +2 -2
  23. package/dist/index.cjs +262 -141
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +32 -27
  26. package/dist/index.d.ts +32 -27
  27. package/dist/index.js +204 -83
  28. package/dist/index.js.map +1 -1
  29. package/dist/{resource-DeEzxUz6.d.cts → resource-D6HfVgiA.d.cts} +7 -2
  30. package/dist/{resource-BHsgURy0.d.ts → resource-ljs2X5NM.d.ts} +7 -2
  31. package/dist/runtime-hardened.cjs +53 -3
  32. package/dist/runtime-hardened.cjs.map +1 -1
  33. package/dist/runtime-hardened.js +53 -3
  34. package/dist/runtime-hardened.js.map +1 -1
  35. package/dist/runtime.cjs +29 -29
  36. package/dist/runtime.js +3 -3
  37. package/dist/server.cjs +7 -7
  38. package/dist/server.d.cts +2 -2
  39. package/dist/server.d.ts +2 -2
  40. package/dist/server.js +2 -2
  41. package/dist/{signal-C9v4akyJ.d.cts → signal-CRBJYQ6B.d.cts} +0 -8
  42. package/dist/{signal-C9v4akyJ.d.ts → signal-CRBJYQ6B.d.ts} +0 -8
  43. package/dist/tc39-compat.cjs +3 -6
  44. package/dist/tc39-compat.cjs.map +1 -1
  45. package/dist/tc39-compat.d.cts +1 -1
  46. package/dist/tc39-compat.d.ts +1 -1
  47. package/dist/tc39-compat.js +1 -4
  48. package/dist/tc39-compat.js.map +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-3G7ET4O5.js.map +0 -1
  51. package/dist/chunk-6FW5E54W.cjs.map +0 -1
  52. package/dist/chunk-HLM5BZZQ.js +0 -35
  53. package/dist/chunk-HLM5BZZQ.js.map +0 -1
  54. package/dist/chunk-QLPCVK7C.cjs +0 -38
  55. package/dist/chunk-QLPCVK7C.cjs.map +0 -1
@@ -348,6 +348,9 @@ var FormaJS = (() => {
348
348
  }
349
349
  }
350
350
  });
351
+ function getActiveSub() {
352
+ return activeSub;
353
+ }
351
354
  function setActiveSub(sub) {
352
355
  const prevSub = activeSub;
353
356
  activeSub = sub;
@@ -593,16 +596,50 @@ var FormaJS = (() => {
593
596
  }
594
597
  }
595
598
 
599
+ // src/reactive/dev.ts
600
+ function isDev() {
601
+ if (typeof process !== "undefined") {
602
+ return process.env?.NODE_ENV !== "production";
603
+ }
604
+ return typeof __FORMA_DEV__ !== "undefined" ? !!__FORMA_DEV__ : false;
605
+ }
606
+ var __DEV__ = isDev();
607
+ var _errorHandlers = /* @__PURE__ */ new Set();
608
+ function onError(handler) {
609
+ _errorHandlers.add(handler);
610
+ return () => {
611
+ _errorHandlers.delete(handler);
612
+ };
613
+ }
614
+ function reportError(error, source) {
615
+ for (const handler of _errorHandlers) {
616
+ try {
617
+ handler(error, source ? { source } : {});
618
+ } catch {
619
+ }
620
+ }
621
+ if (isDev()) {
622
+ console.error(`[forma] ${source ?? "Unknown"} error:`, error);
623
+ }
624
+ }
625
+
596
626
  // src/reactive/signal.ts
627
+ var signalNames = /* @__PURE__ */ new WeakMap();
628
+ function value(v) {
629
+ return () => v;
630
+ }
597
631
  function applySignalSet(s, v, equals) {
598
632
  if (typeof v !== "function") {
599
- if (equals) {
600
- const prevSub2 = setActiveSub(void 0);
601
- const prev2 = s();
602
- setActiveSub(prevSub2);
603
- if (equals(prev2, v)) return;
633
+ if (!equals) {
634
+ s(v);
635
+ return;
604
636
  }
637
+ const prevSub2 = setActiveSub(void 0);
638
+ const prev2 = s();
639
+ setActiveSub(prevSub2);
640
+ if (equals(prev2, v)) return;
605
641
  s(v);
642
+ if (Object.is(prev2, v)) forceNotify(s);
606
643
  return;
607
644
  }
608
645
  const prevSub = setActiveSub(void 0);
@@ -611,10 +648,19 @@ var FormaJS = (() => {
611
648
  const next = v(prev);
612
649
  if (equals && equals(prev, next)) return;
613
650
  s(next);
651
+ if (equals && Object.is(prev, next)) forceNotify(s);
652
+ }
653
+ function forceNotify(s) {
654
+ trigger(() => {
655
+ s();
656
+ });
614
657
  }
615
658
  function createSignal(initialValue, options) {
616
659
  const s = signal(initialValue);
617
660
  const getter = s;
661
+ if (__DEV__ && options?.name) {
662
+ signalNames.set(getter, options.name);
663
+ }
618
664
  const eq = options?.equals;
619
665
  const setter = (v) => applySignalSet(s, v, eq);
620
666
  return [getter, setter];
@@ -623,29 +669,73 @@ var FormaJS = (() => {
623
669
  // src/reactive/root.ts
624
670
  var currentRoot = null;
625
671
  var rootStack = [];
672
+ var currentOwner = null;
673
+ function getOwner() {
674
+ return currentOwner;
675
+ }
676
+ function runWithOwner(owner, fn) {
677
+ const prev = currentOwner;
678
+ currentOwner = owner;
679
+ try {
680
+ return fn();
681
+ } finally {
682
+ currentOwner = prev;
683
+ }
684
+ }
685
+ function registerOwnerDisposer(owner, dispose) {
686
+ owner.disposers.push(dispose);
687
+ }
688
+ function createChildOwner() {
689
+ return { disposers: [] };
690
+ }
691
+ function disposeOwner(owner) {
692
+ const ds = owner.disposers;
693
+ for (const d of ds) {
694
+ try {
695
+ d();
696
+ } catch {
697
+ }
698
+ }
699
+ ds.length = 0;
700
+ }
626
701
  function createRootImpl(fn, owned) {
627
702
  const scope = { disposers: [], scopeDispose: null };
628
703
  const parentRoot = owned ? currentRoot : null;
629
704
  rootStack.push(currentRoot);
630
705
  currentRoot = scope;
706
+ const prevOwner = currentOwner;
707
+ currentOwner = scope;
631
708
  let disposed = false;
632
- const dispose = () => {
633
- if (disposed) return;
634
- disposed = true;
635
- if (scope.scopeDispose) {
709
+ let setupComplete = false;
710
+ let disposeRequestedDuringSetup = false;
711
+ const runDisposers = () => {
712
+ for (const d of scope.disposers) {
636
713
  try {
637
- scope.scopeDispose();
714
+ d();
638
715
  } catch {
639
716
  }
640
- scope.scopeDispose = null;
641
717
  }
642
- for (const d of scope.disposers) {
718
+ scope.disposers.length = 0;
719
+ };
720
+ const runTeardown = () => {
721
+ if (scope.scopeDispose) {
643
722
  try {
644
- d();
723
+ scope.scopeDispose();
645
724
  } catch {
646
725
  }
726
+ scope.scopeDispose = null;
647
727
  }
648
- scope.disposers.length = 0;
728
+ runDisposers();
729
+ };
730
+ const dispose = () => {
731
+ if (disposed) return;
732
+ disposed = true;
733
+ if (!setupComplete) {
734
+ disposeRequestedDuringSetup = true;
735
+ runDisposers();
736
+ return;
737
+ }
738
+ runTeardown();
649
739
  };
650
740
  if (parentRoot) {
651
741
  parentRoot.disposers.push(dispose);
@@ -666,8 +756,13 @@ var FormaJS = (() => {
666
756
  setActiveSub(prevSub);
667
757
  }
668
758
  }
759
+ setupComplete = true;
760
+ if (disposeRequestedDuringSetup) {
761
+ runTeardown();
762
+ }
669
763
  } finally {
670
764
  currentRoot = rootStack.pop() ?? null;
765
+ currentOwner = prevOwner;
671
766
  }
672
767
  return result;
673
768
  }
@@ -678,12 +773,12 @@ var FormaJS = (() => {
678
773
  return createRootImpl(fn, false);
679
774
  }
680
775
  function registerDisposer(dispose) {
681
- if (currentRoot) {
682
- currentRoot.disposers.push(dispose);
776
+ if (currentOwner) {
777
+ currentOwner.disposers.push(dispose);
683
778
  }
684
779
  }
685
780
  function hasActiveRoot() {
686
- return currentRoot !== null;
781
+ return currentOwner !== null;
687
782
  }
688
783
 
689
784
  // src/reactive/cleanup.ts
@@ -697,30 +792,13 @@ var FormaJS = (() => {
697
792
  return prev;
698
793
  }
699
794
 
700
- // src/reactive/dev.ts
701
- var __DEV__ = typeof process !== "undefined" ? process.env?.NODE_ENV !== "production" : true;
702
- var _errorHandler = null;
703
- function onError(handler) {
704
- _errorHandler = handler;
705
- }
706
- function reportError(error, source) {
707
- if (_errorHandler) {
708
- try {
709
- _errorHandler(error, source ? { source } : {});
710
- } catch {
711
- }
712
- }
713
- if (__DEV__) {
714
- console.error(`[forma] ${source ?? "Unknown"} error:`, error);
715
- }
716
- }
717
-
718
795
  // src/reactive/effect.ts
719
796
  var POOL_SIZE = 32;
720
797
  var MAX_REENTRANT_RUNS = 100;
721
798
  var pool = [];
722
799
  for (let i = 0; i < POOL_SIZE; i++) pool.push([]);
723
800
  var poolIdx = POOL_SIZE;
801
+ var PENDING = 32;
724
802
  function acquireArray() {
725
803
  if (poolIdx > 0) {
726
804
  const arr = pool[--poolIdx];
@@ -761,7 +839,8 @@ var FormaJS = (() => {
761
839
  return dispose;
762
840
  }
763
841
  function createEffect(fn) {
764
- const shouldRegister = hasActiveRoot();
842
+ const ownerAtCreate = getOwner();
843
+ const childOwner = createChildOwner();
765
844
  let cleanup2;
766
845
  let cleanupBag;
767
846
  let nextCleanup;
@@ -780,11 +859,8 @@ var FormaJS = (() => {
780
859
  }
781
860
  nextCleanup = cb;
782
861
  };
783
- let skipCleanupInfra = false;
784
- let firstRun = true;
785
- let running = false;
786
- let rerunRequested = false;
787
862
  const runOnce = () => {
863
+ disposeOwner(childOwner);
788
864
  if (cleanup2 !== void 0) {
789
865
  runCleanup(cleanup2);
790
866
  cleanup2 = void 0;
@@ -794,68 +870,46 @@ var FormaJS = (() => {
794
870
  releaseArray(cleanupBag);
795
871
  cleanupBag = void 0;
796
872
  }
797
- if (skipCleanupInfra) {
798
- try {
799
- fn();
800
- } catch (e) {
801
- reportError(e, "effect");
802
- }
803
- return;
804
- }
805
873
  nextCleanup = void 0;
806
874
  nextCleanupBag = void 0;
807
875
  const prevCollector = setCleanupCollector(addCleanup);
808
876
  try {
809
- const result = fn();
877
+ const result = runWithOwner(childOwner, fn);
810
878
  if (typeof result === "function") {
811
879
  addCleanup(result);
812
880
  }
813
- if (nextCleanup === void 0 && nextCleanupBag === void 0) {
814
- if (firstRun) skipCleanupInfra = true;
815
- return;
816
- }
817
881
  if (nextCleanupBag !== void 0) {
818
882
  cleanupBag = nextCleanupBag;
819
- } else {
883
+ } else if (nextCleanup !== void 0) {
820
884
  cleanup2 = nextCleanup;
821
885
  }
822
886
  } catch (e) {
823
887
  reportError(e, "effect");
824
888
  if (nextCleanupBag !== void 0) {
825
889
  cleanupBag = nextCleanupBag;
826
- } else {
890
+ } else if (nextCleanup !== void 0) {
827
891
  cleanup2 = nextCleanup;
828
892
  }
829
893
  } finally {
830
894
  setCleanupCollector(prevCollector);
831
- firstRun = false;
832
895
  }
833
896
  };
834
897
  const safeFn = () => {
835
- if (running) {
836
- rerunRequested = true;
837
- return;
838
- }
839
- running = true;
840
- try {
841
- let reentrantRuns = 0;
842
- do {
843
- rerunRequested = false;
844
- runOnce();
845
- if (rerunRequested) {
846
- reentrantRuns++;
847
- if (reentrantRuns >= MAX_REENTRANT_RUNS) {
848
- reportError(
849
- new Error(`createEffect exceeded ${MAX_REENTRANT_RUNS} re-entrant runs`),
850
- "effect"
851
- );
852
- rerunRequested = false;
853
- }
854
- }
855
- } while (rerunRequested);
856
- } finally {
857
- running = false;
858
- }
898
+ const node = getActiveSub();
899
+ let reentrantRuns = 0;
900
+ do {
901
+ if (node) node.flags &= ~PENDING;
902
+ runOnce();
903
+ if (node === void 0 || (node.flags & PENDING) === 0) break;
904
+ if (++reentrantRuns >= MAX_REENTRANT_RUNS) {
905
+ node.flags &= ~PENDING;
906
+ reportError(
907
+ new Error(`createEffect exceeded ${MAX_REENTRANT_RUNS} self-triggered re-runs (cycle?)`),
908
+ "effect"
909
+ );
910
+ break;
911
+ }
912
+ } while (true);
859
913
  };
860
914
  const dispose = effect(safeFn);
861
915
  let disposed = false;
@@ -863,6 +917,7 @@ var FormaJS = (() => {
863
917
  if (disposed) return;
864
918
  disposed = true;
865
919
  dispose();
920
+ disposeOwner(childOwner);
866
921
  if (cleanup2 !== void 0) {
867
922
  runCleanup(cleanup2);
868
923
  cleanup2 = void 0;
@@ -873,15 +928,42 @@ var FormaJS = (() => {
873
928
  cleanupBag = void 0;
874
929
  }
875
930
  };
876
- if (shouldRegister) {
877
- registerDisposer(wrappedDispose);
931
+ if (ownerAtCreate) {
932
+ registerOwnerDisposer(ownerAtCreate, wrappedDispose);
878
933
  }
879
934
  return wrappedDispose;
880
935
  }
881
936
 
882
937
  // src/reactive/computed.ts
938
+ var ERR = /* @__PURE__ */ Symbol("formaComputedError");
939
+ function isErrBox(v) {
940
+ return typeof v === "object" && v !== null && ERR in v;
941
+ }
883
942
  function createComputed(fn) {
884
- return computed(fn);
943
+ let errored = false;
944
+ let error;
945
+ let lastGood;
946
+ const raw = computed(() => {
947
+ try {
948
+ const v = fn(lastGood);
949
+ errored = false;
950
+ error = void 0;
951
+ lastGood = v;
952
+ return v;
953
+ } catch (e) {
954
+ errored = true;
955
+ error = e;
956
+ reportError(e, "computed");
957
+ return { [ERR]: e };
958
+ }
959
+ });
960
+ const reader = () => {
961
+ const v = raw();
962
+ if (errored || isErrBox(v)) throw error;
963
+ return v;
964
+ };
965
+ Object.defineProperty(reader, "name", { value: raw.name, configurable: true });
966
+ return reader;
885
967
  }
886
968
 
887
969
  // src/reactive/memo.ts
@@ -974,25 +1056,34 @@ var FormaJS = (() => {
974
1056
  suspenseCtx.increment();
975
1057
  suspensePending = true;
976
1058
  }
977
- setLoading(true);
978
- setError(void 0);
979
- Promise.resolve(fetcher(sourceValue)).then((result) => {
1059
+ batch(() => {
1060
+ setLoading(true);
1061
+ setError(void 0);
1062
+ });
1063
+ let promise;
1064
+ try {
1065
+ promise = Promise.resolve(fetcher(sourceValue, { signal: controller.signal }));
1066
+ } catch (err) {
1067
+ promise = Promise.reject(err);
1068
+ }
1069
+ promise.then((result) => {
980
1070
  if (isLatest() && !controller.signal.aborted) {
981
- setData(() => result);
1071
+ batch(() => {
1072
+ setData(() => result);
1073
+ setLoading(false);
1074
+ });
982
1075
  }
983
1076
  }).catch((err) => {
984
- if (isLatest() && !controller.signal.aborted) {
985
- if (err?.name !== "AbortError") {
1077
+ if (isLatest() && !controller.signal.aborted && err?.name !== "AbortError") {
1078
+ batch(() => {
986
1079
  setError(err);
987
- }
1080
+ setLoading(false);
1081
+ });
988
1082
  }
989
1083
  }).finally(() => {
990
1084
  if (suspensePending) suspenseCtx?.decrement();
991
- if (isLatest()) {
992
- setLoading(false);
993
- if (abortController === controller) {
994
- abortController = null;
995
- }
1085
+ if (isLatest() && abortController === controller) {
1086
+ abortController = null;
996
1087
  }
997
1088
  });
998
1089
  };
@@ -1000,6 +1091,15 @@ var FormaJS = (() => {
1000
1091
  source();
1001
1092
  doFetch();
1002
1093
  });
1094
+ if (hasActiveRoot()) {
1095
+ registerDisposer(() => {
1096
+ fetchVersion++;
1097
+ if (abortController) {
1098
+ abortController.abort();
1099
+ abortController = null;
1100
+ }
1101
+ });
1102
+ }
1003
1103
  const resource = (() => data());
1004
1104
  resource.loading = loading;
1005
1105
  resource.error = error;
@@ -3059,6 +3159,19 @@ var FormaJS = (() => {
3059
3159
  function createStore(initial) {
3060
3160
  const signals = /* @__PURE__ */ new Map();
3061
3161
  const children2 = /* @__PURE__ */ new Map();
3162
+ const arrayVersions = /* @__PURE__ */ new Map();
3163
+ function getArrayVersion(path) {
3164
+ let p = arrayVersions.get(path);
3165
+ if (!p) {
3166
+ p = createSignal(0);
3167
+ arrayVersions.set(path, p);
3168
+ }
3169
+ return p;
3170
+ }
3171
+ function bumpArrayVersion(path) {
3172
+ const p = arrayVersions.get(path);
3173
+ if (p) p[1]((n) => n + 1);
3174
+ }
3062
3175
  function registerChild(path) {
3063
3176
  const lastDot = path.lastIndexOf(".");
3064
3177
  if (lastDot === -1) return;
@@ -3090,14 +3203,37 @@ var FormaJS = (() => {
3090
3203
  }
3091
3204
  childSet.clear();
3092
3205
  }
3206
+ function lastSegment(path) {
3207
+ const d = path.lastIndexOf(".");
3208
+ return d === -1 ? path : path.substring(d + 1);
3209
+ }
3210
+ function setLiteral(pair, v) {
3211
+ pair[1](typeof v === "function" ? value(v) : v);
3212
+ }
3213
+ function reconcileChildren(parentPath, rawParent) {
3214
+ const set = children2.get(parentPath);
3215
+ if (!set) return;
3216
+ for (const childPath of set) {
3217
+ const key = lastSegment(childPath);
3218
+ const nv = rawParent[key];
3219
+ const pair = signals.get(childPath);
3220
+ if (pair) setLiteral(pair, nv);
3221
+ if (nv != null && typeof nv === "object") {
3222
+ reconcileChildren(childPath, nv);
3223
+ } else {
3224
+ invalidateChildren(childPath);
3225
+ }
3226
+ }
3227
+ }
3093
3228
  function wrap(raw, basePath) {
3094
3229
  if (!shouldWrap(raw)) return raw;
3095
- const cached = proxyCache.get(raw);
3096
- if (cached) return cached;
3230
+ let byPath = proxyCache.get(raw);
3231
+ if (byPath) {
3232
+ const hit = byPath.get(basePath);
3233
+ if (hit) return hit;
3234
+ }
3097
3235
  const isArr = Array.isArray(raw);
3098
3236
  const basePrefix = basePath ? basePath + "." : "";
3099
- let lastKey = "";
3100
- let lastSignal;
3101
3237
  const proxy = new Proxy(raw, {
3102
3238
  // -------------------------------------------------------------------
3103
3239
  // GET
@@ -3118,12 +3254,10 @@ var FormaJS = (() => {
3118
3254
  (a) => a != null && typeof a === "object" && a[RAW] ? a[RAW] : a
3119
3255
  );
3120
3256
  result = target[key].apply(target, rawArgs);
3121
- invalidateChildren(basePath);
3122
- const [, setLen] = getSignal(
3123
- basePrefix + "length",
3124
- target.length
3125
- );
3126
- setLen(target.length);
3257
+ reconcileChildren(basePath, target);
3258
+ const lenPair = signals.get(basePrefix + "length");
3259
+ if (lenPair) lenPair[1](target.length);
3260
+ if (basePath) bumpArrayVersion(basePath);
3127
3261
  });
3128
3262
  return result;
3129
3263
  };
@@ -3134,15 +3268,11 @@ var FormaJS = (() => {
3134
3268
  return target.length;
3135
3269
  }
3136
3270
  const value2 = Reflect.get(target, prop);
3137
- let pair;
3138
- if (key === lastKey && lastSignal) {
3139
- pair = lastSignal;
3140
- } else {
3141
- pair = getSignal(childPath, value2);
3142
- lastKey = key;
3143
- lastSignal = pair;
3144
- }
3271
+ const pair = getSignal(childPath, value2);
3145
3272
  pair[0]();
3273
+ if (Array.isArray(value2)) {
3274
+ getArrayVersion(childPath)[0]();
3275
+ }
3146
3276
  if (shouldWrap(value2)) {
3147
3277
  return wrap(value2, childPath);
3148
3278
  }
@@ -3158,9 +3288,11 @@ var FormaJS = (() => {
3158
3288
  const key = String(prop);
3159
3289
  const childPath = basePrefix + key;
3160
3290
  const rawValue = value2 != null && typeof value2 === "object" && value2[RAW] ? value2[RAW] : value2;
3291
+ const oldRaw = Reflect.get(target, prop);
3161
3292
  Reflect.set(target, prop, rawValue);
3162
- if (rawValue != null && typeof rawValue === "object") {
3293
+ if (rawValue != null && typeof rawValue === "object" && oldRaw !== rawValue) {
3163
3294
  invalidateChildren(childPath);
3295
+ evictProxy(oldRaw, childPath);
3164
3296
  }
3165
3297
  if (isArr && key !== "length") {
3166
3298
  const lengthPath = basePrefix + "length";
@@ -3169,6 +3301,12 @@ var FormaJS = (() => {
3169
3301
  lenPair[1](target.length);
3170
3302
  }
3171
3303
  }
3304
+ if (isArr && key === "length") {
3305
+ batch(() => {
3306
+ reconcileChildren(basePath, target);
3307
+ if (basePath) bumpArrayVersion(basePath);
3308
+ });
3309
+ }
3172
3310
  const [, setter2] = getSignal(childPath, rawValue);
3173
3311
  setter2(rawValue);
3174
3312
  return true;
@@ -3207,24 +3345,30 @@ var FormaJS = (() => {
3207
3345
  }
3208
3346
  const key = String(prop);
3209
3347
  const childPath = basePrefix + key;
3348
+ const oldRaw = Reflect.get(target, prop);
3210
3349
  const result = Reflect.deleteProperty(target, prop);
3350
+ const delPair = signals.get(childPath);
3351
+ if (delPair) delPair[1](void 0);
3352
+ evictProxy(oldRaw, childPath);
3211
3353
  invalidateChildren(childPath);
3212
- signals.delete(childPath);
3213
- const parentPath = basePath;
3214
- if (parentPath !== void 0) {
3215
- const parentSet = children2.get(parentPath);
3216
- if (parentSet) {
3217
- parentSet.delete(childPath);
3218
- if (parentSet.size === 0) children2.delete(parentPath);
3219
- }
3220
- }
3221
- children2.delete(childPath);
3222
3354
  return result;
3223
3355
  }
3224
3356
  });
3225
- proxyCache.set(raw, proxy);
3357
+ if (!byPath) {
3358
+ byPath = /* @__PURE__ */ new Map();
3359
+ proxyCache.set(raw, byPath);
3360
+ }
3361
+ byPath.set(basePath, proxy);
3226
3362
  return proxy;
3227
3363
  }
3364
+ function evictProxy(oldRaw, path) {
3365
+ if (oldRaw == null || typeof oldRaw !== "object") return;
3366
+ const om = proxyCache.get(oldRaw);
3367
+ if (om) {
3368
+ om.delete(path);
3369
+ if (om.size === 0) proxyCache.delete(oldRaw);
3370
+ }
3371
+ }
3228
3372
  const rootProxy = wrap(initial, "");
3229
3373
  function getCurrentSnapshot() {
3230
3374
  return untrack(() => deepClone(initial));
@@ -3241,10 +3385,24 @@ var FormaJS = (() => {
3241
3385
  }
3242
3386
 
3243
3387
  // src/state/history.ts
3388
+ function cloneEntry(v, seen) {
3389
+ if (v === null || typeof v !== "object") return v;
3390
+ const proto = Object.getPrototypeOf(v);
3391
+ if (!Array.isArray(v) && proto !== Object.prototype && proto !== null) return v;
3392
+ if (!seen) seen = /* @__PURE__ */ new WeakSet();
3393
+ if (seen.has(v)) return v;
3394
+ seen.add(v);
3395
+ if (Array.isArray(v)) return v.map((i) => cloneEntry(i, seen));
3396
+ const out = {};
3397
+ for (const k of Object.keys(v)) {
3398
+ out[k] = cloneEntry(v[k], seen);
3399
+ }
3400
+ return out;
3401
+ }
3244
3402
  function createHistory(source, options) {
3245
3403
  const [sourceGet, sourceSet] = source;
3246
- const maxLength = options?.maxLength ?? 100;
3247
- let _stack = [sourceGet()];
3404
+ const maxLength = Math.max(1, options?.maxLength ?? 100);
3405
+ let _stack = [cloneEntry(sourceGet())];
3248
3406
  let _cursor = 0;
3249
3407
  const [stackSignal, setStackSignal] = createSignal([..._stack]);
3250
3408
  const [cursorSignal, setCursorSignal] = createSignal(_cursor);
@@ -3256,45 +3414,52 @@ var FormaJS = (() => {
3256
3414
  setStackLenSignal(_stack.length);
3257
3415
  });
3258
3416
  }
3259
- let ignoreNext = false;
3417
+ const NONE = /* @__PURE__ */ Symbol("none");
3418
+ let _expected = NONE;
3260
3419
  let isFirstRun = true;
3261
- internalEffect(() => {
3420
+ const disposeEffect = internalEffect(() => {
3262
3421
  const value2 = sourceGet();
3263
3422
  if (isFirstRun) {
3264
3423
  isFirstRun = false;
3265
3424
  return;
3266
3425
  }
3267
- if (ignoreNext) {
3268
- ignoreNext = false;
3426
+ if (_expected !== NONE && Object.is(value2, _expected)) {
3427
+ _expected = NONE;
3269
3428
  return;
3270
3429
  }
3430
+ _expected = NONE;
3271
3431
  _stack = _stack.slice(0, _cursor + 1);
3272
- _stack.push(value2);
3432
+ _stack.push(cloneEntry(value2));
3273
3433
  if (_stack.length > maxLength) {
3274
3434
  _stack.splice(0, _stack.length - maxLength);
3275
3435
  }
3276
3436
  _cursor = _stack.length - 1;
3277
3437
  syncSignals();
3278
3438
  });
3439
+ const destroy = () => {
3440
+ disposeEffect();
3441
+ };
3279
3442
  const canUndo = () => cursorSignal() > 0;
3280
3443
  const canRedo = () => cursorSignal() < stackLenSignal() - 1;
3281
3444
  const undo = () => {
3282
3445
  if (_cursor <= 0) return;
3283
3446
  _cursor--;
3284
- ignoreNext = true;
3285
- sourceSet(_stack[_cursor]);
3447
+ const restored = cloneEntry(_stack[_cursor]);
3448
+ _expected = restored;
3449
+ sourceSet(restored);
3286
3450
  syncSignals();
3287
3451
  };
3288
3452
  const redo = () => {
3289
3453
  if (_cursor >= _stack.length - 1) return;
3290
3454
  _cursor++;
3291
- ignoreNext = true;
3292
- sourceSet(_stack[_cursor]);
3455
+ const restored = cloneEntry(_stack[_cursor]);
3456
+ _expected = restored;
3457
+ sourceSet(restored);
3293
3458
  syncSignals();
3294
3459
  };
3295
3460
  const clear = () => {
3296
3461
  const currentValue = sourceGet();
3297
- _stack = [currentValue];
3462
+ _stack = [cloneEntry(currentValue)];
3298
3463
  _cursor = 0;
3299
3464
  syncSignals();
3300
3465
  };
@@ -3305,35 +3470,91 @@ var FormaJS = (() => {
3305
3470
  canRedo,
3306
3471
  history: () => stackSignal(),
3307
3472
  cursor: () => cursorSignal(),
3308
- clear
3473
+ clear,
3474
+ destroy
3309
3475
  };
3310
3476
  }
3311
3477
 
3312
3478
  // src/state/persist.ts
3479
+ var ENVELOPE_TAG = "$forma:v";
3313
3480
  function persist(source, key, options) {
3314
3481
  const [sourceGet, sourceSet] = source;
3315
3482
  const storage = options?.storage ?? globalThis.localStorage;
3316
3483
  const serialize = options?.serialize ?? JSON.stringify;
3317
3484
  const deserialize = options?.deserialize ?? JSON.parse;
3318
3485
  const validate = options?.validate;
3319
- try {
3320
- const stored = storage.getItem(key);
3321
- if (stored !== null) {
3322
- const value2 = deserialize(stored);
3486
+ const version = options?.version;
3487
+ const migrate = options?.migrate;
3488
+ const onError2 = options?.onError;
3489
+ let writing = false;
3490
+ function unwrap(stored) {
3491
+ const parsed = deserialize(stored);
3492
+ if (parsed != null && typeof parsed === "object" && Object.prototype.hasOwnProperty.call(parsed, ENVELOPE_TAG)) {
3493
+ const env = parsed;
3494
+ return { value: env.value, version: Number(env[ENVELOPE_TAG]) };
3495
+ }
3496
+ return { value: parsed, version: 0 };
3497
+ }
3498
+ function hydrate() {
3499
+ let stored;
3500
+ try {
3501
+ stored = storage.getItem(key);
3502
+ } catch (err) {
3503
+ onError2?.(err, "hydrate");
3504
+ return;
3505
+ }
3506
+ if (stored === null) return;
3507
+ try {
3508
+ const { value: raw, version: storedVersion } = unwrap(stored);
3509
+ let value2 = raw;
3510
+ if (version !== void 0 && storedVersion < version) {
3511
+ if (!migrate) return;
3512
+ try {
3513
+ value2 = migrate(raw, storedVersion);
3514
+ } catch (err) {
3515
+ onError2?.(err, "migrate");
3516
+ return;
3517
+ }
3518
+ }
3323
3519
  if (!validate || validate(value2)) {
3324
- sourceSet(value2);
3520
+ writing = true;
3521
+ try {
3522
+ sourceSet(value2);
3523
+ } finally {
3524
+ writing = false;
3525
+ }
3325
3526
  }
3527
+ } catch (err) {
3528
+ onError2?.(err, "hydrate");
3326
3529
  }
3327
- } catch {
3328
3530
  }
3329
- internalEffect(() => {
3531
+ hydrate();
3532
+ const stopEffect = internalEffect(() => {
3330
3533
  const value2 = sourceGet();
3534
+ if (writing) return;
3331
3535
  try {
3332
- const serialized = serialize(value2);
3536
+ const serialized = version !== void 0 ? serialize({ [ENVELOPE_TAG]: version, value: value2 }) : serialize(value2);
3333
3537
  storage.setItem(key, serialized);
3334
- } catch {
3538
+ } catch (err) {
3539
+ onError2?.(err, err?.name === "QuotaExceededError" ? "write" : "serialize");
3335
3540
  }
3336
3541
  });
3542
+ const enableSync = options?.syncTabs ?? (typeof window !== "undefined" && storage === globalThis.localStorage);
3543
+ let onStorage;
3544
+ if (enableSync && typeof window !== "undefined") {
3545
+ onStorage = (e) => {
3546
+ if (e.storageArea !== storage) return;
3547
+ if (e.key !== null && e.key !== key) return;
3548
+ hydrate();
3549
+ };
3550
+ window.addEventListener("storage", onStorage);
3551
+ }
3552
+ return () => {
3553
+ stopEffect();
3554
+ if (onStorage && typeof window !== "undefined") {
3555
+ window.removeEventListener("storage", onStorage);
3556
+ }
3557
+ };
3337
3558
  }
3338
3559
 
3339
3560
  // src/events/bus.ts