aberdeen 1.0.13 → 1.1.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/src/aberdeen.ts CHANGED
@@ -166,7 +166,7 @@ abstract class Scope implements QueueRunner {
166
166
 
167
167
  [ptr: ReverseSortedSetPointer]: this;
168
168
 
169
- abstract onChange(index: any, newData: any, oldData: any): void;
169
+ abstract onChange(index: any): void;
170
170
  abstract queueRun(): void;
171
171
 
172
172
  abstract getLastNode(): Node | undefined;
@@ -245,7 +245,7 @@ abstract class ContentScope extends Scope {
245
245
  return this.getLastNode() || this.getPrecedingNode();
246
246
  }
247
247
 
248
- onChange(index: any, newData: any, oldData: any) {
248
+ onChange() {
249
249
  queue(this);
250
250
  }
251
251
 
@@ -451,43 +451,6 @@ class SetArgScope extends ChainedScope {
451
451
  }
452
452
  }
453
453
 
454
- let immediateQueue: ReverseSortedSet<Scope, "prio"> = new ReverseSortedSet(
455
- "prio",
456
- );
457
-
458
- class ImmediateScope extends RegularScope {
459
- onChange(index: any, newData: any, oldData: any) {
460
- immediateQueue.add(this);
461
- }
462
- }
463
-
464
- let immediateQueueRunning = false;
465
- function runImmediateQueue() {
466
- for (
467
- let count = 0;
468
- !immediateQueue.isEmpty() && !immediateQueueRunning;
469
- count++
470
- ) {
471
- if (count > 42) {
472
- immediateQueue.clear();
473
- throw new Error("Too many immediate-mode recursive updates");
474
- }
475
- immediateQueueRunning = true;
476
- const copy = immediateQueue;
477
- immediateQueue = new ReverseSortedSet("prio");
478
- try {
479
- for (const scope of copy) {
480
- // On exception, the exception will be bubbled up to the call site, discarding any
481
- // remaining immediate scopes from the queue. This behavior is perhaps debatable,
482
- // but getting a synchronous exception at the call site can be very helpful.
483
- scope.queueRun();
484
- }
485
- } finally {
486
- immediateQueueRunning = false;
487
- }
488
- }
489
- }
490
-
491
454
  /** @internal */
492
455
  class OnEachScope extends Scope {
493
456
  // biome-ignore lint/correctness/noInvalidUseBeforeDeclaration: circular, as currentScope is initialized with a Scope
@@ -528,15 +491,11 @@ class OnEachScope extends Scope {
528
491
  // Do _addChild() calls for initial items
529
492
  if (target instanceof Array) {
530
493
  for (let i = 0; i < target.length; i++) {
531
- if (target[i] !== undefined) {
532
- new OnEachItemScope(this, i, false);
533
- }
494
+ new OnEachItemScope(this, i, false);
534
495
  }
535
496
  } else {
536
- for (const [key, value] of getEntries(target)) {
537
- if (value !== undefined) {
538
- new OnEachItemScope(this, key, false);
539
- }
497
+ for (const key of (target instanceof Map ? target.keys() : Object.keys(target))) {
498
+ new OnEachItemScope(this, key, false);
540
499
  }
541
500
  }
542
501
  }
@@ -545,7 +504,7 @@ class OnEachScope extends Scope {
545
504
  return findLastNodeInPrevSiblings(this.prevSibling);
546
505
  }
547
506
 
548
- onChange(index: any, newData: any, oldData: any) {
507
+ onChange(index: any) {
549
508
  if (!(this.target instanceof Array) || typeof index === "number")
550
509
  this.changedIndexes.add(index);
551
510
  queue(this);
@@ -558,17 +517,12 @@ class OnEachScope extends Scope {
558
517
  const oldScope = this.byIndex.get(index);
559
518
  if (oldScope) oldScope.remove();
560
519
 
561
- let hasValue;
562
- if (this.target instanceof Map) {
563
- hasValue = this.target.has(index);
520
+ if (this.target instanceof Map ? this.target.has(index) : index in this.target) {
521
+ // Item still exists
522
+ new OnEachItemScope(this, index, true);
564
523
  } else {
565
- hasValue = (this.target as any)[index] !== undefined;
566
- }
567
-
568
- if (!hasValue) {
524
+ // Item has disappeared
569
525
  this.byIndex.delete(index);
570
- } else {
571
- new OnEachItemScope(this, index, true);
572
526
  }
573
527
  }
574
528
  topRedrawScope = undefined;
@@ -774,6 +728,23 @@ function addNode(node: Node) {
774
728
  const ROOT_SCOPE = new RootScope();
775
729
  let currentScope: ContentScope = ROOT_SCOPE;
776
730
 
731
+ /**
732
+ * Execute a function in a never-cleaned root scope. Even {@link unmountAll} will not
733
+ * clean up observers/nodes created by the function.
734
+ * @param func The function to execute.
735
+ * @returns The return value of the function.
736
+ * @internal
737
+ */
738
+ export function leakScope<T>(func: () => T): T {
739
+ const savedScope = currentScope;
740
+ currentScope = new RootScope();
741
+ try {
742
+ return func();
743
+ } finally {
744
+ currentScope = savedScope;
745
+ }
746
+ }
747
+
777
748
  /**
778
749
  * A special Node observer index to subscribe to any value in the map changing.
779
750
  */
@@ -925,10 +896,12 @@ export function onEach(
925
896
  }
926
897
 
927
898
  function isObjEmpty(obj: object): boolean {
928
- for (const k in obj) return false;
899
+ for (const k of Object.keys(obj)) return false;
929
900
  return true;
930
901
  }
931
902
 
903
+ const EMPTY = Symbol("empty");
904
+
932
905
  /**
933
906
  * Reactively checks if an observable array or object is empty.
934
907
  *
@@ -938,7 +911,7 @@ function isObjEmpty(obj: object): boolean {
938
911
  * is deleted from an object), the scope that called `isEmpty` will be automatically
939
912
  * scheduled for re-evaluation.
940
913
  *
941
- * @param proxied The observable array or object (obtained via `observe()`) to check.
914
+ * @param proxied The observable array or object to check.
942
915
  * @returns `true` if the array has length 0 or the object has no own enumerable properties, `false` otherwise.
943
916
  *
944
917
  * @example
@@ -981,7 +954,7 @@ export function isEmpty(proxied: TargetType): boolean {
981
954
 
982
955
  const result = isObjEmpty(target);
983
956
  subscribe(target, ANY_SYMBOL, (index: any, newData: any, oldData: any) => {
984
- if (result ? oldData === undefined : newData === undefined) queue(scope);
957
+ if (result ? oldData === EMPTY : newData === EMPTY) queue(scope);
985
958
  });
986
959
  return result;
987
960
  }
@@ -1006,8 +979,8 @@ export interface ValueRef<T> {
1006
979
  * $('div', {text: cnt});
1007
980
  * // <div>2</div>
1008
981
 
1009
- * // Or we can use it in an {@link observe} function:
1010
- * observe(() => console.log("The count is now", cnt.value));
982
+ * // Or we can use it in an {@link derive} function:
983
+ * $(() => console.log("The count is now", cnt.value));
1011
984
  * // The count is now 2
1012
985
  *
1013
986
  * // Adding/removing items will update the count
@@ -1023,7 +996,7 @@ export function count(proxied: TargetType): ValueRef<number> {
1023
996
 
1024
997
  const target = (proxied as any)[TARGET_SYMBOL] || proxied;
1025
998
  let cnt = 0;
1026
- for (const k in target) if (target[k] !== undefined) cnt++;
999
+ for (const k of Object.keys(target)) if (target[k] !== undefined) cnt++;
1027
1000
 
1028
1001
  const result = proxy(cnt);
1029
1002
  subscribe(
@@ -1031,8 +1004,8 @@ export function count(proxied: TargetType): ValueRef<number> {
1031
1004
  ANY_SYMBOL,
1032
1005
  (index: any, newData: any, oldData: any) => {
1033
1006
  if (oldData === newData) {
1034
- } else if (oldData === undefined) result.value = ++cnt;
1035
- else if (newData === undefined) result.value = --cnt;
1007
+ } else if (oldData === EMPTY) result.value = ++cnt;
1008
+ else if (newData === EMPTY) result.value = --cnt;
1036
1009
  },
1037
1010
  );
1038
1011
 
@@ -1058,7 +1031,7 @@ export function defaultEmitHandler(
1058
1031
  if (byIndex) {
1059
1032
  for (const observer of byIndex) {
1060
1033
  if (typeof observer === "function") observer(index, newData, oldData);
1061
- else observer.onChange(index, newData, oldData);
1034
+ else observer.onChange(index);
1062
1035
  }
1063
1036
  }
1064
1037
  }
@@ -1075,25 +1048,22 @@ const objectHandler: ProxyHandler<any> = {
1075
1048
  // Make sure newData is unproxied
1076
1049
  if (typeof newData === "object" && newData)
1077
1050
  newData = (newData as any)[TARGET_SYMBOL] || newData;
1078
- const oldData = target[prop];
1051
+ const oldData = target.hasOwnProperty(prop) ? target[prop] : EMPTY;
1079
1052
  if (newData !== oldData) {
1080
1053
  target[prop] = newData;
1081
1054
  emit(target, prop, newData, oldData);
1082
- runImmediateQueue();
1083
1055
  }
1084
1056
  return true;
1085
1057
  },
1086
1058
  deleteProperty(target: any, prop: any) {
1087
- const old = target[prop];
1059
+ const old = target.hasOwnProperty(prop) ? target[prop] : undefined;
1088
1060
  delete target[prop];
1089
- emit(target, prop, undefined, old);
1090
- runImmediateQueue();
1061
+ emit(target, prop, EMPTY, old);
1091
1062
  return true;
1092
1063
  },
1093
1064
  has(target: any, prop: any) {
1094
- const result = prop in target;
1095
1065
  subscribe(target, prop);
1096
- return result;
1066
+ return target.hasOwnProperty(prop);
1097
1067
  },
1098
1068
  ownKeys(target: any) {
1099
1069
  subscribe(target, ANY_SYMBOL);
@@ -1103,9 +1073,11 @@ const objectHandler: ProxyHandler<any> = {
1103
1073
 
1104
1074
  function arraySet(target: any, prop: any, newData: any) {
1105
1075
  // Make sure newData is unproxied
1106
- if (typeof newData === "object" && newData)
1076
+ if (typeof newData === "object" && newData) {
1107
1077
  newData = (newData as any)[TARGET_SYMBOL] || newData;
1108
- const oldData = target[prop];
1078
+ }
1079
+ let oldData = target[prop];
1080
+ if (oldData === undefined && !target.hasOwnProperty(prop)) oldData = EMPTY;
1109
1081
  if (newData !== oldData) {
1110
1082
  const oldLength = target.length;
1111
1083
 
@@ -1114,11 +1086,13 @@ function arraySet(target: any, prop: any, newData: any) {
1114
1086
 
1115
1087
  // We only need to emit for shrinking, as growing just adds undefineds
1116
1088
  for (let i = newData; i < oldLength; i++) {
1117
- emit(target, i, undefined, target[i]);
1089
+ emit(target, i, EMPTY, target[i]);
1118
1090
  }
1119
1091
  } else {
1120
- const intProp = Number.parseInt(prop);
1121
- if (intProp.toString() === prop) prop = intProp;
1092
+ if (typeof prop === 'string') { // Convert to int when possible
1093
+ const n = 0|prop as any;
1094
+ if (String(n) === prop && n >= 0) prop = n;
1095
+ }
1122
1096
 
1123
1097
  target[prop] = newData;
1124
1098
  emit(target, prop, newData, oldData);
@@ -1126,7 +1100,6 @@ function arraySet(target: any, prop: any, newData: any) {
1126
1100
  if (target.length !== oldLength) {
1127
1101
  emit(target, "length", target.length, oldLength);
1128
1102
  }
1129
- runImmediateQueue();
1130
1103
  }
1131
1104
  return true;
1132
1105
  }
@@ -1134,17 +1107,24 @@ function arraySet(target: any, prop: any, newData: any) {
1134
1107
  const arrayHandler: ProxyHandler<any[]> = {
1135
1108
  get(target: any, prop: any) {
1136
1109
  if (prop === TARGET_SYMBOL) return target;
1137
- let subProp = prop;
1138
- if (typeof prop !== "symbol") {
1139
- const intProp = Number.parseInt(prop);
1140
- if (intProp.toString() === prop) subProp = intProp;
1110
+ if (typeof prop === 'string') { // Convert to int when possible
1111
+ const n = 0|prop as any;
1112
+ if (String(n) === prop && n >= 0) prop = n;
1141
1113
  }
1142
- subscribe(target, subProp);
1114
+ subscribe(target, prop);
1143
1115
  return optProxy(target[prop]);
1144
1116
  },
1145
1117
  set: arraySet,
1146
- deleteProperty(target: any, prop: string | symbol) {
1147
- return arraySet(target, prop, undefined);
1118
+ deleteProperty(target: any, prop: any) {
1119
+ if (typeof prop === 'string') { // Convert to int when possible
1120
+ const n = 0|prop as any;
1121
+ if (String(n) === prop && n >= 0) prop = n;
1122
+ }
1123
+ let oldData = target[prop];
1124
+ if (oldData === undefined && !target.hasOwnProperty(prop)) oldData = EMPTY;
1125
+ delete target[prop];
1126
+ emit(target, prop, EMPTY, oldData);
1127
+ return true;
1148
1128
  },
1149
1129
  };
1150
1130
 
@@ -1190,31 +1170,34 @@ const mapMethodHandlers = {
1190
1170
  set(this: any, key: any, newData: any): any {
1191
1171
  const target: Map<any, any> = this[TARGET_SYMBOL];
1192
1172
  // Make sure key and newData are unproxied
1193
- if (typeof key === "object" && key)
1173
+ if (typeof key === "object" && key) {
1194
1174
  key = (key as any)[TARGET_SYMBOL] || key;
1195
- if (typeof newData === "object" && newData)
1175
+ }
1176
+ if (typeof newData === "object" && newData) {
1196
1177
  newData = (newData as any)[TARGET_SYMBOL] || newData;
1197
- const oldData = target.get(key);
1178
+ }
1179
+ let oldData = target.get(key);
1180
+ if (oldData === undefined && !target.has(key)) oldData = EMPTY;
1198
1181
  if (newData !== oldData) {
1199
1182
  const oldSize = target.size;
1200
1183
  target.set(key, newData);
1201
1184
  emit(target, key, newData, oldData);
1202
1185
  emit(target, MAP_SIZE_SYMBOL, target.size, oldSize);
1203
- runImmediateQueue();
1204
1186
  }
1205
1187
  return this;
1206
1188
  },
1207
1189
  delete(this: any, key: any): boolean {
1208
1190
  const target: Map<any, any> = this[TARGET_SYMBOL];
1209
1191
  // Make sure key is unproxied
1210
- if (typeof key === "object" && key)
1192
+ if (typeof key === "object" && key) {
1211
1193
  key = (key as any)[TARGET_SYMBOL] || key;
1212
- const oldData = target.get(key);
1194
+ }
1195
+ let oldData = target.get(key);
1196
+ if (oldData === undefined && !target.has(key)) oldData = EMPTY;
1213
1197
  const result: boolean = target.delete(key);
1214
1198
  if (result) {
1215
- emit(target, key, undefined, oldData);
1199
+ emit(target, key, EMPTY, oldData);
1216
1200
  emit(target, MAP_SIZE_SYMBOL, target.size, target.size + 1);
1217
- runImmediateQueue();
1218
1201
  }
1219
1202
  return result;
1220
1203
  },
@@ -1222,18 +1205,17 @@ const mapMethodHandlers = {
1222
1205
  const target: Map<any, any> = this[TARGET_SYMBOL];
1223
1206
  const oldSize = target.size;
1224
1207
  for (const key of target.keys()) {
1225
- const oldData = target.get(key);
1226
- emit(target, key, undefined, oldData);
1208
+ emit(target, key, undefined, target.get(key));
1227
1209
  }
1228
1210
  target.clear();
1229
1211
  emit(target, MAP_SIZE_SYMBOL, 0, oldSize);
1230
- runImmediateQueue();
1231
1212
  },
1232
1213
  has(this: any, key: any): boolean {
1233
1214
  const target: Map<any, any> = this[TARGET_SYMBOL];
1234
1215
  // Make sure key is unproxied
1235
- if (typeof key === "object" && key)
1216
+ if (typeof key === "object" && key) {
1236
1217
  key = (key as any)[TARGET_SYMBOL] || key;
1218
+ }
1237
1219
  subscribe(target, key);
1238
1220
  return target.has(key);
1239
1221
  },
@@ -1264,7 +1246,7 @@ const mapHandler: ProxyHandler<Map<any, any>> = {
1264
1246
  if (prop === TARGET_SYMBOL) return target;
1265
1247
 
1266
1248
  // Handle Map methods using lookup object
1267
- if (prop in mapMethodHandlers) {
1249
+ if (mapMethodHandlers.hasOwnProperty(prop)) {
1268
1250
  return (mapMethodHandlers as any)[prop];
1269
1251
  }
1270
1252
 
@@ -1275,8 +1257,7 @@ const mapHandler: ProxyHandler<Map<any, any>> = {
1275
1257
  }
1276
1258
 
1277
1259
  // Handle other properties normally
1278
- subscribe(target, prop);
1279
- return optProxy((target as any)[prop]);
1260
+ return (target as any)[prop];
1280
1261
  },
1281
1262
  };
1282
1263
 
@@ -1308,35 +1289,15 @@ function optProxy(value: any): any {
1308
1289
  return proxied;
1309
1290
  }
1310
1291
 
1311
- export function proxy<T extends any>(
1312
- target: Array<T>,
1313
- ): Array<
1314
- T extends number
1315
- ? number
1316
- : T extends string
1317
- ? string
1318
- : T extends boolean
1319
- ? boolean
1320
- : T
1321
- >;
1292
+ export function proxy<T extends any>(target: Array<T>): Array<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T >;
1322
1293
  export function proxy<T extends object>(target: T): T;
1323
- export function proxy<T extends any>(
1324
- target: T,
1325
- ): ValueRef<
1326
- T extends number
1327
- ? number
1328
- : T extends string
1329
- ? string
1330
- : T extends boolean
1331
- ? boolean
1332
- : T
1333
- >;
1294
+ export function proxy<T extends any>(target: T): ValueRef<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T>;
1334
1295
 
1335
1296
  /**
1336
1297
  * Creates a reactive proxy around the given data.
1337
1298
  *
1338
1299
  * Reading properties from the returned proxy within a reactive scope (like one created by
1339
- * {@link $} or {@link observe}) establishes a subscription. Modifying properties *through*
1300
+ * {@link $} or {@link derive}) establishes a subscription. Modifying properties *through*
1340
1301
  * the proxy will notify subscribed scopes, causing them to re-execute.
1341
1302
  *
1342
1303
  * - Plain objects and arrays are wrapped in a standard JavaScript `Proxy` that intercepts
@@ -1353,23 +1314,23 @@ export function proxy<T extends any>(
1353
1314
  * @example Object
1354
1315
  * ```javascript
1355
1316
  * const state = proxy({ count: 0, message: 'Hello' });
1356
- * observe(() => console.log(state.message)); // Subscribes to message
1357
- * setTimeout(() => state.message = 'World', 1000); // Triggers the observe function
1317
+ * $(() => console.log(state.message)); // Subscribes to message
1318
+ * setTimeout(() => state.message = 'World', 1000); // Triggers the observing function
1358
1319
  * setTimeout(() => state.count++, 2000); // Triggers nothing
1359
1320
  * ```
1360
1321
  *
1361
1322
  * @example Array
1362
1323
  * ```javascript
1363
1324
  * const items = proxy(['a', 'b']);
1364
- * observe(() => console.log(items.length)); // Subscribes to length
1365
- * setTimeout(() => items.push('c'), 2000); // Triggers the observe function
1325
+ * $(() => console.log(items.length)); // Subscribes to length
1326
+ * setTimeout(() => items.push('c'), 2000); // Triggers the observing function
1366
1327
  * ```
1367
1328
  *
1368
1329
  * @example Primitive
1369
1330
  * ```javascript
1370
1331
  * const name = proxy('Aberdeen');
1371
- * observe(() => console.log(name.value)); // Subscribes to value
1372
- * setTimeout(() => name.value = 'UI', 2000); // Triggers the observe function
1332
+ * $(() => console.log(name.value)); // Subscribes to value
1333
+ * setTimeout(() => name.value = 'UI', 2000); // Triggers the observing function
1373
1334
  * ```
1374
1335
  *
1375
1336
  * @example Class instance
@@ -1380,7 +1341,7 @@ export function proxy<T extends any>(
1380
1341
  * toString() { return `${this.name}Widget (${this.width}x${this.height})`; }
1381
1342
  * }
1382
1343
  * let graph: Widget = proxy(new Widget('Graph', 200, 100));
1383
- * observe(() => console.log(''+graph));
1344
+ * $(() => console.log(''+graph));
1384
1345
  * setTimeout(() => graph.grow(), 2000);
1385
1346
  * setTimeout(() => graph.grow(), 4000);
1386
1347
  * ```
@@ -1453,12 +1414,9 @@ function destroyWithClass(element: Element, cls: string) {
1453
1414
  *
1454
1415
  * @param dst - The destination object/array/Map (proxied or unproxied).
1455
1416
  * @param src - The source object/array/Map (proxied or unproxied). It won't be modified.
1456
- * @param flags - Bitmask controlling copy behavior:
1457
- * - {@link MERGE}: Performs a partial update. Properties in `dst` not present in `src` are kept.
1458
- * `null`/`undefined` in `src` delete properties in `dst`. Handles partial array updates via object keys.
1459
- * - {@link SHALLOW}: Performs a shallow copy; when an array/object of the right type doesn't exist in `dst` yet, a reference to the array/object in `src` will be made, instead of creating a copy. If the array/object already exists, it won't be replaced (by a reference), but all items will be individually checked and copied like normal, keeping changes (and therefore UI updates) to a minimum.
1460
- * @template T - The type of the destination object.
1461
- * @throws Error if attempting to copy an array into a non-array or vice versa (unless {@link MERGE} is set, allowing for sparse array updates).
1417
+ * @template T - The type of the objects being copied.
1418
+ * @returns `true` if any changes were made to `dst`, or `false` if not.
1419
+ * @throws Error if attempting to copy an array into a non-array or vice versa.
1462
1420
  *
1463
1421
  * @example Basic Copy
1464
1422
  * ```typescript
@@ -1466,108 +1424,79 @@ function destroyWithClass(element: Element, cls: string) {
1466
1424
  * const dest = proxy({ b: { d: 3 } });
1467
1425
  * copy(dest, source);
1468
1426
  * console.log(dest); // proxy({ a: 1, b: { c: 2 } })
1427
+ * copy(dest, 'b', { e: 4 });
1428
+ * console.log(dest); // proxy({ a: 1, b: { e: 4 } })
1469
1429
  * ```
1470
- *
1471
- * @example Map to Object
1472
- * ```typescript
1473
- * const source = new Map([['x', 3], ['y', 4]]);
1474
- * const dest = proxy({});
1475
- * copy(dest, source);
1476
- * console.log(dest); // proxy({ x: 3, y: 4 })
1477
- * ```
1478
- *
1479
- * @example Object to Map
1480
- * ```typescript
1481
- * const source = { x: 3, y: 4 };
1482
- * const dest = proxy(new Map());
1483
- * copy(dest, source);
1484
- * console.log(dest); // proxy(Map([['x', 3], ['y', 4]]))
1485
- * ```
1486
- *
1487
- * @example MERGE
1430
+ */
1431
+
1432
+ export function copy<T extends object>(dst: T, src: T): boolean;
1433
+ /**
1434
+ * Like above, but copies `src` into `dst[dstKey]`. This is useful if you're unsure if dst[dstKey]
1435
+ * already exists (as the right type of object) or if you don't want to subscribe to dst[dstKey].
1436
+ *
1437
+ * @param dstKey - Optional key in `dst` to copy into.
1438
+ */
1439
+ export function copy<T extends object>(dst: T, dstKey: keyof T, src: T[typeof dstKey]): boolean;
1440
+ export function copy(a: any, b: any, c?: any): boolean {
1441
+ if (arguments.length > 2) return copySet(a, b, c, 0);
1442
+ return copyRecursive(a, b, 0);
1443
+ }
1444
+
1445
+ function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
1446
+ let dstVal = peek(dst, dstKey);
1447
+ if (src === dstVal) return false;
1448
+ if (typeof dstVal === "object" && dstVal && typeof src === "object" && src && dstVal.constructor === src.constructor) {
1449
+ return copyRecursive(dstVal, src, flags);
1450
+ }
1451
+ src = clone(src);
1452
+ if (dst instanceof Map) dst.set(dstKey, src);
1453
+ else dst[dstKey] = clone(src);
1454
+ return true;
1455
+ }
1456
+
1457
+ /**
1458
+ * Like {@link copy}, but uses merge semantics. Properties in `dst` not present in `src` are kept.
1459
+ * `null`/`undefined` in `src` delete properties in `dst`.
1460
+ *
1461
+ * When the destination is an object and the source is an array, its keys are used as (sparse) array indices.
1462
+ *
1463
+ * @example Basic merge
1488
1464
  * ```typescript
1489
1465
  * const source = { b: { c: 99 }, d: undefined }; // d: undefined will delete
1490
1466
  * const dest = proxy({ a: 1, b: { x: 5 }, d: 4 });
1491
- * copy(dest, source, MERGE);
1492
- * console.log(dest); // proxy({ a: 1, b: { c: 99, x: 5 } })
1467
+ * merge(dest, source);
1468
+ * merge(dest, 'b', { y: 6 }); // merge into dest.b
1469
+ * merge(dest, 'c', { z: 7 }); // merge.c doesn't exist yet, so it will just be assigned
1470
+ * console.log(dest); // proxy({ a: 1, b: { c: 99, x: 5, y: 6 }, c: { z: 7 } })
1493
1471
  * ```
1494
1472
  *
1495
- * @example Partial Array Update with MERGE
1473
+ * @example Partial Array Merge
1496
1474
  * ```typescript
1497
1475
  * const messages = proxy(['msg1', 'msg2', 'msg3']);
1498
1476
  * const update = { 1: 'updated msg2' }; // Update using object key as index
1499
- * copy(messages, update, MERGE);
1477
+ * merge(messages, update);
1500
1478
  * console.log(messages); // proxy(['msg1', 'updated msg2', 'msg3'])
1501
1479
  * ```
1502
1480
  *
1503
- * @example SHALLOW
1504
- * ```typescript
1505
- * const source = { nested: [1, 2] };
1506
- * const dest = {};
1507
- * copy(dest, source, SHALLOW);
1508
- * dest.nested.push(3);
1509
- * console.log(source.nested); // [1, 2, 3] (source was modified)
1510
- * ```
1511
1481
  */
1512
-
1513
- // Overload for Map destination with object source
1514
- export function copy<K, V>(dst: Map<K, V>, src: Record<K extends string | number | symbol ? K : never, V> | Partial<Record<K extends string | number | symbol ? K : never, V>>, flags?: number): void;
1515
- // Overload for Map destination with Map source
1516
- export function copy<K, V>(dst: Map<K, V>, src: Map<K, V> | Partial<Map<K, V>>, flags?: number): void;
1517
- // Overload for object destination with Map source
1518
- export function copy<T extends Record<string | number | symbol, any>>(dst: T, src: Map<keyof T, T[keyof T]>, flags?: number): void;
1519
- // Overload for same-type copying
1520
- export function copy<T extends object>(dst: T, src: Partial<T>, flags?: number): void;
1521
- // Implementation
1522
- export function copy(dst: any, src: any, flags = 0) {
1523
- copyRecurse(dst, src, flags);
1524
- runImmediateQueue();
1482
+ export function merge<T extends object>(dst: T, value: Partial<T>): boolean;
1483
+ export function merge<T extends object>(dst: T, dstKey: keyof T, value: Partial<T[typeof dstKey]>): boolean;
1484
+ export function merge(a: any, b: any, c?: any) {
1485
+ if (arguments.length > 2) return copySet(a, b, c, MERGE);
1486
+ return copyRecursive(a, b, MERGE);
1525
1487
  }
1526
- /** Flag to {@link copy} causing it to use merge semantics. See {@link copy} for details. */
1527
- export const MERGE = 1;
1528
- /** Flag to {@link copy} and {@link clone} causing them to create a shallow copy (instead of the deep copy done by default).*/
1529
- export const SHALLOW = 2;
1530
- const COPY_SUBSCRIBE = 32;
1531
- const COPY_EMIT = 64;
1532
1488
 
1533
- /**
1534
- * Clone an (optionally proxied) object or array.
1535
- *
1536
- * @param src The object or array to clone. If it is proxied, `clone` will subscribe to any changes to the (nested) data structure.
1537
- * @param flags
1538
- * - {@link SHALLOW}: Performs a shallow clone, meaning that only the top-level array or object will be copied, while object/array values will just be references to the original data in `src`.
1539
- * @template T - The type of the objects being copied.
1540
- * @returns A new unproxied array or object (of the same type as `src`), containing a deep (by default) copy of `src`.
1541
- */
1542
- export function clone<T extends object>(src: T, flags = 0): T {
1543
- let dst: T;
1544
- if (src instanceof Map) {
1545
- dst = new Map() as T;
1546
- } else {
1547
- dst = Object.create(Object.getPrototypeOf(src)) as T;
1548
- }
1549
- copyRecurse(dst, src, flags);
1550
- return dst;
1551
- }
1552
-
1553
- function getEntries(subject: any) {
1554
- return (subject instanceof Map) ? subject.entries() : Object.entries(subject);
1555
- }
1556
-
1557
- function getKeys(subject: any) {
1558
- return (subject instanceof Map) ? subject.keys() : Object.keys(subject);
1559
- }
1560
-
1561
- function copyRecurse(dst: any, src: any, flags: number) {
1489
+ // The dst and src parameters must be objects. Will throw a friendly message if they're not both the same type.
1490
+ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean {
1562
1491
  // We never want to subscribe to reads we do to the target (to find changes). So we'll
1563
1492
  // take the unproxied version and `emit` updates ourselve.
1564
- let unproxied = dst[TARGET_SYMBOL];
1493
+ let unproxied = (dst as any)[TARGET_SYMBOL] as T;
1565
1494
  if (unproxied) {
1566
1495
  dst = unproxied;
1567
1496
  flags |= COPY_EMIT;
1568
1497
  }
1569
1498
  // For performance, we'll work on the unproxied `src` and manually subscribe to changes.
1570
- unproxied = src[TARGET_SYMBOL];
1499
+ unproxied = (src as any)[TARGET_SYMBOL] as T;
1571
1500
  if (unproxied) {
1572
1501
  src = unproxied;
1573
1502
  // If we're not in peek mode, we'll manually subscribe to all source reads.
@@ -1575,95 +1504,146 @@ function copyRecurse(dst: any, src: any, flags: number) {
1575
1504
  }
1576
1505
 
1577
1506
  if (flags & COPY_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
1578
- if (src instanceof Array) {
1579
- if (!(dst instanceof Array))
1580
- throw new Error("Cannot copy array into object");
1507
+ let changed = false;
1508
+
1509
+ // The following loops are somewhat repetitive, but it keeps performance high by avoiding
1510
+ // function calls and extra checks within the loops.
1511
+
1512
+ if (src instanceof Array && dst instanceof Array) {
1581
1513
  const dstLen = dst.length;
1582
1514
  const srcLen = src.length;
1583
- for (let i = 0; i < srcLen; i++) {
1584
- copyValue(dst, i, src[i], flags);
1515
+ for (let index = 0; index < srcLen; index++) {
1516
+ // changed = copyValue(dst, i, src[i], flags) || changed;
1517
+
1518
+ let dstValue = dst[index];
1519
+ if (dstValue === undefined && !dst.hasOwnProperty(index)) dstValue = EMPTY;
1520
+ let srcValue = src[index];
1521
+ if (srcValue === undefined && !src.hasOwnProperty(index)) {
1522
+ delete dst[index];
1523
+ if (flags & COPY_EMIT) emit(dst, index, EMPTY, dstValue);
1524
+ changed = true;
1525
+ }
1526
+ else if (dstValue !== srcValue) {
1527
+ if (srcValue && typeof srcValue === "object") {
1528
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1529
+ changed = copyRecursive(dstValue, srcValue, flags) || changed;
1530
+ continue;
1531
+ }
1532
+ srcValue = clone(srcValue);
1533
+ }
1534
+ dst[index] = srcValue;
1535
+
1536
+ if (flags & COPY_EMIT) emit(dst, index, srcValue, dstValue);
1537
+ changed = true;
1538
+ }
1585
1539
  }
1540
+
1586
1541
  // Leaving additional values in the old array doesn't make sense, so we'll do this even when MERGE is set:
1587
1542
  if (srcLen !== dstLen) {
1588
1543
  if (flags & COPY_EMIT) {
1589
1544
  for (let i = srcLen; i < dstLen; i++) {
1590
1545
  const old = dst[i];
1591
- dst[i] = undefined;
1592
- emit(dst, i, undefined, old);
1546
+ delete dst[i];
1547
+ emit(dst, i, EMPTY, old);
1593
1548
  }
1594
1549
  dst.length = srcLen;
1595
1550
  emit(dst, "length", srcLen, dstLen);
1596
1551
  } else {
1597
1552
  dst.length = srcLen;
1598
1553
  }
1554
+ changed = true;
1599
1555
  }
1600
- } else {
1601
- // Copy all entries from src to dst (both of which can be Map or object)
1602
- for (const [key, value] of getEntries(src)) {
1603
- copyValue(dst, key, value, flags);
1556
+ } else if (src instanceof Map && dst instanceof Map) {
1557
+ for (const key of src.keys()) {
1558
+ // changed = copyValue(dst, k, src.get(k), flags) || changed;
1559
+ let srcValue = src.get(key);
1560
+ let dstValue = dst.get(key);
1561
+ if (dstValue === undefined && !dst.has(key)) dstValue = EMPTY;
1562
+ if (dstValue !== srcValue) {
1563
+ if (srcValue && typeof srcValue === "object") {
1564
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1565
+ changed = copyRecursive(dstValue, srcValue, flags) || changed;
1566
+ continue;
1567
+ }
1568
+ srcValue = clone(srcValue);
1569
+ }
1570
+
1571
+ dst.set(key, srcValue);
1572
+
1573
+ if (flags & COPY_EMIT) emit(dst, key, srcValue, dstValue);
1574
+ changed = true;
1575
+ }
1604
1576
  }
1605
-
1606
- // Remove entries from dst that don't exist in src (unless MERGE flag is set)
1577
+
1607
1578
  if (!(flags & MERGE)) {
1608
- if (src instanceof Map) {
1609
- for (const key of getKeys(dst)) {
1610
- if (!src.has(key)) {
1611
- deleteKey(dst, key, flags);
1579
+ for (const k of dst.keys()) {
1580
+ if (!src.has(k)) {
1581
+ const old = dst.get(k);
1582
+ dst.delete(k);
1583
+ if (flags & COPY_EMIT) {
1584
+ emit(dst, k, undefined, old);
1612
1585
  }
1586
+ changed = true;
1613
1587
  }
1614
- } else {
1615
- for (const key of getKeys(dst)) {
1616
- if (!(key in src)) {
1617
- deleteKey(dst, key, flags);
1588
+ }
1589
+ }
1590
+ } else if (src.constructor === dst.constructor) {
1591
+ for (const key of Object.keys(src) as (keyof typeof src)[]) {
1592
+ // changed = copyValue(dst, k, src[k as keyof typeof src], flags) || changed;
1593
+ let srcValue = src[key];
1594
+ const dstValue = dst.hasOwnProperty(key) ? dst[key] : EMPTY;
1595
+ if (dstValue !== srcValue) {
1596
+ if (srcValue && typeof srcValue === "object") {
1597
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1598
+ changed = copyRecursive(dstValue as typeof srcValue, srcValue, flags) || changed;
1599
+ continue;
1618
1600
  }
1601
+ srcValue = clone(srcValue);
1619
1602
  }
1603
+
1604
+ dst[key] = srcValue;
1605
+
1606
+ if (flags & COPY_EMIT) emit(dst, key, srcValue, dstValue);
1607
+ changed = true;
1620
1608
  }
1621
1609
  }
1622
- }
1623
- }
1624
1610
 
1625
- function deleteKey(dst: any, key: any, flags: number) {
1626
- let old;
1627
- if (dst instanceof Map) {
1628
- old = dst.get(key);
1629
- dst.delete(key);
1611
+ if (!(flags & MERGE)) {
1612
+ for (const k of Object.keys(dst) as (keyof typeof dst)[]) {
1613
+ if (!src.hasOwnProperty(k)) {
1614
+ const old = dst[k];
1615
+ delete dst[k];
1616
+ if (flags & COPY_EMIT && old !== undefined) {
1617
+ emit(dst, k, undefined, old);
1618
+ }
1619
+ changed = true;
1620
+ }
1621
+ }
1622
+ }
1630
1623
  } else {
1631
- old = dst[key];
1632
- delete dst[key];
1633
- }
1634
- if (flags & COPY_EMIT && old !== undefined) {
1635
- emit(dst, key, undefined, old);
1624
+ throw new Error(`Incompatible or non-object types: ${src?.constructor?.name || typeof src} vs ${dst?.constructor?.name || typeof dst}`);
1636
1625
  }
1626
+ return changed;
1637
1627
  }
1638
1628
 
1639
- function copyValue(dst: any, index: any, srcValue: any, flags: number) {
1640
- const dstValue = dst instanceof Map ? dst.get(index) : dst[index];
1641
- if (srcValue !== dstValue) {
1642
- if (
1643
- srcValue && dstValue && typeof srcValue === "object" && typeof dstValue === "object" &&
1644
- (srcValue.constructor === dstValue.constructor || (flags & MERGE && dstValue instanceof Array))
1645
- ) {
1646
- copyRecurse(dstValue, srcValue, flags);
1647
- return;
1648
- }
1629
+ const MERGE = 1;
1630
+ const COPY_SUBSCRIBE = 32;
1631
+ const COPY_EMIT = 64;
1649
1632
 
1650
- if (!(flags & SHALLOW) && srcValue && typeof srcValue === "object") {
1651
- // Create an empty object of the same type
1652
- const copy = Object.create(Object.getPrototypeOf(srcValue));
1653
- // Copy all properties to it. This doesn't need to emit anything
1654
- // and MERGE does not apply as this is a new branch.
1655
- copyRecurse(copy, srcValue, 0);
1656
- srcValue = copy;
1657
- }
1658
- if (dst instanceof Map) {
1659
- if (flags & MERGE && srcValue == null) dst.delete(index);
1660
- else dst.set(index, srcValue);
1661
- } else {
1662
- if (flags & MERGE && srcValue == null) delete dst[index];
1663
- else dst[index] = srcValue;
1664
- }
1665
- if (flags & COPY_EMIT) emit(dst, index, srcValue, dstValue);
1666
- }
1633
+ /**
1634
+ * Clone an (optionally proxied) object or array.
1635
+ *
1636
+ * @param src The object or array to clone. If it is proxied, `clone` will subscribe to any changes to the (nested) data structure.
1637
+ * @template T - The type of the objects being copied.
1638
+ * @returns A new unproxied array or object (of the same type as `src`), containing a deep copy of `src`.
1639
+ */
1640
+ export function clone<T extends object>(src: T): T {
1641
+ // Create an empty object of the same type
1642
+ const copied = Array.isArray(src) ? [] : src instanceof Map ? new Map() : Object.create(Object.getPrototypeOf(src));
1643
+ // Copy all properties to it. This doesn't need to emit anything, and because
1644
+ // the destination is an empty object, we can just MERGE, which is a bit faster.
1645
+ copyRecursive(copied, src, MERGE);
1646
+ return copied;
1667
1647
  }
1668
1648
 
1669
1649
  interface RefTarget {
@@ -1771,7 +1751,7 @@ function applyBind(el: HTMLInputElement, target: any) {
1771
1751
  throw new Error(`SELECT has no '${target.value}' OPTION (yet)`);
1772
1752
  };
1773
1753
  }
1774
- observe(onProxyChange);
1754
+ derive(onProxyChange);
1775
1755
  el.addEventListener("input", onInputChange);
1776
1756
  clean(() => {
1777
1757
  el.removeEventListener("input", onInputChange);
@@ -1807,12 +1787,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1807
1787
  },
1808
1788
  text: (value: any) => {
1809
1789
  addNode(document.createTextNode(value));
1810
- },
1811
- element: (value: any) => {
1812
- console.log("Aberdeen: $({element: myElement}) is deprecated, use $(myElement) instead");
1813
- addNode(value);
1814
- SPECIAL_PROPS.element = addNode; // Avoid the console.log next time
1815
- },
1790
+ }
1816
1791
  };
1817
1792
 
1818
1793
  /**
@@ -1828,7 +1803,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1828
1803
  * - An optional HTML **tag**, something like `h1`. If present, a DOM element of that tag is created, and that element will be the *current* element for the rest of this `$` function execution.
1829
1804
  * - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element.
1830
1805
  * - Optional content **text** prefixed by a `:` character, ranging til the end of the string. This will be added as a TextNode to the *current* element.
1831
- * - `function`: When a function (without argument nor a return value) is passed in, it will be reactively executed in its own observe scope, preserving the *current element*. So any `$()` invocations within this function will create DOM elements with our *current* element as parent. If the function reads observable data, and that data is changed later on, the function we re-execute (after side effects, such as DOM modifications through `$`, have been cleaned - see also {@link clean}).
1806
+ * - `function`: When a function (without argument nor a return value) is passed in, it will be reactively executed in its own observer scope, preserving the *current element*. So any `$()` invocations within this function will create DOM elements with our *current* element as parent. If the function reads observable data, and that data is changed later on, the function we re-execute (after side effects, such as DOM modifications through `$`, have been cleaned - see also {@link clean}).
1832
1807
  * - `object`: When an object is passed in, its key-value pairs are used to modify the *current* element in the following ways...
1833
1808
  * - `{<attrName>: any}`: The common case is setting the value as an HTML attribute named key. So `{placeholder: "Your name"}` would add `placeholder="Your name"` to the current HTML element.
1834
1809
  * - `{<propName>: boolean}` or `{value: any}` or `{selectedIndex: number}`: If the value is a boolean, or if the key is `value` or `selectedIndex`, it is set on the `current` element as a DOM property instead of an HTML attribute. For example `{checked: true}` would do `el.checked = true` for the *current* element.
@@ -1839,7 +1814,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1839
1814
  * - `{destroy: string}`: When the *current* element is a top-level element to be removed (due to reactivity cleanup), actual removal from the DOM is delayed by 2 seconds, and in the mean time the value string is added as a CSS class to the element, allowing for a deletion transition. The string may also contain multiple dot-separated CSS classes, such as `.fade.shrink`.
1840
1815
  * - `{create: function}` and `{destroy: function}`: The function is invoked when the *current* element is the top-level element being created/destroyed. It can be used for more involved creation/deletion animations. In case of `destroy`, the function is responsible for actually removing the element from the DOM (eventually). See `transitions.ts` in the Aberdeen source code for some examples.
1841
1816
  * - `{bind: <obsValue>}`: Create a two-way binding element between the `value` property of the given observable (proxy) variable, and the *current* input element (`<input>`, `<select>` or `<textarea>`). This is often used together with {@link ref}, in order to use properties other than `.value`.
1842
- * - `{<any>: <obsvalue>}`: Create a new observe scope and read the `value` property of the given observable (proxy) variable from within it, and apply the contained value using any of the other rules in this list. Example:
1817
+ * - `{<any>: <obsvalue>}`: Create a new observer scope and read the `value` property of the given observable (proxy) variable from within it, and apply the contained value using any of the other rules in this list. Example:
1843
1818
  * ```typescript
1844
1819
  * const myColor = proxy('red');
1845
1820
  * $('p:Test', {$color: myColor, click: () => myColor.value = 'yellow'})
@@ -1984,7 +1959,7 @@ export function $(
1984
1959
  break;
1985
1960
  }
1986
1961
  } else {
1987
- for (const key in arg) {
1962
+ for (const key of Object.keys(arg)) {
1988
1963
  const val = arg[key];
1989
1964
  applyArg(key, val);
1990
1965
  }
@@ -2072,7 +2047,7 @@ export function insertCss(style: object, global = false): string {
2072
2047
  function styleToCss(style: object, prefix: string): string {
2073
2048
  let props = "";
2074
2049
  let rules = "";
2075
- for (const kOr in style) {
2050
+ for (const kOr of Object.keys(style)) {
2076
2051
  const v = (style as any)[kOr];
2077
2052
  for (const k of kOr.split(/, ?/g)) {
2078
2053
  if (v && typeof v === "object") {
@@ -2147,7 +2122,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
2147
2122
  /**
2148
2123
  * Sets a custom error handler function for errors that occur asynchronously
2149
2124
  * within reactive scopes (e.g., during updates triggered by proxy changes in
2150
- * {@link observe} or {@link $} render functions).
2125
+ * {@link derive} or {@link $} render functions).
2151
2126
  *
2152
2127
  * The default handler logs the error to `console.error` and adds a simple
2153
2128
  * 'Error' message div to the DOM at the location where the error occurred (if possible).
@@ -2235,7 +2210,7 @@ export function getParentElement(): Element {
2235
2210
  * This is useful for releasing resources, removing manual event listeners, or cleaning up
2236
2211
  * side effects associated with the scope. Cleaners are run in reverse order of registration.
2237
2212
  *
2238
- * Scopes are created by functions like {@link observe}, {@link mount}, {@link $} (when given a render function),
2213
+ * Scopes are created by functions like {@link derive}, {@link mount}, {@link $} (when given a render function),
2239
2214
  * and internally by constructs like {@link onEach}.
2240
2215
  *
2241
2216
  * @param cleaner - The function to execute during cleanup.
@@ -2253,7 +2228,7 @@ export function getParentElement(): Element {
2253
2228
  * peek(() => sum.value += item);
2254
2229
  * // Clean gets called before each rerun for a certain item index
2255
2230
  * // No need for peek here, as the clean code doesn't run in an
2256
- * // observe scope.
2231
+ * // observer scope.
2257
2232
  * clean(() => sum.value -= item);
2258
2233
  * })
2259
2234
  *
@@ -2281,7 +2256,7 @@ export function clean(cleaner: () => void) {
2281
2256
  *
2282
2257
  * @param func - The function to execute reactively. Any DOM manipulations should typically
2283
2258
  * be done using {@link $} within this function. Its return value will be made available as an
2284
- * observable returned by the `observe()` function.
2259
+ * observable returned by the `derive()` function.
2285
2260
  * @returns An observable object, with its `value` property containing whatever the last run of `func` returned.
2286
2261
  *
2287
2262
  * @example Observation creating a UI components
@@ -2292,7 +2267,7 @@ export function clean(cleaner: () => void) {
2292
2267
  * console.log('Welcome');
2293
2268
  * $('h3:Welcome, ' + data.user); // Reactive text
2294
2269
  *
2295
- * observe(() => {
2270
+ * derive(() => {
2296
2271
  * // When data.notifications changes, only this inner scope reruns,
2297
2272
  * // leaving the `<p>Welcome, ..</p>` untouched.
2298
2273
  * console.log('Notifications');
@@ -2302,13 +2277,13 @@ export function clean(cleaner: () => void) {
2302
2277
  * });
2303
2278
  * ```
2304
2279
  *
2305
- * ***Note*** that the above could just as easily be done using `$(func)` instead of `observe(func)`.
2280
+ * ***Note*** that the above could just as easily be done using `$(func)` instead of `derive(func)`.
2306
2281
  *
2307
2282
  * @example Observation with return value
2308
2283
  * ```typescript
2309
2284
  * const counter = proxy(0);
2310
2285
  * setInterval(() => counter.value++, 1000);
2311
- * const double = observe(() => counter.value * 2);
2286
+ * const double = derive(() => counter.value * 2);
2312
2287
  *
2313
2288
  * $('h3', () => {
2314
2289
  * $(`:counter=${counter.value} double=${double.value}`);
@@ -2318,42 +2293,10 @@ export function clean(cleaner: () => void) {
2318
2293
  * @overload
2319
2294
  * @param func Func without a return value.
2320
2295
  */
2321
- export function observe<T>(func: () => T): ValueRef<T> {
2296
+ export function derive<T>(func: () => T): ValueRef<T> {
2322
2297
  return new ResultScope<T>(currentScope.parentElement, func).result;
2323
2298
  }
2324
2299
 
2325
- /**
2326
- * Similar to {@link observe}, creates a reactive scope that re-executes the function
2327
- * when its proxied dependencies change.
2328
- *
2329
- * **Difference:** Updates run **synchronously and immediately** after the proxy modification
2330
- * that triggered the update occurs.
2331
- *
2332
- * **Caution:** Use sparingly. Immediate execution bypasses Aberdeen's usual batching and
2333
- * ordering optimizations, which can lead to performance issues or observing inconsistent
2334
- * intermediate states if multiple related updates are applied sequentially.
2335
- * Prefer {@link observe} or {@link $} for most use cases.
2336
- *
2337
- * @param func - The function to execute reactively and synchronously.
2338
- *
2339
- * @example
2340
- * ```javascript
2341
- * const state = proxy({ single: 'A' });
2342
- *
2343
- * immediateObserve(() => {
2344
- * state.double = state.single + state.single
2345
- * });
2346
- * console.log(state.double); // 'AA'
2347
- *
2348
- * state.single = 'B';
2349
- * // Synchronously:
2350
- * console.log(state.double); // 'BB'
2351
- * ```
2352
- */
2353
- export function immediateObserve(func: () => void) {
2354
- new ImmediateScope(currentScope.parentElement, func);
2355
- }
2356
-
2357
2300
  /**
2358
2301
  * Attaches a reactive Aberdeen UI fragment to an existing DOM element. Without the use of
2359
2302
  * this function, {@link $} will assume `document.body` as its root.
@@ -2363,11 +2306,11 @@ export function immediateObserve(func: () => void) {
2363
2306
  * will cause it to re-execute when the data changes, updating the DOM elements created within it.
2364
2307
  *
2365
2308
  * Calls to {@link $} inside `func` will append nodes to `parentElement`.
2366
- * You can nest {@link observe} or other {@link $} scopes within `func`.
2309
+ * You can nest {@link derive} or other {@link $} scopes within `func`.
2367
2310
  * Use {@link unmountAll} to clean up all mounted scopes and their DOM nodes.
2368
2311
  *
2369
2312
  * Mounting scopes happens reactively, meaning that if this function is called from within another
2370
- * ({@link observe} or {@link $} or {@link mount}) scope that gets cleaned up, so will the mount.
2313
+ * ({@link derive} or {@link $} or {@link mount}) scope that gets cleaned up, so will the mount.
2371
2314
  *
2372
2315
  * @param parentElement - The native DOM `Element` to which the UI fragment will be appended.
2373
2316
  * @param func - The function that defines the UI fragment, typically containing calls to {@link $}.
@@ -2403,7 +2346,7 @@ export function mount(parentElement: Element, func: () => void) {
2403
2346
 
2404
2347
  /**
2405
2348
  * Removes all Aberdeen-managed DOM nodes and stops all active reactive scopes
2406
- * (created by {@link mount}, {@link observe}, {@link $} with functions, etc.).
2349
+ * (created by {@link mount}, {@link derive}, {@link $} with functions, etc.).
2407
2350
  *
2408
2351
  * This effectively cleans up the entire Aberdeen application state.
2409
2352
  */
@@ -2413,20 +2356,21 @@ export function unmountAll() {
2413
2356
  }
2414
2357
 
2415
2358
  /**
2416
- * Executes a function *without* creating subscriptions in the current reactive scope, and returns its result.
2359
+ * Executes a function or retrieves a value *without* creating subscriptions in the current reactive scope, and returns its result.
2417
2360
  *
2418
- * This is useful when you need to access reactive data inside a reactive scope (like {@link observe})
2361
+ * This is useful when you need to access reactive data inside a reactive scope (like {@link $})
2419
2362
  * but do not want changes to that specific data to trigger a re-execute of the scope.
2363
+ *
2364
+ * Note: You may also use {@link unproxy} to get to the raw underlying data structure, which can be used to similar effect.
2420
2365
  *
2421
- * @template T The type of the return value of your function.
2422
- *
2423
- * @param func - The function to execute without creating subscriptions.
2424
- * @returns Whatever `func` returns.
2366
+ * @param target - Either a function to execute, or an object (which may also be an Array or a Map) to index.
2367
+ * @param key - Optional key/index to use when `target` is an object.
2368
+ * @returns The result of the function call, or the value at `target[key]` when `target` is an object or `target.get(key)` when it's a Map.
2425
2369
  *
2426
- * @example Peeking within observe
2370
+ * @example Peeking within observer
2427
2371
  * ```typescript
2428
2372
  * const data = proxy({ a: 1, b: 2 });
2429
- * observe(() => {
2373
+ * $(() => {
2430
2374
  * // re-executes only when data.a changes, because data.b is peeked.
2431
2375
  * const b = peek(() => data.b);
2432
2376
  * console.log(`A is ${data.a}, B was ${b} when A changed.`);
@@ -2436,10 +2380,20 @@ export function unmountAll() {
2436
2380
  * ```
2437
2381
  *
2438
2382
  */
2439
- export function peek<T>(func: () => T): T {
2383
+
2384
+ export function peek<T extends object>(target: T, key: keyof T): T[typeof key];
2385
+ export function peek<K,V>(target: Map<K,V>, key: K): V | undefined;
2386
+ export function peek<T>(target: T[], key: number): T | undefined;
2387
+ export function peek<T>(target: () => T): T;
2388
+
2389
+ export function peek(target: any, key?: any) {
2440
2390
  peeking++;
2441
2391
  try {
2442
- return func();
2392
+ if (arguments.length === 1) {
2393
+ return target();
2394
+ } else {
2395
+ return (target instanceof Map) ? target.get(key) : target[key];
2396
+ }
2443
2397
  } finally {
2444
2398
  peeking--;
2445
2399
  }
@@ -2450,16 +2404,16 @@ export function map<K, IN, OUT>(
2450
2404
  source: Map<K, IN>,
2451
2405
  func: (value: IN, key: K) => undefined | OUT,
2452
2406
  ): Map<K, OUT>;
2453
- /** When using an object as `source`. */
2454
- export function map<IN, const IN_KEY extends string | number | symbol, OUT>(
2455
- source: Record<IN_KEY, IN>,
2456
- func: (value: IN, index: KeyToString<IN_KEY>) => undefined | OUT,
2457
- ): Record<string | symbol, OUT>;
2458
2407
  /** When using an array as `source`. */
2459
2408
  export function map<IN, OUT>(
2460
2409
  source: Array<IN>,
2461
2410
  func: (value: IN, index: number) => undefined | OUT,
2462
2411
  ): Array<OUT>;
2412
+ /** When using an object as `source`. */
2413
+ export function map<IN, const IN_KEY extends string | number | symbol, OUT>(
2414
+ source: Record<IN_KEY, IN>,
2415
+ func: (value: IN, index: KeyToString<IN_KEY>) => undefined | OUT,
2416
+ ): Record<string | symbol, OUT>;
2463
2417
  /**
2464
2418
  * Reactively maps/filters items from a proxied source array or object to a new proxied array or object.
2465
2419
  *
@@ -2483,7 +2437,7 @@ export function map<IN, OUT>(
2483
2437
  * const doubled = map(numbers, (n) => n * 2);
2484
2438
  * // doubled is proxy([2, 4, 6])
2485
2439
  *
2486
- * observe(() => console.log(doubled)); // Logs updates
2440
+ * $(() => console.log(doubled)); // Logs updates
2487
2441
  * numbers.push(4); // doubled becomes proxy([2, 4, 6, 8])
2488
2442
  * ```
2489
2443
  *
@@ -2497,7 +2451,7 @@ export function map<IN, OUT>(
2497
2451
  *
2498
2452
  * const activeUserNames = map(users, (user) => user.active ? user.name : undefined);
2499
2453
  * // activeUserNames is proxy({ u1: 'Alice', u3: 'Charlie' })
2500
- * observe(() => console.log(Object.values(activeUserNames)));
2454
+ * $(() => console.log(Object.values(activeUserNames)));
2501
2455
  *
2502
2456
  * users.u2.active = true;
2503
2457
  * // activeUserNames becomes proxy({ u1: 'Alice', u2: 'Bob', u3: 'Charlie' })
@@ -2601,9 +2555,9 @@ export function multiMap(
2601
2555
  onEach(source, (item: any, key: symbol | string | number) => {
2602
2556
  const pairs = func(item, key);
2603
2557
  if (pairs) {
2604
- for (const key in pairs) out[key] = pairs[key];
2558
+ for (const key of Object.keys(pairs)) out[key] = pairs[key];
2605
2559
  clean(() => {
2606
- for (const key in pairs) delete out[key];
2560
+ for (const key of Object.keys(pairs)) delete out[key];
2607
2561
  });
2608
2562
  }
2609
2563
  });