informa 4.0.1 → 5.0.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/low.ts CHANGED
@@ -5,6 +5,7 @@ import { isListInGrid } from "./utils.js";
5
5
  import { EnumerableWeakMap } from "./EnumerableWeakMap.js";
6
6
  import { StatifiedMap } from "./quirks/map.js";
7
7
  import { StatifiedArray } from "./quirks/array.js";
8
+ import { clearPendingAssemblies } from "./quirks/basestatified.js";
8
9
 
9
10
  type ProxyEventEmitterEvents = {
10
11
  "set": [any],
@@ -157,30 +158,15 @@ export type Statified<T extends StatifiableProp> = T extends object
157
158
  : Statify<{ [K in keyof T]: Statified<T[K]> }>
158
159
  : T;
159
160
 
160
- /**
161
- * Emit the appropriate structural events when a field transitions from `old` to
162
- * `next`. Called after unhook/hook so the event graph is already up-to-date.
163
- *
164
- * Rules:
165
- * Set → Set : symmetric diff (deleteItem for removed, addItem for added)
166
- * Set → other : wipe old set (deleteItem every item)
167
- * other→ Set : introduce new set (addItem every item)
168
- * Array→ other : wipe old array (spliceOutElement every item)
169
- * other→ Array : introduce new array (spliceInElement every item)
170
- * Map → Map : diff entries (deleteEntry, replaceEntry, addEntry)
171
- * Map → other : wipe old map (deleteEntry every entry)
172
- * other→ Map : introduce new map (addEntry every entry)
173
- */
174
161
  export function emitCollectionTransition(old: unknown, next: unknown): void {
175
- const oldIsSet = old != null && typeof old === "object" && (old as any)[isStatifiedSetKey];
176
- const nextIsSet = next != null && typeof next === "object" && (next as any)[isStatifiedSetKey];
177
- const oldIsArray = old != null && typeof old === "object" && (old as any)[isStatifiedArrayKey];
178
- const nextIsArray= next != null && typeof next === "object" && (next as any)[isStatifiedArrayKey];
179
- const oldIsMap = old != null && typeof old === "object" && (old as any)[isStatifiedMapKey];
180
- const nextIsMap = next != null && typeof next === "object" && (next as any)[isStatifiedMapKey];
181
-
182
- // ---- Set transitions ----
183
- if (oldIsSet && nextIsSet) {
162
+ const oldIsSet = old != null && typeof old === "object" && (old as any)[isStatifiedSetKey];
163
+ const nextIsSet = next != null && typeof next === "object" && (next as any)[isStatifiedSetKey];
164
+ const oldIsArray = old != null && typeof old === "object" && (old as any)[isStatifiedArrayKey];
165
+ const nextIsArray = next != null && typeof next === "object" && (next as any)[isStatifiedArrayKey];
166
+ const oldIsMap = old != null && typeof old === "object" && (old as any)[isStatifiedMapKey];
167
+ const nextIsMap = next != null && typeof next === "object" && (next as any)[isStatifiedMapKey];
168
+
169
+ if (oldIsSet && nextIsSet && old !== next) {
184
170
  const oldSet = old as StatifiedSet<unknown>;
185
171
  const nextSet = next as StatifiedSet<unknown>;
186
172
  const oldMeta = getMetadataOf(oldSet as Statify<{}>);
@@ -193,15 +179,13 @@ export function emitCollectionTransition(old: unknown, next: unknown): void {
193
179
  }
194
180
  if (changed) oldMeta.emit("cardChanged");
195
181
  return;
196
- }
197
- if (oldIsSet) {
182
+ } else if (oldIsSet) {
198
183
  const oldSet = old as StatifiedSet<unknown>;
199
184
  const oldMeta = getMetadataOf(oldSet as Statify<{}>);
200
185
  let changed = false;
201
186
  for (const item of oldSet) { oldMeta.emit("deleteItem", item); changed = true; }
202
187
  if (changed) oldMeta.emit("cardChanged");
203
- }
204
- if (nextIsSet) {
188
+ } else if (nextIsSet) {
205
189
  const nextSet = next as StatifiedSet<unknown>;
206
190
  const nextMeta = getMetadataOf(nextSet as Statify<{}>);
207
191
  let changed = false;
@@ -209,30 +193,30 @@ export function emitCollectionTransition(old: unknown, next: unknown): void {
209
193
  if (changed) nextMeta.emit("cardChanged");
210
194
  }
211
195
 
212
- // ---- Array transitions ----
213
- if (oldIsArray && !nextIsArray) {
214
- const oldArr = old as StatifiedArray<unknown>;
215
- const oldMeta = getMetadataOf(oldArr as Statify<{}>);
216
- if (oldArr.length > 0) {
217
- for (let i = oldArr.length - 1; i >= 0; i--) {
218
- oldMeta.emit("spliceOutElement", oldArr[i], i);
196
+ if (old !== next) {
197
+ if (oldIsArray) {
198
+ const oldArr = old as StatifiedArray<unknown>;
199
+ const oldMeta = getMetadataOf(oldArr as Statify<{}>);
200
+ if (oldArr.length > 0) {
201
+ for (let i = oldArr.length - 1; i >= 0; i--) {
202
+ oldMeta.emit("spliceOutElement", oldArr[i], i);
203
+ }
204
+ oldMeta.emit("lengthChanged");
219
205
  }
220
- oldMeta.emit("lengthChanged");
221
206
  }
222
- }
223
- if (nextIsArray && !oldIsArray) {
224
- const nextArr = next as StatifiedArray<unknown>;
225
- const nextMeta = getMetadataOf(nextArr as Statify<{}>);
226
- if (nextArr.length > 0) {
227
- for (let i = 0; i < nextArr.length; i++) {
228
- nextMeta.emit("spliceInElement", nextArr[i], i);
207
+ if (nextIsArray) {
208
+ const nextArr = next as StatifiedArray<unknown>;
209
+ const nextMeta = getMetadataOf(nextArr as Statify<{}>);
210
+ if (nextArr.length > 0) {
211
+ for (let i = 0; i < nextArr.length; i++) {
212
+ nextMeta.emit("spliceInElement", nextArr[i], i);
213
+ }
214
+ nextMeta.emit("lengthChanged");
229
215
  }
230
- nextMeta.emit("lengthChanged");
231
216
  }
232
217
  }
233
218
 
234
- // ---- Map transitions ----
235
- if (oldIsMap && nextIsMap) {
219
+ if (oldIsMap && nextIsMap && old !== next) {
236
220
  const oldMap = old as StatifiedMap<unknown, unknown>;
237
221
  const nextMap = next as StatifiedMap<unknown, unknown>;
238
222
  const oldMeta = getMetadataOf(oldMap as Statify<{}>);
@@ -246,15 +230,13 @@ export function emitCollectionTransition(old: unknown, next: unknown): void {
246
230
  }
247
231
  if (changed) oldMeta.emit("sizeChanged");
248
232
  return;
249
- }
250
- if (oldIsMap) {
233
+ } else if (oldIsMap) {
251
234
  const oldMap = old as StatifiedMap<unknown, unknown>;
252
235
  const oldMeta = getMetadataOf(oldMap as Statify<{}>);
253
236
  let changed = false;
254
237
  for (const [k, v] of oldMap) { oldMeta.emit("deleteEntry", k, v); changed = true; }
255
238
  if (changed) oldMeta.emit("sizeChanged");
256
- }
257
- if (nextIsMap) {
239
+ } else if (nextIsMap) {
258
240
  const nextMap = next as StatifiedMap<unknown, unknown>;
259
241
  const nextMeta = getMetadataOf(nextMap as Statify<{}>);
260
242
  let changed = false;
@@ -440,16 +422,8 @@ export function extract(
440
422
  });
441
423
  }
442
424
 
443
- const preExtractionHooks = new Set<() => void>();
444
- export function registerPreExtractionHook(fn: () => void): () => void {
445
- preExtractionHooks.add(fn);
446
- return () => preExtractionHooks.delete(fn);
447
- }
448
-
449
425
  export function selectorToRootAndPath(selector: () => Statify<StatifiableObj>) {
450
- // Reconcile all pending class instances before entering extract-proxy-path mode.
451
- // This ensures field accessors are installed so path extraction resolves correctly.
452
- for (const hook of preExtractionHooks) hook();
426
+ clearPendingAssemblies();
453
427
 
454
428
  setGlobalStateMode("extract-proxy-path");
455
429
 
@@ -1,11 +1,8 @@
1
- import { EnumerableWeakMap } from "../EnumerableWeakMap.js";
2
1
  import { EnumerableWeakSet } from "../EnumerableWeakSet.js";
3
2
  import {
4
3
  exitProxySymbol,
5
4
  getMetadataOf,
6
5
  getGlobalStateMode,
7
- isStatified,
8
- metadataMap,
9
6
  setMetadataOf,
10
7
  statifySealKey,
11
8
  type Statify,
@@ -15,7 +12,6 @@ import {
15
12
  emitCollectionTransition,
16
13
  extract,
17
14
  hook,
18
- registerPreExtractionHook,
19
15
  StateMetadata,
20
16
  unhook,
21
17
  type StatifiableObj,
@@ -23,14 +19,8 @@ import {
23
19
 
24
20
  type ClassType<T extends any[] = any[], U = object> = new (...args: T) => U;
25
21
 
26
- // Classes whose instances have been constructed but not yet field-instrumented.
27
22
  const pendingAssemblies = new EnumerableWeakSet<object>();
28
23
 
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
24
  const shimCache = new WeakMap<Function, object>();
35
25
 
36
26
  function getPrototypeChainDescriptor(
@@ -46,295 +36,176 @@ function getPrototypeChainDescriptor(
46
36
  return undefined;
47
37
  }
48
38
 
49
- const shimHandler: ProxyHandler<object> = {
50
- get(target, prop, recv) {
51
- // Always signal that this object is statified.
52
- if (prop === statifySealKey) return true;
39
+ function clearAssembly(assembly: object) {
40
+ for (const key of Reflect.ownKeys(assembly)) {
41
+ const value = Reflect.get(assembly, key);
53
42
 
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> };
58
- }
43
+ Reflect.deleteProperty(assembly, key);
59
44
 
60
- // Safety net: reconcile if somehow still pending at extraction time.
61
- if (pendingAssemblies.has(recv as object)) {
62
- reconcileInstance(recv as object);
63
- }
45
+ (assembly as Record<string | symbol, unknown>)[key] = value;
46
+ }
64
47
 
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
- }
48
+ console.log(Reflect.ownKeys(assembly));
74
49
 
75
- return Reflect.get(target, prop, recv);
76
- },
77
-
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.
87
-
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);
103
- }
104
- }
50
+ pendingAssemblies.delete(assembly);
51
+ }
105
52
 
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;
53
+ export function clearPendingAssemblies() {
54
+ for (const assembly of pendingAssemblies) {
55
+ clearAssembly(assembly);
56
+ }
126
57
  }
127
58
 
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
- }
59
+ export function makeStatified<
60
+ StateLayer extends object,
61
+ >(
62
+ Superclass: ClassType<any[], object>,
63
+ ): typeof Superclass & ClassType<any[], Statify<StateLayer>> {
64
+
65
+ if (shimCache.has(Superclass)) {
66
+ return shimCache.get(Superclass) as typeof Superclass & ClassType<any[], Statify<StateLayer>>;
146
67
  }
147
68
 
148
- // Metadata was installed eagerly in ExtractionShimBase's constructor.
149
- const metadata = getMetadataOf(instance as Statify<StatifiableObj>);
69
+ const dataLayers = new WeakMap<object, object>();
70
+
71
+ const ExtractionShimBase = function (...args: typeof Superclass extends ClassType<infer Args, object> ? Args : never) {
72
+ const inst = Reflect.construct(Superclass, args, new.target);
73
+
74
+ if (!pendingAssemblies.has(inst)) {
75
+ setMetadataOf(
76
+ inst as unknown as Statify<StatifiableObj>,
77
+ new StateMetadata(),
78
+ );
79
+ pendingAssemblies.add(inst);
80
+ }
150
81
 
151
- const isArray = Array.isArray(instance);
82
+ queueMicrotask(() => {
83
+ clearPendingAssemblies();
84
+ });
152
85
 
153
- const props: (string | symbol)[] = [
154
- ...Object.getOwnPropertyNames(instance),
155
- ...Object.getOwnPropertySymbols(instance),
156
- ];
86
+ const proto = new Proxy({}, {
87
+ get(target, prop, receiver) {
88
+ const result = Reflect.get(target, prop, receiver);
157
89
 
158
- for (const prop of props) {
159
- // Skip Informa internal symbols.
160
- if (prop === statifySealKey || prop === exitProxySymbol) continue;
90
+ if (getGlobalStateMode() === "extract-proxy-path") {
91
+ if (prop === exitProxySymbol) {
92
+ return { path: [], stateRoot: receiver };
93
+ }
161
94
 
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;
95
+ return extract(result, receiver, [prop]);
96
+ }
164
97
 
165
- const desc = Object.getOwnPropertyDescriptor(instance, prop)!;
98
+ return result;
99
+ },
100
+ });
166
101
 
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;
102
+ Reflect.setPrototypeOf(proto, Reflect.getPrototypeOf(inst))
171
103
 
172
- // Existing accessor (get/set): do not double-wrap.
173
- if ("get" in desc || "set" in desc) continue;
104
+ Reflect.setPrototypeOf(inst, proto);
174
105
 
175
- // ---- Instrument this configurable data property ----
176
- let value: unknown = desc.value;
106
+ dataLayers.set(inst, {});
177
107
 
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 });
108
+ return inst;
109
+ }
110
+
111
+ ExtractionShimBase.prototype = new Proxy({}, {
112
+ get(target, prop, recv) {
113
+ return Reflect.has(dataLayers.get(recv)!, prop)
114
+ ? Reflect.get(dataLayers.get(recv)!, prop)
115
+ : Reflect.get(target, prop, recv);
116
+ },
117
+
118
+ set(target, prop, newVal, recv) {
119
+ const stateMetadata = getMetadataOf(recv);
120
+
121
+ const had = Reflect.has(recv, prop);
122
+ const oldVal = Reflect.get(recv, prop) as unknown;
123
+
124
+ let result;
125
+ const desc = getPrototypeChainDescriptor(target, prop);
126
+ if (desc?.set || desc?.get) {
127
+ result = Reflect.set(target, prop, newVal, recv);
128
+ } else if (typeof prop === "string" && prop.startsWith("#")) {
129
+ result = Reflect.set(target, prop, newVal, recv);
189
130
  } else {
190
- hook(metadata, prop, getMetadataOf(value as Statify<StatifiableObj>));
131
+ result = Reflect.set(dataLayers.get(recv)!, prop, newVal);
191
132
  }
192
- }
193
133
 
194
- Object.defineProperty(instance, prop, {
195
- get(this: object) {
196
- if (getGlobalStateMode() === "extract-proxy-path") {
197
- return extract(value, this as Statify<StatifiableObj>, [prop]);
134
+ if (result) {
135
+ if (
136
+ oldVal !== newVal
137
+ && typeof newVal === "object"
138
+ && newVal != null
139
+ && newVal[statifySealKey]
140
+ ) {
141
+ const newValMetadata = getMetadataOf(newVal as Statify<StatifiableObj>);
142
+
143
+ hook(stateMetadata, prop, newValMetadata);
198
144
  }
199
- return value;
200
- },
201
145
 
202
- set(this: object, next: unknown) {
203
- const old = value;
204
- value = next;
146
+ if (had) {
147
+ if (
148
+ oldVal !== newVal
149
+ && typeof oldVal === "object"
150
+ && oldVal != null
151
+ && (oldVal as any)[statifySealKey]
152
+ ) {
153
+ const oldValMetadata = getMetadataOf(oldVal as Statify<StatifiableObj>);
154
+
155
+ unhook(stateMetadata, prop, oldValMetadata);
156
+ }
157
+
158
+ emitCollectionTransition(oldVal, newVal);
205
159
 
206
- if (old !== next) {
207
160
  if (
208
- old != null &&
209
- typeof old === "object" &&
210
- isStatified(old as StatifiableObj)
161
+ typeof prop !== "symbol" &&
162
+ Number.isInteger(Number(prop)) &&
163
+ Array.isArray(target)
211
164
  ) {
212
- unhook(metadata, prop, getMetadataOf(old as Statify<StatifiableObj>));
165
+ stateMetadata.emit('replaceElement', newVal, Number(prop));
213
166
  }
214
167
 
168
+ stateMetadata.emit('replaceProp', newVal, prop);
169
+ } else {
215
170
  if (
216
- next != null &&
217
- typeof next === "object" &&
218
- isStatified(next as StatifiableObj)
171
+ typeof prop !== "symbol" &&
172
+ Number.isInteger(Number(prop)) &&
173
+ Array.isArray(target)
219
174
  ) {
220
- hook(metadata, prop, getMetadataOf(next as Statify<StatifiableObj>));
175
+ stateMetadata.emit('spliceInElement', newVal, Number(prop));
221
176
  }
222
177
 
223
- emitCollectionTransition(old, next);
178
+ stateMetadata.emit('addProp', newVal, prop);
224
179
  }
225
180
 
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);
181
+ stateMetadata.emit('setProp', newVal, prop);
230
182
 
231
- const ee = metadata.eventEmitterAtPathMaybe([prop]);
232
- if (ee) {
233
- ee.emit("replace", next);
234
- emitDescendantPathEvents(metadata, [prop], next, true);
235
- }
236
- },
183
+ const maybeEventEmitterAtVal = stateMetadata.eventEmitterAtPathMaybe([prop]);
184
+ if (maybeEventEmitterAtVal) {
185
+ if (had) {
186
+ maybeEventEmitterAtVal.emit('replace', newVal);
187
+ }
237
188
 
238
- enumerable: desc.enumerable ?? true,
239
- configurable: true,
240
- });
241
- }
189
+ maybeEventEmitterAtVal.emit('set', newVal);
242
190
 
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
- }
191
+ emitDescendantPathEvents(
192
+ stateMetadata,
193
+ [prop],
194
+ newVal,
195
+ had,
196
+ );
197
+ }
198
+ }
252
199
 
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.
200
+ return result;
201
+ },
202
+ });
256
203
 
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
- }
204
+ Reflect.setPrototypeOf(ExtractionShimBase.prototype, Superclass.prototype);
323
205
 
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
- }
206
+ shimCache.set(Superclass, ExtractionShimBase);
332
207
 
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>>;
208
+ return ExtractionShimBase as unknown as typeof Superclass & ClassType<any[], Statify<StateLayer>>;
339
209
  }
340
210
 
211
+ export const makeBaseStatified = <T extends object>() => makeStatified<T>(Object);
package/src/test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import $ from "./high.js";
2
+ import { makeStatified } from "./quirks/basestatified.js";
2
3
  import { StatifiedSet } from "./quirks/set.js";
3
4
 
4
5
  const stateful = $.state<{ a: ({ d: number })[], b?: { c?: { d?: Set<number> } } }>({ a: [] });
@@ -26,19 +27,21 @@ const asdf2 = $.state({ c: asdf });
26
27
  // const aassddff = $.state(new Set());
27
28
  stateful.b = asdf2;
28
29
 
29
- const Wayland = $.statifyClass(
30
- (Base) => class Wayland extends Base {
31
- displays = new StatifiedSet();
30
+ interface WaylandState {
31
+ state2: number;
32
+ }
33
+ class Wayland extends makeStatified<WaylandState>(Object) {
34
+ displays = new StatifiedSet();
32
35
 
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
- );
36
+ state = 10;
37
+
38
+ get state2() {
39
+ return super.state2;
40
+ }
41
+ set state2(v) {
42
+ super.state2 = v;
43
+ }
44
+ }
42
45
 
43
46
  const w = new Wayland();
44
47
  $.onSet(() => w.state, (v) => console.log("asdf awawa!!", v));
@@ -47,6 +50,12 @@ $.onSet(() => w.state, (v) => console.log("asdf awawa!!", v));
47
50
  w.state = 6;
48
51
  w.state = 7;
49
52
 
53
+ $.onSet(() => w.state2, (v) => console.log("asdf awawa!!", v));
54
+
55
+ // w.state2 = 5;
56
+ w.state2 = 1234;
57
+ w.state2 = 12345;
58
+
50
59
  const map = $.state<Map<number, string>>(new Map());
51
60
  $.onSetEntry(() => map, (k, v) => console.log("set", k, v));
52
61
  $.onReplaceEntry(() => map, (k, v) => console.log("replace", k, v));