@getforma/core 1.1.0 → 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.
@@ -625,6 +625,9 @@ var FormaJS = (() => {
625
625
 
626
626
  // src/reactive/signal.ts
627
627
  var signalNames = /* @__PURE__ */ new WeakMap();
628
+ function value(v) {
629
+ return () => v;
630
+ }
628
631
  function applySignalSet(s, v, equals) {
629
632
  if (typeof v !== "function") {
630
633
  if (!equals) {
@@ -3156,6 +3159,19 @@ var FormaJS = (() => {
3156
3159
  function createStore(initial) {
3157
3160
  const signals = /* @__PURE__ */ new Map();
3158
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
+ }
3159
3175
  function registerChild(path) {
3160
3176
  const lastDot = path.lastIndexOf(".");
3161
3177
  if (lastDot === -1) return;
@@ -3187,14 +3203,37 @@ var FormaJS = (() => {
3187
3203
  }
3188
3204
  childSet.clear();
3189
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
+ }
3190
3228
  function wrap(raw, basePath) {
3191
3229
  if (!shouldWrap(raw)) return raw;
3192
- const cached = proxyCache.get(raw);
3193
- 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
+ }
3194
3235
  const isArr = Array.isArray(raw);
3195
3236
  const basePrefix = basePath ? basePath + "." : "";
3196
- let lastKey = "";
3197
- let lastSignal;
3198
3237
  const proxy = new Proxy(raw, {
3199
3238
  // -------------------------------------------------------------------
3200
3239
  // GET
@@ -3215,12 +3254,10 @@ var FormaJS = (() => {
3215
3254
  (a) => a != null && typeof a === "object" && a[RAW] ? a[RAW] : a
3216
3255
  );
3217
3256
  result = target[key].apply(target, rawArgs);
3218
- invalidateChildren(basePath);
3219
- const [, setLen] = getSignal(
3220
- basePrefix + "length",
3221
- target.length
3222
- );
3223
- 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);
3224
3261
  });
3225
3262
  return result;
3226
3263
  };
@@ -3231,15 +3268,11 @@ var FormaJS = (() => {
3231
3268
  return target.length;
3232
3269
  }
3233
3270
  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
- }
3271
+ const pair = getSignal(childPath, value2);
3242
3272
  pair[0]();
3273
+ if (Array.isArray(value2)) {
3274
+ getArrayVersion(childPath)[0]();
3275
+ }
3243
3276
  if (shouldWrap(value2)) {
3244
3277
  return wrap(value2, childPath);
3245
3278
  }
@@ -3255,9 +3288,11 @@ var FormaJS = (() => {
3255
3288
  const key = String(prop);
3256
3289
  const childPath = basePrefix + key;
3257
3290
  const rawValue = value2 != null && typeof value2 === "object" && value2[RAW] ? value2[RAW] : value2;
3291
+ const oldRaw = Reflect.get(target, prop);
3258
3292
  Reflect.set(target, prop, rawValue);
3259
- if (rawValue != null && typeof rawValue === "object") {
3293
+ if (rawValue != null && typeof rawValue === "object" && oldRaw !== rawValue) {
3260
3294
  invalidateChildren(childPath);
3295
+ evictProxy(oldRaw, childPath);
3261
3296
  }
3262
3297
  if (isArr && key !== "length") {
3263
3298
  const lengthPath = basePrefix + "length";
@@ -3266,6 +3301,12 @@ var FormaJS = (() => {
3266
3301
  lenPair[1](target.length);
3267
3302
  }
3268
3303
  }
3304
+ if (isArr && key === "length") {
3305
+ batch(() => {
3306
+ reconcileChildren(basePath, target);
3307
+ if (basePath) bumpArrayVersion(basePath);
3308
+ });
3309
+ }
3269
3310
  const [, setter2] = getSignal(childPath, rawValue);
3270
3311
  setter2(rawValue);
3271
3312
  return true;
@@ -3304,24 +3345,30 @@ var FormaJS = (() => {
3304
3345
  }
3305
3346
  const key = String(prop);
3306
3347
  const childPath = basePrefix + key;
3348
+ const oldRaw = Reflect.get(target, prop);
3307
3349
  const result = Reflect.deleteProperty(target, prop);
3350
+ const delPair = signals.get(childPath);
3351
+ if (delPair) delPair[1](void 0);
3352
+ evictProxy(oldRaw, childPath);
3308
3353
  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
3354
  return result;
3320
3355
  }
3321
3356
  });
3322
- proxyCache.set(raw, proxy);
3357
+ if (!byPath) {
3358
+ byPath = /* @__PURE__ */ new Map();
3359
+ proxyCache.set(raw, byPath);
3360
+ }
3361
+ byPath.set(basePath, proxy);
3323
3362
  return proxy;
3324
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
+ }
3325
3372
  const rootProxy = wrap(initial, "");
3326
3373
  function getCurrentSnapshot() {
3327
3374
  return untrack(() => deepClone(initial));
@@ -3338,10 +3385,24 @@ var FormaJS = (() => {
3338
3385
  }
3339
3386
 
3340
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
+ }
3341
3402
  function createHistory(source, options) {
3342
3403
  const [sourceGet, sourceSet] = source;
3343
- const maxLength = options?.maxLength ?? 100;
3344
- let _stack = [sourceGet()];
3404
+ const maxLength = Math.max(1, options?.maxLength ?? 100);
3405
+ let _stack = [cloneEntry(sourceGet())];
3345
3406
  let _cursor = 0;
3346
3407
  const [stackSignal, setStackSignal] = createSignal([..._stack]);
3347
3408
  const [cursorSignal, setCursorSignal] = createSignal(_cursor);
@@ -3353,45 +3414,52 @@ var FormaJS = (() => {
3353
3414
  setStackLenSignal(_stack.length);
3354
3415
  });
3355
3416
  }
3356
- let ignoreNext = false;
3417
+ const NONE = /* @__PURE__ */ Symbol("none");
3418
+ let _expected = NONE;
3357
3419
  let isFirstRun = true;
3358
- internalEffect(() => {
3420
+ const disposeEffect = internalEffect(() => {
3359
3421
  const value2 = sourceGet();
3360
3422
  if (isFirstRun) {
3361
3423
  isFirstRun = false;
3362
3424
  return;
3363
3425
  }
3364
- if (ignoreNext) {
3365
- ignoreNext = false;
3426
+ if (_expected !== NONE && Object.is(value2, _expected)) {
3427
+ _expected = NONE;
3366
3428
  return;
3367
3429
  }
3430
+ _expected = NONE;
3368
3431
  _stack = _stack.slice(0, _cursor + 1);
3369
- _stack.push(value2);
3432
+ _stack.push(cloneEntry(value2));
3370
3433
  if (_stack.length > maxLength) {
3371
3434
  _stack.splice(0, _stack.length - maxLength);
3372
3435
  }
3373
3436
  _cursor = _stack.length - 1;
3374
3437
  syncSignals();
3375
3438
  });
3439
+ const destroy = () => {
3440
+ disposeEffect();
3441
+ };
3376
3442
  const canUndo = () => cursorSignal() > 0;
3377
3443
  const canRedo = () => cursorSignal() < stackLenSignal() - 1;
3378
3444
  const undo = () => {
3379
3445
  if (_cursor <= 0) return;
3380
3446
  _cursor--;
3381
- ignoreNext = true;
3382
- sourceSet(_stack[_cursor]);
3447
+ const restored = cloneEntry(_stack[_cursor]);
3448
+ _expected = restored;
3449
+ sourceSet(restored);
3383
3450
  syncSignals();
3384
3451
  };
3385
3452
  const redo = () => {
3386
3453
  if (_cursor >= _stack.length - 1) return;
3387
3454
  _cursor++;
3388
- ignoreNext = true;
3389
- sourceSet(_stack[_cursor]);
3455
+ const restored = cloneEntry(_stack[_cursor]);
3456
+ _expected = restored;
3457
+ sourceSet(restored);
3390
3458
  syncSignals();
3391
3459
  };
3392
3460
  const clear = () => {
3393
3461
  const currentValue = sourceGet();
3394
- _stack = [currentValue];
3462
+ _stack = [cloneEntry(currentValue)];
3395
3463
  _cursor = 0;
3396
3464
  syncSignals();
3397
3465
  };
@@ -3402,35 +3470,91 @@ var FormaJS = (() => {
3402
3470
  canRedo,
3403
3471
  history: () => stackSignal(),
3404
3472
  cursor: () => cursorSignal(),
3405
- clear
3473
+ clear,
3474
+ destroy
3406
3475
  };
3407
3476
  }
3408
3477
 
3409
3478
  // src/state/persist.ts
3479
+ var ENVELOPE_TAG = "$forma:v";
3410
3480
  function persist(source, key, options) {
3411
3481
  const [sourceGet, sourceSet] = source;
3412
3482
  const storage = options?.storage ?? globalThis.localStorage;
3413
3483
  const serialize = options?.serialize ?? JSON.stringify;
3414
3484
  const deserialize = options?.deserialize ?? JSON.parse;
3415
3485
  const validate = options?.validate;
3416
- try {
3417
- const stored = storage.getItem(key);
3418
- if (stored !== null) {
3419
- 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
+ }
3420
3519
  if (!validate || validate(value2)) {
3421
- sourceSet(value2);
3520
+ writing = true;
3521
+ try {
3522
+ sourceSet(value2);
3523
+ } finally {
3524
+ writing = false;
3525
+ }
3422
3526
  }
3527
+ } catch (err) {
3528
+ onError2?.(err, "hydrate");
3423
3529
  }
3424
- } catch {
3425
3530
  }
3426
- internalEffect(() => {
3531
+ hydrate();
3532
+ const stopEffect = internalEffect(() => {
3427
3533
  const value2 = sourceGet();
3534
+ if (writing) return;
3428
3535
  try {
3429
- const serialized = serialize(value2);
3536
+ const serialized = version !== void 0 ? serialize({ [ENVELOPE_TAG]: version, value: value2 }) : serialize(value2);
3430
3537
  storage.setItem(key, serialized);
3431
- } catch {
3538
+ } catch (err) {
3539
+ onError2?.(err, err?.name === "QuotaExceededError" ? "write" : "serialize");
3432
3540
  }
3433
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
+ };
3434
3558
  }
3435
3559
 
3436
3560
  // src/events/bus.ts