Skip to content

Commit e2bb17a

Browse files
author
Frank van Viegen
committed
v1.13.0: add OPAQUE symbol, deprecate NO_COPY
1 parent f06905c commit e2bb17a

4 files changed

Lines changed: 80 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
### 1.13.0 (2026-05-20)
4+
5+
**New features:**
6+
- Added `OPAQUE` symbol. Set to `true` to make an object fully opaque (not proxied, not deep-copied); set to `false` to suppress deep-copying while still allowing reactive observation.
7+
- `NO_COPY` is now a deprecated alias for `OPAQUE`.
8+
39
### 1.12.1 (2026-04-24)
410

511
**Fixes:**

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aberdeen",
3-
"version": "1.12.1",
3+
"version": "1.13.0",
44
"author": "Frank van Viegen",
55
"main": "dist/src/aberdeen.js",
66
"devDependencies": {

‎src/aberdeen.ts‎

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,7 +1423,7 @@ function optProxy(value: any): any {
14231423
typeof value !== "object" ||
14241424
!value ||
14251425
value[TARGET_SYMBOL] !== undefined ||
1426-
NO_COPY in value
1426+
value[OPAQUE]
14271427
) {
14281428
return value;
14291429
}
@@ -1721,7 +1721,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
17211721
}
17221722
else if (dstValue !== srcValue) {
17231723
if (typeof srcValue === "object" && srcValue !== null) {
1724-
if (typeof dstValue === "object" && dstValue !== null && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1724+
if (typeof dstValue === "object" && dstValue !== null && srcValue.constructor === dstValue.constructor && !(OPAQUE in srcValue)) {
17251725
changed = copyRecursive(dstValue, srcValue, flags) || changed;
17261726
continue;
17271727
}
@@ -1757,7 +1757,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
17571757
if (dstValue === undefined && !dst.has(key)) dstValue = EMPTY;
17581758
if (dstValue !== srcValue) {
17591759
if (typeof srcValue === "object" && srcValue !== null) {
1760-
if (typeof dstValue === "object" && dstValue !== null && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1760+
if (typeof dstValue === "object" && dstValue !== null && srcValue.constructor === dstValue.constructor && !(OPAQUE in srcValue)) {
17611761
changed = copyRecursive(dstValue, srcValue, flags) || changed;
17621762
continue;
17631763
}
@@ -1789,7 +1789,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
17891789
const dstValue = dst.hasOwnProperty(key) ? dst[key] : EMPTY;
17901790
if (dstValue !== srcValue) {
17911791
if (typeof srcValue === "object" && srcValue !== null) {
1792-
if (typeof dstValue === "object" && dstValue !== null && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1792+
if (typeof dstValue === "object" && dstValue !== null && srcValue.constructor === dstValue.constructor && !(OPAQUE in srcValue)) {
17931793
changed = copyRecursive(dstValue as typeof srcValue, srcValue, flags) || changed;
17941794
continue;
17951795
}
@@ -1826,14 +1826,29 @@ const COPY_SUBSCRIBE = 32;
18261826
const COPY_EMIT = 64;
18271827

18281828
/**
1829-
* A symbol that can be added to an object to prevent it from being cloned by {@link clone} or {@link copy}.
1830-
* This is useful for objects that should be shared by reference. That also mean that their contents won't
1831-
* be observed for changes.
1829+
* A symbol that controls how Aberdeen handles an object in copy operations and proxy wrapping.
1830+
*
1831+
* The **presence** of this symbol (regardless of its value) prevents deep-copying: the object is
1832+
* stored and passed by reference in {@link clone} and {@link copy}.
1833+
*
1834+
* The **value** of the symbol controls proxy wrapping when the object is read from reactive state:
1835+
* - **Truthy** (e.g. `true`): the object is fully opaque — it is not wrapped in a proxy, so its
1836+
* properties are not observable. Use this for objects that break when proxied (e.g. class instances
1837+
* with internal slots, Promises) or that must be invisible to Aberdeen's reactive system.
1838+
* - **Falsy** (e.g. `false`): the object is still wrapped in a proxy, so reads on its properties
1839+
* create reactive dependencies as normal — only deep-copying is suppressed.
1840+
*/
1841+
export const OPAQUE = Symbol("OPAQUE");
1842+
1843+
/**
1844+
* Use {@link OPAQUE} instead. This is an alias kept for backward compatibility.
1845+
*
1846+
* @deprecated
18321847
*/
1833-
export const NO_COPY = Symbol("NO_COPY");
1848+
export const NO_COPY = OPAQUE;
18341849

1835-
// Promises break when proxied, so we'll just mark them as NO_COPY
1836-
(Promise.prototype as any)[NO_COPY] = true;
1850+
// Promises break when proxied, so mark them as fully opaque
1851+
(Promise.prototype as any)[OPAQUE] = true;
18371852

18381853
/**
18391854
* A reactive object containing CSS variable definitions.
@@ -1961,7 +1976,7 @@ export function darkMode(): boolean {
19611976

19621977
// Simple recursive clone - no destination checking needed
19631978
function cloneRecursive<T extends object>(src: T, flags: number): T {
1964-
if (NO_COPY in src) return src;
1979+
if (OPAQUE in src) return src;
19651980
if (flags & COPY_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
19661981

19671982
if (src instanceof Array) {
@@ -3299,8 +3314,8 @@ export function dump<T>(data: T): T {
32993314
if (data && typeof data === "object") {
33003315
const name = data.constructor.name.toLowerCase() || "unknown object";
33013316
A(`#<${name}>`);
3302-
if (NO_COPY in data ) {
3303-
A("# [NO_COPY]");
3317+
if (OPAQUE in data) {
3318+
A("# [OPAQUE]");
33043319
} else {
33053320
A("ul", () => {
33063321
onEach(data as any, (value, key) => {
@@ -3422,6 +3437,7 @@ export default Object.assign(A, {
34223437
/** {@inheritDoc merge} */ merge,
34233438
/** {@inheritDoc mount} */ mount,
34243439
/** {@inheritDoc multiMap} */ multiMap,
3440+
/** {@inheritDoc OPAQUE} */ OPAQUE,
34253441
/** {@inheritDoc NO_COPY} */ NO_COPY,
34263442
/** {@inheritDoc onEach} */ onEach,
34273443
/** {@inheritDoc partition} */ partition,

‎tests/copy.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,47 @@ test('A.clone subscribes to all deeply nested values', async () => {
137137
expect(cnt).toBe(2); // Should have re-run because we subscribed to the nested value
138138
});
139139

140+
test('OPAQUE=true prevents proxying and reactive tracking', async () => {
141+
const inner = { x: 1 } as any;
142+
inner[A.OPAQUE] = true;
143+
const state = A.proxy({ obj: inner });
144+
145+
// state.obj should be the raw object — not wrapped in a proxy
146+
expect(state.obj).toBe(inner);
147+
148+
// Reading state.obj.x should NOT create a reactive dependency
149+
let cnt = 0;
150+
A(() => { (state.obj as any).x; cnt++; });
151+
expect(cnt).toBe(1);
152+
inner.x = 2;
153+
await passTime();
154+
expect(cnt).toBe(1); // scope did not re-run
155+
156+
// A.clone returns the same reference, not a deep copy
157+
expect((A.clone(state) as any).obj).toBe(inner);
158+
});
159+
160+
test('OPAQUE=false prevents deep-copy but still allows reactive tracking', async () => {
161+
const inner = { x: 1 } as any;
162+
inner[A.OPAQUE] = false;
163+
const state = A.proxy({ obj: inner });
164+
165+
// state.obj should be a proxy wrapping inner
166+
expect(A.unproxy(state.obj)).toBe(inner);
167+
168+
// Reading state.obj.x SHOULD create a reactive dependency
169+
let result = 0;
170+
let cnt = 0;
171+
A(() => { result = (state.obj as any).x; cnt++; });
172+
expect(cnt).toBe(1);
173+
expect(result).toBe(1);
174+
175+
(state.obj as any).x = 2;
176+
await passTime();
177+
expect(cnt).toBe(2); // scope re-ran
178+
expect(result).toBe(2);
179+
180+
// A.clone still returns the inner object by reference (not deep-copied)
181+
expect((A.clone(state) as any).obj).toBe(inner);
182+
});
183+

0 commit comments

Comments
 (0)