@getforma/core 1.1.0 → 1.3.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 (45) hide show
  1. package/dist/{chunk-MIOMT2CB.js → chunk-AJP4OVCN.js} +29 -9
  2. package/dist/chunk-AJP4OVCN.js.map +1 -0
  3. package/dist/{chunk-XLVBYXOU.js → chunk-ETPJTPYD.js} +6 -3
  4. package/dist/chunk-ETPJTPYD.js.map +1 -0
  5. package/dist/{chunk-JRQNDXX7.cjs → chunk-FBM7V4NE.cjs} +68 -47
  6. package/dist/chunk-FBM7V4NE.cjs.map +1 -0
  7. package/dist/{chunk-W7OUWVRA.cjs → chunk-H7MDUHS5.cjs} +6 -2
  8. package/dist/chunk-H7MDUHS5.cjs.map +1 -0
  9. package/dist/{chunk-2Y5US35K.cjs → chunk-KUXNZ5MG.cjs} +12 -12
  10. package/dist/{chunk-2Y5US35K.cjs.map → chunk-KUXNZ5MG.cjs.map} +1 -1
  11. package/dist/{chunk-INNOI6TG.js → chunk-Y3A2EVVS.js} +3 -3
  12. package/dist/{chunk-INNOI6TG.js.map → chunk-Y3A2EVVS.js.map} +1 -1
  13. package/dist/forma-runtime-csp.js +178 -79
  14. package/dist/forma-runtime.js +178 -79
  15. package/dist/formajs-runtime-hardened.global.js +178 -79
  16. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  17. package/dist/formajs-runtime.global.js +178 -79
  18. package/dist/formajs-runtime.global.js.map +1 -1
  19. package/dist/formajs.global.js +245 -61
  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 +307 -143
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +34 -2
  26. package/dist/index.d.ts +34 -2
  27. package/dist/index.js +248 -88
  28. package/dist/index.js.map +1 -1
  29. package/dist/runtime-hardened.cjs +178 -79
  30. package/dist/runtime-hardened.cjs.map +1 -1
  31. package/dist/runtime-hardened.js +178 -79
  32. package/dist/runtime-hardened.js.map +1 -1
  33. package/dist/runtime.cjs +198 -103
  34. package/dist/runtime.cjs.map +1 -1
  35. package/dist/runtime.js +174 -79
  36. package/dist/runtime.js.map +1 -1
  37. package/dist/server.cjs +7 -7
  38. package/dist/server.js +2 -2
  39. package/dist/tc39-compat.cjs +3 -3
  40. package/dist/tc39-compat.js +1 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-JRQNDXX7.cjs.map +0 -1
  43. package/dist/chunk-MIOMT2CB.js.map +0 -1
  44. package/dist/chunk-W7OUWVRA.cjs.map +0 -1
  45. package/dist/chunk-XLVBYXOU.js.map +0 -1
@@ -86,6 +86,7 @@ var FormaJS = (() => {
86
86
  setStyle: () => setStyle,
87
87
  setText: () => setText,
88
88
  siblings: () => siblings,
89
+ svg: () => svg,
89
90
  template: () => template,
90
91
  templateMany: () => templateMany,
91
92
  toggleClass: () => toggleClass,
@@ -625,6 +626,9 @@ var FormaJS = (() => {
625
626
 
626
627
  // src/reactive/signal.ts
627
628
  var signalNames = /* @__PURE__ */ new WeakMap();
629
+ function value(v) {
630
+ return () => v;
631
+ }
628
632
  function applySignalSet(s, v, equals) {
629
633
  if (typeof v !== "function") {
630
634
  if (!equals) {
@@ -1283,14 +1287,18 @@ var FormaJS = (() => {
1283
1287
  }
1284
1288
  const oldKeyMap = /* @__PURE__ */ new Map();
1285
1289
  for (let i = 0; i < oldLen; i++) {
1286
- oldKeyMap.set(keyFn(oldItems[i]), i);
1290
+ const k = keyFn(oldItems[i]);
1291
+ const bucket = oldKeyMap.get(k);
1292
+ if (bucket) bucket.push(i);
1293
+ else oldKeyMap.set(k, [i]);
1287
1294
  }
1288
1295
  const oldIndices = new Array(newLen);
1289
1296
  const oldUsed = new Uint8Array(oldLen);
1290
1297
  for (let i = 0; i < newLen; i++) {
1291
1298
  const key = keyFn(newItems[i]);
1292
- const oldIdx = oldKeyMap.get(key);
1293
- if (oldIdx !== void 0) {
1299
+ const bucket = oldKeyMap.get(key);
1300
+ if (bucket && bucket.length > 0) {
1301
+ const oldIdx = bucket.shift();
1294
1302
  oldIndices[i] = oldIdx;
1295
1303
  oldUsed[oldIdx] = 1;
1296
1304
  } else {
@@ -2074,6 +2082,17 @@ var FormaJS = (() => {
2074
2082
  var Fragment = /* @__PURE__ */ Symbol.for("forma.fragment");
2075
2083
  var SVG_NS = "http://www.w3.org/2000/svg";
2076
2084
  var XLINK_NS = "http://www.w3.org/1999/xlink";
2085
+ var DUAL_USE_SVG_TAGS = /* @__PURE__ */ new Set(["a", "title", "script", "style", "font"]);
2086
+ var currentNamespace = null;
2087
+ function svg(build) {
2088
+ const prev = currentNamespace;
2089
+ currentNamespace = SVG_NS;
2090
+ try {
2091
+ return build();
2092
+ } finally {
2093
+ currentNamespace = prev;
2094
+ }
2095
+ }
2077
2096
  var SVG_TAGS = /* @__PURE__ */ new Set([
2078
2097
  "svg",
2079
2098
  "path",
@@ -2607,7 +2626,12 @@ var FormaJS = (() => {
2607
2626
  return { type: "element", tag: tagName, props: props ?? null, children: children2 };
2608
2627
  }
2609
2628
  let el;
2610
- if (ELEMENT_PROTOS && ELEMENT_PROTOS[tagName]) {
2629
+ const svgCtx = currentNamespace === SVG_NS;
2630
+ if (svgCtx && (SVG_TAGS.has(tagName) || DUAL_USE_SVG_TAGS.has(tagName))) {
2631
+ el = document.createElementNS(SVG_NS, tagName);
2632
+ } else if (!svgCtx && DUAL_USE_SVG_TAGS.has(tagName)) {
2633
+ el = getProto(tagName).cloneNode(false);
2634
+ } else if (ELEMENT_PROTOS && ELEMENT_PROTOS[tagName]) {
2611
2635
  el = ELEMENT_PROTOS[tagName].cloneNode(false);
2612
2636
  } else if (SVG_TAGS.has(tagName)) {
2613
2637
  el = document.createElementNS(SVG_NS, tagName);
@@ -2873,8 +2897,14 @@ var FormaJS = (() => {
2873
2897
  const isPending = pending() > 0;
2874
2898
  const newNode = isPending ? fallbackNode ??= fallback() : resolvedNode;
2875
2899
  if (newNode === currentNode) return;
2876
- if (currentNode && currentNode.parentNode === parent2) {
2877
- parent2.removeChild(currentNode);
2900
+ if (currentNode) {
2901
+ if (currentNode.parentNode === parent2) {
2902
+ parent2.removeChild(currentNode);
2903
+ } else if (currentNode.nodeType === 11) {
2904
+ while (startMarker.nextSibling && startMarker.nextSibling !== endMarker) {
2905
+ currentNode.appendChild(startMarker.nextSibling);
2906
+ }
2907
+ }
2878
2908
  }
2879
2909
  if (newNode) {
2880
2910
  parent2.insertBefore(newNode, endMarker);
@@ -2930,6 +2960,10 @@ var FormaJS = (() => {
2930
2960
  const sharedProps = scriptBlock ? JSON.parse(scriptBlock.textContent) : null;
2931
2961
  const islands = document.querySelectorAll("[data-forma-island]");
2932
2962
  for (const root of islands) {
2963
+ const status = root.getAttribute("data-forma-status");
2964
+ if (status === "active" || status === "hydrating" || status === "disposed" || status === "error") continue;
2965
+ if (root.__formaScheduled) continue;
2966
+ delete root.__formaDisposed;
2933
2967
  const id = parseInt(root.getAttribute("data-forma-island"), 10);
2934
2968
  const componentName = root.getAttribute("data-forma-component");
2935
2969
  const hydrateFn = registry[componentName];
@@ -2940,30 +2974,39 @@ var FormaJS = (() => {
2940
2974
  }
2941
2975
  const trigger2 = root.getAttribute("data-forma-hydrate") || "load";
2942
2976
  if (trigger2 === "visible") {
2977
+ root.__formaScheduled = true;
2943
2978
  const observer = new IntersectionObserver(
2944
2979
  (entries) => {
2945
2980
  for (const entry of entries) {
2946
2981
  if (!entry.isIntersecting) continue;
2947
2982
  observer.disconnect();
2983
+ delete root.__formaObserver;
2948
2984
  hydrateIslandRoot(root, id, componentName, hydrateFn, sharedProps);
2949
2985
  }
2950
2986
  },
2951
2987
  { rootMargin: "200px" }
2952
2988
  );
2989
+ root.__formaObserver = observer;
2953
2990
  observer.observe(root);
2954
2991
  } else if (trigger2 === "idle") {
2992
+ root.__formaScheduled = true;
2955
2993
  const hydrate = () => hydrateIslandRoot(root, id, componentName, hydrateFn, sharedProps);
2956
2994
  if (typeof requestIdleCallback === "function") {
2957
- requestIdleCallback(hydrate);
2995
+ const handle = requestIdleCallback(hydrate);
2996
+ root.__formaIdleCancel = () => cancelIdleCallback(handle);
2958
2997
  } else {
2959
- setTimeout(hydrate, 200);
2998
+ const handle = setTimeout(hydrate, 200);
2999
+ root.__formaIdleCancel = () => clearTimeout(handle);
2960
3000
  }
2961
3001
  } else if (trigger2 === "interaction") {
3002
+ root.__formaScheduled = true;
2962
3003
  const hydrate = () => {
2963
3004
  root.removeEventListener("pointerdown", hydrate, true);
2964
3005
  root.removeEventListener("focusin", hydrate, true);
3006
+ delete root.__formaInteractionHandler;
2965
3007
  hydrateIslandRoot(root, id, componentName, hydrateFn, sharedProps);
2966
3008
  };
3009
+ root.__formaInteractionHandler = hydrate;
2967
3010
  root.addEventListener("pointerdown", hydrate, { capture: true, once: true });
2968
3011
  root.addEventListener("focusin", hydrate, { capture: true, once: true });
2969
3012
  } else {
@@ -2972,6 +3015,24 @@ var FormaJS = (() => {
2972
3015
  }
2973
3016
  }
2974
3017
  function deactivateIsland(el) {
3018
+ const observer = el.__formaObserver;
3019
+ if (observer) {
3020
+ observer.disconnect();
3021
+ delete el.__formaObserver;
3022
+ }
3023
+ const interactionHandler = el.__formaInteractionHandler;
3024
+ if (interactionHandler) {
3025
+ el.removeEventListener("pointerdown", interactionHandler, true);
3026
+ el.removeEventListener("focusin", interactionHandler, true);
3027
+ delete el.__formaInteractionHandler;
3028
+ }
3029
+ const idleCancel = el.__formaIdleCancel;
3030
+ if (idleCancel) {
3031
+ idleCancel();
3032
+ delete el.__formaIdleCancel;
3033
+ }
3034
+ delete el.__formaScheduled;
3035
+ el.__formaDisposed = true;
2975
3036
  const dispose = el.__formaDispose;
2976
3037
  if (typeof dispose === "function") {
2977
3038
  dispose();
@@ -2980,13 +3041,15 @@ var FormaJS = (() => {
2980
3041
  }
2981
3042
  }
2982
3043
  function deactivateAllIslands(root = document) {
2983
- const islands = root.querySelectorAll('[data-forma-status="active"]');
3044
+ const islands = root.querySelectorAll("[data-forma-island]");
2984
3045
  for (const island of islands) {
2985
3046
  deactivateIsland(island);
2986
3047
  }
2987
3048
  }
2988
3049
  function hydrateIslandRoot(root, id, componentName, hydrateFn, sharedProps) {
3050
+ if (root.__formaDisposed) return;
2989
3051
  try {
3052
+ delete root.__formaScheduled;
2990
3053
  const props = loadIslandProps(root, id, sharedProps);
2991
3054
  root.setAttribute("data-forma-status", "hydrating");
2992
3055
  let activeRoot = root;
@@ -3156,6 +3219,19 @@ var FormaJS = (() => {
3156
3219
  function createStore(initial) {
3157
3220
  const signals = /* @__PURE__ */ new Map();
3158
3221
  const children2 = /* @__PURE__ */ new Map();
3222
+ const arrayVersions = /* @__PURE__ */ new Map();
3223
+ function getArrayVersion(path) {
3224
+ let p = arrayVersions.get(path);
3225
+ if (!p) {
3226
+ p = createSignal(0);
3227
+ arrayVersions.set(path, p);
3228
+ }
3229
+ return p;
3230
+ }
3231
+ function bumpArrayVersion(path) {
3232
+ const p = arrayVersions.get(path);
3233
+ if (p) p[1]((n) => n + 1);
3234
+ }
3159
3235
  function registerChild(path) {
3160
3236
  const lastDot = path.lastIndexOf(".");
3161
3237
  if (lastDot === -1) return;
@@ -3187,14 +3263,37 @@ var FormaJS = (() => {
3187
3263
  }
3188
3264
  childSet.clear();
3189
3265
  }
3266
+ function lastSegment(path) {
3267
+ const d = path.lastIndexOf(".");
3268
+ return d === -1 ? path : path.substring(d + 1);
3269
+ }
3270
+ function setLiteral(pair, v) {
3271
+ pair[1](typeof v === "function" ? value(v) : v);
3272
+ }
3273
+ function reconcileChildren(parentPath, rawParent) {
3274
+ const set = children2.get(parentPath);
3275
+ if (!set) return;
3276
+ for (const childPath of set) {
3277
+ const key = lastSegment(childPath);
3278
+ const nv = rawParent[key];
3279
+ const pair = signals.get(childPath);
3280
+ if (pair) setLiteral(pair, nv);
3281
+ if (nv != null && typeof nv === "object") {
3282
+ reconcileChildren(childPath, nv);
3283
+ } else {
3284
+ invalidateChildren(childPath);
3285
+ }
3286
+ }
3287
+ }
3190
3288
  function wrap(raw, basePath) {
3191
3289
  if (!shouldWrap(raw)) return raw;
3192
- const cached = proxyCache.get(raw);
3193
- if (cached) return cached;
3290
+ let byPath = proxyCache.get(raw);
3291
+ if (byPath) {
3292
+ const hit = byPath.get(basePath);
3293
+ if (hit) return hit;
3294
+ }
3194
3295
  const isArr = Array.isArray(raw);
3195
3296
  const basePrefix = basePath ? basePath + "." : "";
3196
- let lastKey = "";
3197
- let lastSignal;
3198
3297
  const proxy = new Proxy(raw, {
3199
3298
  // -------------------------------------------------------------------
3200
3299
  // GET
@@ -3215,12 +3314,10 @@ var FormaJS = (() => {
3215
3314
  (a) => a != null && typeof a === "object" && a[RAW] ? a[RAW] : a
3216
3315
  );
3217
3316
  result = target[key].apply(target, rawArgs);
3218
- invalidateChildren(basePath);
3219
- const [, setLen] = getSignal(
3220
- basePrefix + "length",
3221
- target.length
3222
- );
3223
- setLen(target.length);
3317
+ reconcileChildren(basePath, target);
3318
+ const lenPair = signals.get(basePrefix + "length");
3319
+ if (lenPair) lenPair[1](target.length);
3320
+ if (basePath) bumpArrayVersion(basePath);
3224
3321
  });
3225
3322
  return result;
3226
3323
  };
@@ -3231,15 +3328,11 @@ var FormaJS = (() => {
3231
3328
  return target.length;
3232
3329
  }
3233
3330
  const value2 = Reflect.get(target, prop);
3234
- let pair;
3235
- if (key === lastKey && lastSignal) {
3236
- pair = lastSignal;
3237
- } else {
3238
- pair = getSignal(childPath, value2);
3239
- lastKey = key;
3240
- lastSignal = pair;
3241
- }
3331
+ const pair = getSignal(childPath, value2);
3242
3332
  pair[0]();
3333
+ if (Array.isArray(value2)) {
3334
+ getArrayVersion(childPath)[0]();
3335
+ }
3243
3336
  if (shouldWrap(value2)) {
3244
3337
  return wrap(value2, childPath);
3245
3338
  }
@@ -3255,9 +3348,11 @@ var FormaJS = (() => {
3255
3348
  const key = String(prop);
3256
3349
  const childPath = basePrefix + key;
3257
3350
  const rawValue = value2 != null && typeof value2 === "object" && value2[RAW] ? value2[RAW] : value2;
3351
+ const oldRaw = Reflect.get(target, prop);
3258
3352
  Reflect.set(target, prop, rawValue);
3259
- if (rawValue != null && typeof rawValue === "object") {
3353
+ if (rawValue != null && typeof rawValue === "object" && oldRaw !== rawValue) {
3260
3354
  invalidateChildren(childPath);
3355
+ evictProxy(oldRaw, childPath);
3261
3356
  }
3262
3357
  if (isArr && key !== "length") {
3263
3358
  const lengthPath = basePrefix + "length";
@@ -3266,6 +3361,12 @@ var FormaJS = (() => {
3266
3361
  lenPair[1](target.length);
3267
3362
  }
3268
3363
  }
3364
+ if (isArr && key === "length") {
3365
+ batch(() => {
3366
+ reconcileChildren(basePath, target);
3367
+ if (basePath) bumpArrayVersion(basePath);
3368
+ });
3369
+ }
3269
3370
  const [, setter2] = getSignal(childPath, rawValue);
3270
3371
  setter2(rawValue);
3271
3372
  return true;
@@ -3304,24 +3405,30 @@ var FormaJS = (() => {
3304
3405
  }
3305
3406
  const key = String(prop);
3306
3407
  const childPath = basePrefix + key;
3408
+ const oldRaw = Reflect.get(target, prop);
3307
3409
  const result = Reflect.deleteProperty(target, prop);
3410
+ const delPair = signals.get(childPath);
3411
+ if (delPair) delPair[1](void 0);
3412
+ evictProxy(oldRaw, childPath);
3308
3413
  invalidateChildren(childPath);
3309
- signals.delete(childPath);
3310
- const parentPath = basePath;
3311
- if (parentPath !== void 0) {
3312
- const parentSet = children2.get(parentPath);
3313
- if (parentSet) {
3314
- parentSet.delete(childPath);
3315
- if (parentSet.size === 0) children2.delete(parentPath);
3316
- }
3317
- }
3318
- children2.delete(childPath);
3319
3414
  return result;
3320
3415
  }
3321
3416
  });
3322
- proxyCache.set(raw, proxy);
3417
+ if (!byPath) {
3418
+ byPath = /* @__PURE__ */ new Map();
3419
+ proxyCache.set(raw, byPath);
3420
+ }
3421
+ byPath.set(basePath, proxy);
3323
3422
  return proxy;
3324
3423
  }
3424
+ function evictProxy(oldRaw, path) {
3425
+ if (oldRaw == null || typeof oldRaw !== "object") return;
3426
+ const om = proxyCache.get(oldRaw);
3427
+ if (om) {
3428
+ om.delete(path);
3429
+ if (om.size === 0) proxyCache.delete(oldRaw);
3430
+ }
3431
+ }
3325
3432
  const rootProxy = wrap(initial, "");
3326
3433
  function getCurrentSnapshot() {
3327
3434
  return untrack(() => deepClone(initial));
@@ -3338,10 +3445,24 @@ var FormaJS = (() => {
3338
3445
  }
3339
3446
 
3340
3447
  // src/state/history.ts
3448
+ function cloneEntry(v, seen) {
3449
+ if (v === null || typeof v !== "object") return v;
3450
+ const proto = Object.getPrototypeOf(v);
3451
+ if (!Array.isArray(v) && proto !== Object.prototype && proto !== null) return v;
3452
+ if (!seen) seen = /* @__PURE__ */ new WeakSet();
3453
+ if (seen.has(v)) return v;
3454
+ seen.add(v);
3455
+ if (Array.isArray(v)) return v.map((i) => cloneEntry(i, seen));
3456
+ const out = {};
3457
+ for (const k of Object.keys(v)) {
3458
+ out[k] = cloneEntry(v[k], seen);
3459
+ }
3460
+ return out;
3461
+ }
3341
3462
  function createHistory(source, options) {
3342
3463
  const [sourceGet, sourceSet] = source;
3343
- const maxLength = options?.maxLength ?? 100;
3344
- let _stack = [sourceGet()];
3464
+ const maxLength = Math.max(1, options?.maxLength ?? 100);
3465
+ let _stack = [cloneEntry(sourceGet())];
3345
3466
  let _cursor = 0;
3346
3467
  const [stackSignal, setStackSignal] = createSignal([..._stack]);
3347
3468
  const [cursorSignal, setCursorSignal] = createSignal(_cursor);
@@ -3353,45 +3474,52 @@ var FormaJS = (() => {
3353
3474
  setStackLenSignal(_stack.length);
3354
3475
  });
3355
3476
  }
3356
- let ignoreNext = false;
3477
+ const NONE = /* @__PURE__ */ Symbol("none");
3478
+ let _expected = NONE;
3357
3479
  let isFirstRun = true;
3358
- internalEffect(() => {
3480
+ const disposeEffect = internalEffect(() => {
3359
3481
  const value2 = sourceGet();
3360
3482
  if (isFirstRun) {
3361
3483
  isFirstRun = false;
3362
3484
  return;
3363
3485
  }
3364
- if (ignoreNext) {
3365
- ignoreNext = false;
3486
+ if (_expected !== NONE && Object.is(value2, _expected)) {
3487
+ _expected = NONE;
3366
3488
  return;
3367
3489
  }
3490
+ _expected = NONE;
3368
3491
  _stack = _stack.slice(0, _cursor + 1);
3369
- _stack.push(value2);
3492
+ _stack.push(cloneEntry(value2));
3370
3493
  if (_stack.length > maxLength) {
3371
3494
  _stack.splice(0, _stack.length - maxLength);
3372
3495
  }
3373
3496
  _cursor = _stack.length - 1;
3374
3497
  syncSignals();
3375
3498
  });
3499
+ const destroy = () => {
3500
+ disposeEffect();
3501
+ };
3376
3502
  const canUndo = () => cursorSignal() > 0;
3377
3503
  const canRedo = () => cursorSignal() < stackLenSignal() - 1;
3378
3504
  const undo = () => {
3379
3505
  if (_cursor <= 0) return;
3380
3506
  _cursor--;
3381
- ignoreNext = true;
3382
- sourceSet(_stack[_cursor]);
3507
+ const restored = cloneEntry(_stack[_cursor]);
3508
+ _expected = restored;
3509
+ sourceSet(restored);
3383
3510
  syncSignals();
3384
3511
  };
3385
3512
  const redo = () => {
3386
3513
  if (_cursor >= _stack.length - 1) return;
3387
3514
  _cursor++;
3388
- ignoreNext = true;
3389
- sourceSet(_stack[_cursor]);
3515
+ const restored = cloneEntry(_stack[_cursor]);
3516
+ _expected = restored;
3517
+ sourceSet(restored);
3390
3518
  syncSignals();
3391
3519
  };
3392
3520
  const clear = () => {
3393
3521
  const currentValue = sourceGet();
3394
- _stack = [currentValue];
3522
+ _stack = [cloneEntry(currentValue)];
3395
3523
  _cursor = 0;
3396
3524
  syncSignals();
3397
3525
  };
@@ -3402,35 +3530,91 @@ var FormaJS = (() => {
3402
3530
  canRedo,
3403
3531
  history: () => stackSignal(),
3404
3532
  cursor: () => cursorSignal(),
3405
- clear
3533
+ clear,
3534
+ destroy
3406
3535
  };
3407
3536
  }
3408
3537
 
3409
3538
  // src/state/persist.ts
3539
+ var ENVELOPE_TAG = "$forma:v";
3410
3540
  function persist(source, key, options) {
3411
3541
  const [sourceGet, sourceSet] = source;
3412
3542
  const storage = options?.storage ?? globalThis.localStorage;
3413
3543
  const serialize = options?.serialize ?? JSON.stringify;
3414
3544
  const deserialize = options?.deserialize ?? JSON.parse;
3415
3545
  const validate = options?.validate;
3416
- try {
3417
- const stored = storage.getItem(key);
3418
- if (stored !== null) {
3419
- const value2 = deserialize(stored);
3546
+ const version = options?.version;
3547
+ const migrate = options?.migrate;
3548
+ const onError2 = options?.onError;
3549
+ let writing = false;
3550
+ function unwrap(stored) {
3551
+ const parsed = deserialize(stored);
3552
+ if (parsed != null && typeof parsed === "object" && Object.prototype.hasOwnProperty.call(parsed, ENVELOPE_TAG)) {
3553
+ const env = parsed;
3554
+ return { value: env.value, version: Number(env[ENVELOPE_TAG]) };
3555
+ }
3556
+ return { value: parsed, version: 0 };
3557
+ }
3558
+ function hydrate() {
3559
+ let stored;
3560
+ try {
3561
+ stored = storage.getItem(key);
3562
+ } catch (err) {
3563
+ onError2?.(err, "hydrate");
3564
+ return;
3565
+ }
3566
+ if (stored === null) return;
3567
+ try {
3568
+ const { value: raw, version: storedVersion } = unwrap(stored);
3569
+ let value2 = raw;
3570
+ if (version !== void 0 && storedVersion < version) {
3571
+ if (!migrate) return;
3572
+ try {
3573
+ value2 = migrate(raw, storedVersion);
3574
+ } catch (err) {
3575
+ onError2?.(err, "migrate");
3576
+ return;
3577
+ }
3578
+ }
3420
3579
  if (!validate || validate(value2)) {
3421
- sourceSet(value2);
3580
+ writing = true;
3581
+ try {
3582
+ sourceSet(value2);
3583
+ } finally {
3584
+ writing = false;
3585
+ }
3422
3586
  }
3587
+ } catch (err) {
3588
+ onError2?.(err, "hydrate");
3423
3589
  }
3424
- } catch {
3425
3590
  }
3426
- internalEffect(() => {
3591
+ hydrate();
3592
+ const stopEffect = internalEffect(() => {
3427
3593
  const value2 = sourceGet();
3594
+ if (writing) return;
3428
3595
  try {
3429
- const serialized = serialize(value2);
3596
+ const serialized = version !== void 0 ? serialize({ [ENVELOPE_TAG]: version, value: value2 }) : serialize(value2);
3430
3597
  storage.setItem(key, serialized);
3431
- } catch {
3598
+ } catch (err) {
3599
+ onError2?.(err, err?.name === "QuotaExceededError" ? "write" : "serialize");
3432
3600
  }
3433
3601
  });
3602
+ const enableSync = options?.syncTabs ?? (typeof window !== "undefined" && storage === globalThis.localStorage);
3603
+ let onStorage;
3604
+ if (enableSync && typeof window !== "undefined") {
3605
+ onStorage = (e) => {
3606
+ if (e.storageArea !== storage) return;
3607
+ if (e.key !== null && e.key !== key) return;
3608
+ hydrate();
3609
+ };
3610
+ window.addEventListener("storage", onStorage);
3611
+ }
3612
+ return () => {
3613
+ stopEffect();
3614
+ if (onStorage && typeof window !== "undefined") {
3615
+ window.removeEventListener("storage", onStorage);
3616
+ }
3617
+ };
3434
3618
  }
3435
3619
 
3436
3620
  // src/events/bus.ts