informa 3.1.1 → 4.0.1

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 (48) hide show
  1. package/README.md +171 -184
  2. package/dist/EnumerableWeakSet.d.ts +10 -0
  3. package/dist/EnumerableWeakSet.d.ts.map +1 -0
  4. package/dist/EnumerableWeakSet.js +44 -0
  5. package/dist/EnumerableWeakSet.js.map +1 -0
  6. package/dist/high.d.ts +2 -3
  7. package/dist/high.d.ts.map +1 -1
  8. package/dist/high.js +1 -2
  9. package/dist/high.js.map +1 -1
  10. package/dist/internals.d.ts +3 -0
  11. package/dist/internals.d.ts.map +1 -1
  12. package/dist/internals.js +4 -1
  13. package/dist/internals.js.map +1 -1
  14. package/dist/low.d.ts +16 -0
  15. package/dist/low.d.ts.map +1 -1
  16. package/dist/low.js +150 -0
  17. package/dist/low.js.map +1 -1
  18. package/dist/quirks/array.d.ts +4 -3
  19. package/dist/quirks/array.d.ts.map +1 -1
  20. package/dist/quirks/array.js +163 -75
  21. package/dist/quirks/array.js.map +1 -1
  22. package/dist/quirks/basestatified.d.ts +24 -2
  23. package/dist/quirks/basestatified.d.ts.map +1 -1
  24. package/dist/quirks/basestatified.js +240 -91
  25. package/dist/quirks/basestatified.js.map +1 -1
  26. package/dist/quirks/basestatified.test.d.ts +9 -0
  27. package/dist/quirks/basestatified.test.d.ts.map +1 -0
  28. package/dist/quirks/basestatified.test.js +291 -0
  29. package/dist/quirks/basestatified.test.js.map +1 -0
  30. package/dist/quirks/map.d.ts.map +1 -1
  31. package/dist/quirks/map.js +1 -0
  32. package/dist/quirks/map.js.map +1 -1
  33. package/dist/quirks/set.d.ts.map +1 -1
  34. package/dist/quirks/set.js +1 -0
  35. package/dist/quirks/set.js.map +1 -1
  36. package/dist/test.js +2 -6
  37. package/dist/test.js.map +1 -1
  38. package/package.json +2 -1
  39. package/src/EnumerableWeakSet.ts +46 -0
  40. package/src/high.ts +2 -3
  41. package/src/internals.ts +4 -0
  42. package/src/low.ts +120 -1
  43. package/src/quirks/array.ts +168 -92
  44. package/src/quirks/basestatified.test.ts +375 -0
  45. package/src/quirks/basestatified.ts +289 -125
  46. package/src/quirks/map.ts +3 -1
  47. package/src/quirks/set.ts +3 -1
  48. package/src/test.ts +13 -15
@@ -1,176 +1,340 @@
1
+ import { EnumerableWeakMap } from "../EnumerableWeakMap.js";
2
+ import { EnumerableWeakSet } from "../EnumerableWeakSet.js";
1
3
  import {
2
4
  exitProxySymbol,
3
5
  getMetadataOf,
4
6
  getGlobalStateMode,
7
+ isStatified,
8
+ metadataMap,
5
9
  setMetadataOf,
6
10
  statifySealKey,
7
11
  type Statify,
8
12
  } from "../internals.js";
9
13
  import {
10
14
  emitDescendantPathEvents,
15
+ emitCollectionTransition,
11
16
  extract,
12
17
  hook,
18
+ registerPreExtractionHook,
13
19
  StateMetadata,
14
20
  unhook,
15
- type ExitProxyValue,
16
21
  type StatifiableObj,
17
22
  } from "../low.js";
18
23
 
19
24
  type ClassType<T extends any[] = any[], U = object> = new (...args: T) => U;
20
- const classMapMemo = new WeakMap<ClassType, ClassType>();
21
25
 
22
- const blanketProtoMemo = new WeakMap<object, object>();
23
- const blanketProtoSet = new WeakSet<object>();
24
- const blanketDataLayerMemo = new WeakMap<object, object>();
25
-
26
- function getBlanketPrototype(actualProto: object) {
27
- const maybeMemoed = blanketProtoMemo.get(actualProto);
28
- if (maybeMemoed) return maybeMemoed;
29
-
30
- const storage = Object.create(actualProto) as {
31
- [statifySealKey]: true;
32
- [exitProxySymbol]: ExitProxyValue;
33
- };
34
- // storage[statifySealKey] = true;
35
-
36
- const blanket = new Proxy(storage, {
37
- get(target, prop, recv) {
38
- if (prop === statifySealKey) return true;
26
+ // Classes whose instances have been constructed but not yet field-instrumented.
27
+ const pendingAssemblies = new EnumerableWeakSet<object>();
28
+
29
+ // Map from a still-pending child instance → list of (parentMetadata, prop) pairs
30
+ // that need hook() called once the child is reconciled.
31
+ const deferredHooks = new EnumerableWeakMap<object, { metadata: StateMetadata; prop: string | symbol }[]>();
32
+
33
+ // One shim proxy per outermost constructor (new.target).
34
+ const shimCache = new WeakMap<Function, object>();
35
+
36
+ function getPrototypeChainDescriptor(
37
+ obj: object,
38
+ prop: string | symbol,
39
+ ): PropertyDescriptor | undefined {
40
+ let curr: object | null = obj;
41
+ while (curr !== null) {
42
+ const desc = Object.getOwnPropertyDescriptor(curr, prop);
43
+ if (desc) return desc;
44
+ curr = Object.getPrototypeOf(curr);
45
+ }
46
+ return undefined;
47
+ }
39
48
 
40
- if (getGlobalStateMode() === "extract-proxy-path") {
41
- if (prop === exitProxySymbol) {
42
- return { path: [], stateRoot: recv as Statify<StatifiableObj> };
43
- }
49
+ const shimHandler: ProxyHandler<object> = {
50
+ get(target, prop, recv) {
51
+ // Always signal that this object is statified.
52
+ if (prop === statifySealKey) return true;
44
53
 
45
- return extract(undefined, recv as Statify<StatifiableObj>, [prop]);
54
+ if (getGlobalStateMode() === "extract-proxy-path") {
55
+ // Root-level exit: this instance IS the stateRoot.
56
+ if (prop === exitProxySymbol) {
57
+ return { path: [], stateRoot: recv as Statify<StatifiableObj> };
46
58
  }
47
59
 
48
- if (Reflect.has(blanketDataLayerMemo.get(recv)!, prop)) {
49
- return Reflect.get(blanketDataLayerMemo.get(recv)!, prop);
60
+ // Safety net: reconcile if somehow still pending at extraction time.
61
+ if (pendingAssemblies.has(recv as object)) {
62
+ reconcileInstance(recv as object);
50
63
  }
51
64
 
52
- return Reflect.get(target, prop, recv);
53
- },
65
+ // Forward to extract() so the path is captured.
66
+ // Own accessor properties on recv are already handling extract-proxy-path
67
+ // themselves; the shim is only reached for prototype-level props.
68
+ return extract(
69
+ Reflect.get(target, prop, recv),
70
+ recv as Statify<StatifiableObj>,
71
+ [prop],
72
+ );
73
+ }
54
74
 
55
- set(target, prop, newVal, recv) {
56
- const stateMetadata = getMetadataOf(recv as Statify<StatifiableObj>);
75
+ return Reflect.get(target, prop, recv);
76
+ },
57
77
 
58
- const had = Reflect.has(recv, prop);
59
- const oldVal = Reflect.get(recv, prop) as unknown;
78
+ set(target, prop, newVal, recv) {
79
+ // This trap fires only when `prop` is NOT an own property of `recv`.
80
+ // That covers:
81
+ // (a) Pre-reconciliation field initializer writes → create own data prop.
82
+ // (b) Prototype accessor writes (e.g. `get state()` / `set state()`).
83
+ // (c) Genuinely new dynamic property additions.
84
+ //
85
+ // Own accessor properties installed by reconcileInstance are handled
86
+ // by their own setters and never reach here.
60
87
 
61
- let result;
62
- const desc = Reflect.getOwnPropertyDescriptor(target, prop);
63
- if (desc === undefined || "value" in desc) {
64
- result = Reflect.set(blanketDataLayerMemo.get(recv)!, prop, newVal);
65
- } else {
66
- result = Reflect.set(target, prop, newVal, recv);
88
+ // Check for a prototype-level accessor setter BEFORE calling Reflect.set.
89
+ const protoDesc = getPrototypeChainDescriptor(target, prop);
90
+
91
+ // Delegate the actual assignment.
92
+ const result = Reflect.set(target, prop, newVal, recv);
93
+
94
+ // If this was a prototype accessor write and the instance has metadata,
95
+ // emit replacement events so subscribers fire.
96
+ if (result && protoDesc?.set && metadataMap.has(recv as Statify<StatifiableObj>)) {
97
+ const stateMetadata = getMetadataOf(recv as Statify<StatifiableObj>);
98
+ stateMetadata.emit("replaceProp", newVal, prop);
99
+ const ee = stateMetadata.eventEmitterAtPathMaybe([prop]);
100
+ if (ee) {
101
+ ee.emit("replace", newVal);
102
+ emitDescendantPathEvents(stateMetadata, [prop], newVal, true);
67
103
  }
104
+ }
68
105
 
69
- try {
70
- if (result) {
71
- if (
72
- typeof newVal === "object"
73
- && newVal != null
74
- && (newVal as any)[statifySealKey]
75
- ) {
76
- hook(
77
- stateMetadata,
78
- prop,
79
- getMetadataOf(newVal as Statify<StatifiableObj>),
80
- );
81
- }
106
+ return result;
107
+ },
108
+
109
+ has(target, prop) {
110
+ if (prop === statifySealKey || prop === exitProxySymbol) return true;
111
+ return Reflect.has(target, prop);
112
+ },
113
+ };
114
+
115
+ function getOrCreateShim(ctor: Function): object {
116
+ const cached = shimCache.get(ctor);
117
+ if (cached) return cached;
118
+
119
+ // shimTarget's [[Prototype]] = ctor.prototype, so the full chain is:
120
+ // instance → shim → shimTarget → ctor.prototype → … → Superclass.prototype
121
+ // This preserves instanceof for all classes in the chain.
122
+ const shimTarget = Object.create((ctor as ClassType).prototype) as object;
123
+ const shim = new Proxy(shimTarget, shimHandler);
124
+ shimCache.set(ctor, shim);
125
+ return shim;
126
+ }
82
127
 
83
- if (had) {
84
- if (
85
- oldVal !== newVal
86
- && typeof oldVal === "object"
87
- && oldVal != null
88
- && (oldVal as any)[statifySealKey]
89
- ) {
90
- unhook(
91
- stateMetadata,
92
- prop,
93
- getMetadataOf(oldVal as Statify<StatifiableObj>),
94
- );
95
- }
96
-
97
- stateMetadata.emit("replaceProp", newVal, prop);
98
- }
128
+ /**
129
+ * Converts all enumerable, configurable own data properties of `instance` into
130
+ * accessor-backed reactive properties connected to `instance`'s StateMetadata.
131
+ *
132
+ * Idempotent: returns immediately if the instance is not in pendingAssemblies.
133
+ */
134
+ function reconcileInstance(instance: object): void {
135
+ if (!pendingAssemblies.has(instance)) return;
136
+ pendingAssemblies.delete(instance);
137
+
138
+ // Drain any deferred hooks where this instance was the pending child.
139
+ const deferred = deferredHooks.get(instance);
140
+ if (deferred) {
141
+ deferredHooks.delete(instance);
142
+ const childMetadata = getMetadataOf(instance as Statify<StatifiableObj>);
143
+ for (const { metadata: parentMetadata, prop } of deferred) {
144
+ hook(parentMetadata, prop, childMetadata);
145
+ }
146
+ }
99
147
 
100
- stateMetadata.emit("setProp", newVal, prop);
148
+ // Metadata was installed eagerly in ExtractionShimBase's constructor.
149
+ const metadata = getMetadataOf(instance as Statify<StatifiableObj>);
101
150
 
102
- const maybeEventEmitterAtPath = stateMetadata.eventEmitterAtPathMaybe([prop]);
103
- if (maybeEventEmitterAtPath) {
104
- if (had) {
105
- maybeEventEmitterAtPath.emit("replace", newVal);
106
- }
151
+ const isArray = Array.isArray(instance);
107
152
 
108
- maybeEventEmitterAtPath.emit("set", newVal);
153
+ const props: (string | symbol)[] = [
154
+ ...Object.getOwnPropertyNames(instance),
155
+ ...Object.getOwnPropertySymbols(instance),
156
+ ];
109
157
 
110
- emitDescendantPathEvents(
111
- stateMetadata,
112
- [prop],
113
- newVal,
114
- had,
115
- );
116
- }
117
- }
118
- } finally {
119
- return result;
120
- }
121
- },
158
+ for (const prop of props) {
159
+ // Skip Informa internal symbols.
160
+ if (prop === statifySealKey || prop === exitProxySymbol) continue;
122
161
 
123
- has(target, prop) {
124
- if (getGlobalStateMode() === "extract-proxy-path" && prop === exitProxySymbol) {
125
- return true;
126
- }
162
+ // Skip array numeric indices — managed by the array's own push/pop/splice overrides.
163
+ if (isArray && typeof prop === "string" && Number.isInteger(Number(prop))) continue;
127
164
 
128
- return Reflect.has(target, prop);
129
- },
130
- });
165
+ const desc = Object.getOwnPropertyDescriptor(instance, prop)!;
131
166
 
132
- blanketProtoMemo.set(actualProto, blanket);
133
- blanketProtoSet.add(blanket);
167
+ // Non-configurable: cannot be redefined; leave non-reactive.
168
+ // Note: these properties retain their value but won't participate in the
169
+ // observable graph. This is intentional and tested.
170
+ if (!desc.configurable) continue;
134
171
 
135
- return blanket;
136
- }
172
+ // Existing accessor (get/set): do not double-wrap.
173
+ if ("get" in desc || "set" in desc) continue;
137
174
 
138
- export function makeStatified<
139
- V extends ClassType<T, U>,
140
- T extends any[],
141
- U extends object,
142
- >(
143
- OriginalClass: V,
144
- ): V {
145
- const maybeMemoed = classMapMemo.get(OriginalClass);
146
- if (maybeMemoed) return maybeMemoed as V;
175
+ // ---- Instrument this configurable data property ----
176
+ let value: unknown = desc.value;
147
177
 
148
- // @ts-ignore - ts-2545 "A mixin class must have a constructor with a single rest parameter of type 'any[]'."
149
- class Statified extends OriginalClass {
150
- constructor(...args: T) {
151
- super(...args);
178
+ // Hook the initial value if it is itself statified.
179
+ if (
180
+ value != null &&
181
+ typeof value === "object" &&
182
+ isStatified(value as StatifiableObj)
183
+ ) {
184
+ if (pendingAssemblies.has(value as object)) {
185
+ // The nested value is itself pending (constructed during this constructor
186
+ // but not yet reconciled — its metadata doesn't exist yet). Defer the
187
+ // hook: it will be wired once reconcileInstance runs for that child.
188
+ deferredHooks.getOrInsertComputed(value as object, () => []).push({ metadata, prop });
189
+ } else {
190
+ hook(metadata, prop, getMetadataOf(value as Statify<StatifiableObj>));
191
+ }
192
+ }
152
193
 
153
- const ctor = new.target ?? Statified;
154
- const inst = Reflect.construct(OriginalClass, args, ctor) as Statify<U>;
194
+ Object.defineProperty(instance, prop, {
195
+ get(this: object) {
196
+ if (getGlobalStateMode() === "extract-proxy-path") {
197
+ return extract(value, this as Statify<StatifiableObj>, [prop]);
198
+ }
199
+ return value;
200
+ },
155
201
 
156
- setMetadataOf(inst, new StateMetadata());
202
+ set(this: object, next: unknown) {
203
+ const old = value;
204
+ value = next;
157
205
 
158
- blanketDataLayerMemo.set(inst, {});
206
+ if (old !== next) {
207
+ if (
208
+ old != null &&
209
+ typeof old === "object" &&
210
+ isStatified(old as StatifiableObj)
211
+ ) {
212
+ unhook(metadata, prop, getMetadataOf(old as Statify<StatifiableObj>));
213
+ }
159
214
 
160
- const actualProto = ctor.prototype;
161
- const finalProto = blanketProtoSet.has(actualProto)
162
- ? actualProto
163
- : getBlanketPrototype(actualProto);
215
+ if (
216
+ next != null &&
217
+ typeof next === "object" &&
218
+ isStatified(next as StatifiableObj)
219
+ ) {
220
+ hook(metadata, prop, getMetadataOf(next as Statify<StatifiableObj>));
221
+ }
164
222
 
165
- Reflect.setPrototypeOf(inst, finalProto);
223
+ emitCollectionTransition(old, next);
224
+ }
166
225
 
167
- return inst;
168
- }
169
- };
226
+ // Enter the existing replacement machinery — same path as statifyObject's
227
+ // set trap in low.ts. "listen → replace → fire" is preserved because this
228
+ // accessor is installed before any external mutation can reach the field.
229
+ metadata.emit("replaceProp", next, prop);
230
+
231
+ const ee = metadata.eventEmitterAtPathMaybe([prop]);
232
+ if (ee) {
233
+ ee.emit("replace", next);
234
+ emitDescendantPathEvents(metadata, [prop], next, true);
235
+ }
236
+ },
237
+
238
+ enumerable: desc.enumerable ?? true,
239
+ configurable: true,
240
+ });
241
+ }
242
+
243
+ // Selector/stateRoot identity: verified correct. Every on*()/off*() call goes
244
+ // through selectorToRootAndPath(), which fires preExtractionHooks() before
245
+ // entering extract-proxy-path mode. The preExtractionHook registered below
246
+ // reconciles all pending instances first — so by the time the selector function
247
+ // runs, every pending `this.x` field is already an accessor that returns
248
+ // extract(value, this, ['x']). Path chains like `() => this.a.b.c` are therefore
249
+ // built up correctly, and stateRoot is the raw instance, which is the same object
250
+ // keyed in metadataMap. No re-mapping needed.
251
+ }
170
252
 
171
- classMapMemo.set(OriginalClass, Statified);
253
+ // Before selectorToRootAndPath enters extract-proxy-path mode it calls all
254
+ // registered hooks. This hook reconciles every pending instance so that their
255
+ // own data properties are already accessor-backed when the selector runs.
256
+
257
+ registerPreExtractionHook(() => {
258
+ // Snapshot: reconcileInstance removes from pendingAssemblies, which mutates
259
+ // the set during iteration — take a copy first.
260
+ const snapshot = [...pendingAssemblies];
261
+ for (const instance of snapshot) {
262
+ reconcileInstance(instance);
263
+ }
264
+ });
265
+
266
+ /**
267
+ * Wraps a user-defined class factory in the Informa observable lifecycle.
268
+ *
269
+ * Usage:
270
+ * ```ts
271
+ * const MyClass = statifyClass(
272
+ * (Base) => class MyClass extends Base {
273
+ * name = "default";
274
+ * constructor(name: string) { super(); this.name = name; }
275
+ * },
276
+ * Object,
277
+ * );
278
+ * ```
279
+ *
280
+ * Construction lifecycle:
281
+ * 1. ExtractionShimBase constructor runs → metadata installed, shim set as prototype.
282
+ * 2. UserSubclass field initializers run → own data properties created on instance.
283
+ * 3. UserSubclass constructor body runs.
284
+ * 4. Instance is in pendingAssemblies (field instrumentation deferred).
285
+ * 5. On first `on()`/`off()` call → registerPreExtractionHook fires → reconcileInstance
286
+ * converts own data properties to reactive accessor-backed properties.
287
+ */
288
+ export function statifyClass<
289
+ SubclassType extends ClassType<ArgsSub, OutSub>,
290
+ ArgsSub extends any[],
291
+ OutSub extends object,
292
+ SuperclassType extends ClassType<ArgsSuper, OutSuper>,
293
+ ArgsSuper extends any[],
294
+ OutSuper extends object,
295
+ >(
296
+ makeSubclass: (SuperclassIn: SuperclassType) => SubclassType,
297
+ Superclass: SuperclassType,
298
+ ): SubclassType & ClassType<ArgsSub, Statify<OutSub>> {
299
+
300
+ // ExtractionShimBase sits between Superclass and the user's class.
301
+ // Its constructor:
302
+ // - Installs StateMetadata on the instance eagerly (so subclasses like
303
+ // StatifiedArray can call getMetadataOf() from their constructor bodies).
304
+ // - Marks the instance as pending for lazy field instrumentation.
305
+ // - Inserts the shim proxy as the instance's [[Prototype]], enabling
306
+ // statifySealKey / exitProxySymbol / extract-proxy-path handling.
307
+ //
308
+ // Guard: in nested statifyClass chains (e.g. statifyClass(..., A) where A
309
+ // was itself produced by statifyClass), multiple ExtractionShimBase
310
+ // constructors run. The guard ensures setup happens exactly once — at the
311
+ // innermost (deepest) ExtractionShimBase call.
312
+ class ExtractionShimBase extends (Superclass as unknown as typeof Object) {
313
+ constructor(...args: any[]) {
314
+ super(...(args as []));
315
+
316
+ if (!pendingAssemblies.has(this)) {
317
+ setMetadataOf(
318
+ this as unknown as Statify<StatifiableObj>,
319
+ new StateMetadata(),
320
+ );
321
+ pendingAssemblies.add(this);
322
+ }
172
323
 
173
- return Statified;
324
+ // Always update the shim to match the outermost constructor (new.target).
325
+ // In nested chains the innermost ExtractionShimBase sets the shim first;
326
+ // subsequent (shallower) ones overwrite it with the same value since
327
+ // new.target propagates as the outermost class throughout the chain.
328
+ const ctor = (new.target ?? ExtractionShimBase) as Function;
329
+ Reflect.setPrototypeOf(this, getOrCreateShim(ctor));
330
+ }
331
+ }
332
+
333
+ // Invoke the user's factory with the shim base, then return the result.
334
+ // No outer Statified wrapper is needed: field instrumentation is lazy,
335
+ // triggered by registerPreExtractionHook before any selectorToRootAndPath call.
336
+ return makeSubclass(
337
+ ExtractionShimBase as unknown as SuperclassType,
338
+ ) as unknown as SubclassType & ClassType<ArgsSub, Statify<OutSub>>;
174
339
  }
175
340
 
176
- export const BaseStatified = makeStatified(Object);
package/src/quirks/map.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { exitProxySymbol, getGlobalStateMode, setMetadataOf, Statify, statifySealKey } from "../internals.js";
1
+ import { exitProxySymbol, getGlobalStateMode, isStatifiedMapKey, setMetadataOf, Statify, statifySealKey } from "../internals.js";
2
2
  import { ExitProxyValue, StateMetadata, StatifiableObj } from "../low.js";
3
3
 
4
4
  export class StatifiedMap<K, V> extends Map<K, V> implements Statify<Map<K, V>> {
@@ -58,3 +58,5 @@ export class StatifiedMap<K, V> extends Map<K, V> implements Statify<Map<K, V>>
58
58
  return this;
59
59
  }
60
60
  }
61
+
62
+ (StatifiedMap.prototype as any)[isStatifiedMapKey] = true;
package/src/quirks/set.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { exitProxySymbol, getGlobalStateMode, setMetadataOf, statifySealKey, type Statify } from "../internals.js";
1
+ import { exitProxySymbol, getGlobalStateMode, isStatifiedSetKey, setMetadataOf, statifySealKey, type Statify } from "../internals.js";
2
2
  import { StateMetadata, StatifiableObj, type ExitProxyValue, type StatifiableProp } from "../low.js";
3
3
 
4
4
  export class StatifiedSet<T extends StatifiableProp> extends Set<T> implements Statify<Set<T>> {
@@ -51,3 +51,5 @@ export class StatifiedSet<T extends StatifiableProp> extends Set<T> implements S
51
51
  return false;
52
52
  }
53
53
  }
54
+
55
+ (StatifiedSet.prototype as any)[isStatifiedSetKey] = true;
package/src/test.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import $ from "./high.js";
2
- import { BaseStatified } from "./quirks/basestatified.js";
3
2
  import { StatifiedSet } from "./quirks/set.js";
4
3
 
5
4
  const stateful = $.state<{ a: ({ d: number })[], b?: { c?: { d?: Set<number> } } }>({ a: [] });
@@ -27,20 +26,19 @@ const asdf2 = $.state({ c: asdf });
27
26
  // const aassddff = $.state(new Set());
28
27
  stateful.b = asdf2;
29
28
 
30
- class Wayland extends BaseStatified {
31
- displays = new StatifiedSet();
32
-
33
- #state = 0;
34
- get state() { return this.#state; }
35
- set state(v: number) {
36
- console.log("set", v)
37
- this.#state = v;
38
- }
39
-
40
- constructor() {
41
- super();
42
- }
43
- }
29
+ const Wayland = $.statifyClass(
30
+ (Base) => class Wayland extends Base {
31
+ displays = new StatifiedSet();
32
+
33
+ #state = 0;
34
+ get state() { return this.#state; }
35
+ set state(v: number) {
36
+ console.log("set", v);
37
+ this.#state = v;
38
+ }
39
+ },
40
+ Object,
41
+ );
44
42
 
45
43
  const w = new Wayland();
46
44
  $.onSet(() => w.state, (v) => console.log("asdf awawa!!", v));