Skip to content

Commit a380e75

Browse files
committed
Splitting out dynamic objects from static objects
1 parent 5855bd3 commit a380e75

4 files changed

Lines changed: 361 additions & 85 deletions

File tree

src/api/observable.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@ import { IObservableValue, ObservableValue } from "../types/observablevalue"
1111
import { IObservableArray, createObservableArray } from "../types/observablearray"
1212
import { createDecoratorForEnhancer, IObservableDecorator } from "./observabledecorator"
1313
import { isObservable } from "./isobservable"
14-
import {
15-
IObservableObject,
16-
asObservableObject,
17-
createDynamicObservableObject
18-
} from "../types/observableobject"
14+
import { IObservableObject, asObservableObject } from "../types/observableobject"
1915
import { extendObservable } from "./extendobservable"
2016
import { IObservableMapInitialValues, ObservableMap } from "../types/observablemap"
17+
import { createDynamicObservableObject } from "../types/dynamicobject"
2118

2219
export type CreateObservableOptions = {
2320
name?: string

src/types/dynamicobject.ts

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
import { ObservableValue, UNCHANGED } from "./observablevalue"
2+
import { IAtom, Atom } from "../core/atom"
3+
import { ComputedValue, IComputedValueOptions } from "../core/computedvalue"
4+
import {
5+
createInstanceofPredicate,
6+
isObject,
7+
Lambda,
8+
getNextId,
9+
invariant,
10+
assertPropertyConfigurable,
11+
isPlainObject,
12+
fail,
13+
addHiddenFinalProp,
14+
isPropertyConfigurable,
15+
addHiddenProp
16+
} from "../utils/utils"
17+
import {
18+
hasInterceptors,
19+
IInterceptable,
20+
registerInterceptor,
21+
interceptChange
22+
} from "./intercept-utils"
23+
import { IListenable, registerListener, hasListeners, notifyListeners } from "./listen-utils"
24+
import { isSpyEnabled, spyReportStart, spyReportEnd } from "../core/spy"
25+
import { IEnhancer, referenceEnhancer, deepEnhancer } from "./modifiers"
26+
import { createObservableArray } from "./observablearray"
27+
import { initializeInstance } from "../utils/decorators2"
28+
import { startBatch, endBatch } from "../core/observable"
29+
import { IIsObservableObject } from "./observableobject"
30+
31+
// TODO: dedupe
32+
33+
export interface IObservableObject {
34+
"observable-object": IObservableObject
35+
}
36+
37+
export type IObjectDidChange =
38+
| {
39+
name: string
40+
object: any
41+
type: "add"
42+
newValue: any
43+
}
44+
| {
45+
name: string
46+
object: any
47+
type: "update"
48+
oldValue: any
49+
newValue: any
50+
}
51+
| {
52+
name: string
53+
object: any
54+
type: "remove"
55+
oldValue: any
56+
}
57+
58+
export type IObjectWillChange =
59+
| {
60+
object: any
61+
type: "update" | "add"
62+
name: string
63+
newValue: any
64+
}
65+
| {
66+
object: any
67+
type: "remove"
68+
name: string
69+
}
70+
71+
export class DynamicObservableObjectAdministration
72+
implements IInterceptable<IObjectWillChange>, IListenable {
73+
keysAtom: IAtom
74+
// TODO: kep a hasMap like with observable Maps (probably, reuse same mechanism?)
75+
changeListeners
76+
interceptors
77+
78+
constructor(
79+
public target: any,
80+
// TODO: split into two; enumerable and non-enumerable members
81+
public values: { [key: string]: ObservableValue<any> | ComputedValue<any> },
82+
public name: string,
83+
public defaultEnhancer: IEnhancer<any>
84+
) {
85+
this.keysAtom = new Atom(name + ".keys")
86+
}
87+
88+
read(owner: any, key: string) {
89+
if (typeof key !== "string" || !this.values.hasOwnProperty(key)) return this.values[key] // might be on prototype
90+
const observable = this.values[key]
91+
if (observable) {
92+
return observable.get()
93+
} else {
94+
// lazily create the property if it a dynamic object
95+
console.log("adding" + key)
96+
this.addObservableProp(key, undefined)
97+
return this.values[key].get()
98+
}
99+
}
100+
101+
write(owner: any, key: string, newValue) {
102+
const instance = this.target
103+
const observable = this.values[key]
104+
if (!observable) {
105+
this.addObservableProp(key, newValue)
106+
return
107+
}
108+
if (observable instanceof ComputedValue) {
109+
observable.set(newValue)
110+
return
111+
}
112+
113+
// intercept
114+
if (hasInterceptors(this)) {
115+
const change = interceptChange<IObjectWillChange>(this, {
116+
type: "update",
117+
object: instance,
118+
name: key,
119+
newValue
120+
})
121+
if (!change) return
122+
newValue = (change as any).newValue
123+
}
124+
newValue = (observable as any).prepareNewValue(newValue)
125+
126+
// notify spy & observers
127+
if (newValue !== UNCHANGED) {
128+
const notify = hasListeners(this)
129+
const notifySpy = isSpyEnabled()
130+
const change =
131+
notify || notifySpy
132+
? {
133+
type: "update",
134+
object: instance,
135+
oldValue: (observable as any).value,
136+
name: key,
137+
newValue
138+
}
139+
: null
140+
141+
if (notifySpy) spyReportStart({ ...change, name: this.name, key })
142+
;(observable as ObservableValue<any>).setNewValue(newValue)
143+
if (notify) notifyListeners(this, change)
144+
if (notifySpy) spyReportEnd()
145+
}
146+
}
147+
148+
addObservableProp(propName: string, newValue, enhancer: IEnhancer<any> = this.defaultEnhancer) {
149+
const { target } = this
150+
assertPropertyConfigurable(target, propName)
151+
152+
if (hasInterceptors(this)) {
153+
const change = interceptChange<IObjectWillChange>(this, {
154+
object: target,
155+
name: propName,
156+
type: "add",
157+
newValue
158+
})
159+
if (!change) return
160+
newValue = (change as any).newValue
161+
}
162+
const observable = (this.values[propName] = new ObservableValue(
163+
newValue,
164+
enhancer,
165+
`${this.name}.${propName}`,
166+
false
167+
))
168+
newValue = (observable as any).value // observableValue might have changed it
169+
170+
this.notifyPropertyAddition(propName, newValue)
171+
}
172+
173+
addComputedProp(
174+
propertyOwner: any, // where is the property declared?
175+
propName: string,
176+
options: IComputedValueOptions<any>
177+
) {
178+
const { target } = this
179+
options.name = options.name || `${this.name}.${propName}`
180+
options.context = target
181+
// TODO: fix
182+
addHiddenProp(this.values, propName, new ComputedValue(options)) // non enumerable
183+
}
184+
185+
remove(key: string) {
186+
if (!this.values[key]) return
187+
const { target } = this
188+
if (hasInterceptors(this)) {
189+
const change = interceptChange<IObjectWillChange>(this, {
190+
object: target,
191+
name: key,
192+
type: "remove"
193+
})
194+
if (!change) return
195+
}
196+
try {
197+
startBatch()
198+
const notify = hasListeners(this)
199+
const notifySpy = isSpyEnabled()
200+
const oldValue = this.values[key].get() // TODO: might not exist on dynamic objects
201+
this.values[key].set(undefined)
202+
this.keysAtom.reportChanged()
203+
delete this.values[key]
204+
const change =
205+
notify || notifySpy
206+
? {
207+
type: "remove",
208+
object: target,
209+
oldValue: oldValue,
210+
name: key
211+
}
212+
: null
213+
if (notifySpy) spyReportStart({ ...change, name: this.name, key })
214+
if (notify) notifyListeners(this, change)
215+
if (notifySpy) spyReportEnd()
216+
} finally {
217+
endBatch()
218+
}
219+
}
220+
221+
illegalAccess(owner, propName) {
222+
/**
223+
* This happens if a property is accessed through the prototype chain, but the property was
224+
* declared directly as own property on the prototype.
225+
*
226+
* E.g.:
227+
* class A {
228+
* }
229+
* extendObservable(A.prototype, { x: 1 })
230+
*
231+
* classB extens A {
232+
* }
233+
* console.log(new B().x)
234+
*
235+
* It is unclear whether the property should be considered 'static' or inherited.
236+
* Either use `console.log(A.x)`
237+
* or: decorate(A, { x: observable })
238+
*
239+
* When using decorate, the property will always be redeclared as own property on the actual instance
240+
*/
241+
return fail(
242+
`Property '${propName}' of '${owner}' was accessed through the prototype chain. Use 'decorate' instead to declare the prop or access it statically through it's owner`
243+
)
244+
}
245+
246+
/**
247+
* Observes this object. Triggers for the events 'add', 'update' and 'delete'.
248+
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe
249+
* for callback details
250+
*/
251+
observe(callback: (changes: IObjectDidChange) => void, fireImmediately?: boolean): Lambda {
252+
process.env.NODE_ENV !== "production" &&
253+
invariant(
254+
fireImmediately !== true,
255+
"`observe` doesn't support the fire immediately property for observable objects."
256+
)
257+
return registerListener(this, callback)
258+
}
259+
260+
intercept(handler): Lambda {
261+
return registerInterceptor(this, handler)
262+
}
263+
264+
notifyPropertyAddition(key: string, newValue) {
265+
const notify = hasListeners(this)
266+
const notifySpy = isSpyEnabled()
267+
const change =
268+
notify || notifySpy
269+
? {
270+
type: "add",
271+
object: this.target,
272+
name: key,
273+
newValue
274+
}
275+
: null
276+
277+
if (notifySpy) spyReportStart({ ...change, name: this.name, key })
278+
if (notify) notifyListeners(this, change)
279+
if (notifySpy) spyReportEnd()
280+
this.keysAtom.reportChanged()
281+
}
282+
283+
getKeys(): string[] {
284+
this.keysAtom.reportObserved()
285+
return Object.keys(this.values).filter(key => this.values[key] instanceof ObservableValue)
286+
}
287+
}
288+
289+
const objectProxyTraps: ProxyHandler<any> = {
290+
get(target: IIsObservableObject, name: string) {
291+
if (name === "$mobx") return target.$mobx
292+
// TODO: use symbol for "__mobxDidRunLazyInitializers" and "$mobx", and remove these checks
293+
if (
294+
typeof name === "string" &&
295+
name !== "constructor" &&
296+
name !== "__mobxDidRunLazyInitializers"
297+
)
298+
return target.$mobx.read(target, name)
299+
return target[name]
300+
},
301+
set(target: IIsObservableObject, name: string, value: any) {
302+
const adm = target.$mobx
303+
if (typeof name === "string" && name !== "constructor" && name !== "$mobx") {
304+
adm.write(target, name, value)
305+
return true
306+
}
307+
return fail(`Cannot reassign ${name}`)
308+
},
309+
deleteProperty(target: IIsObservableObject, name: string) {
310+
const adm = target.$mobx
311+
if (name === "$mobx") return fail(`Cannot reassign $mobx`)
312+
adm.remove(name)
313+
return true
314+
},
315+
ownKeys(target: IIsObservableObject) {
316+
const adm = target.$mobx
317+
return adm.getKeys()
318+
}
319+
}
320+
321+
export function createDynamicObservableObject(
322+
name,
323+
defaultEnhancer: IEnhancer<any> = deepEnhancer
324+
) {
325+
const values = {}
326+
const proxy = new Proxy(values, objectProxyTraps)
327+
const adm = new DynamicObservableObjectAdministration(
328+
proxy,
329+
values,
330+
name || "ObservableObject@" + getNextId(),
331+
defaultEnhancer
332+
)
333+
addHiddenFinalProp(values, "$mobx", adm)
334+
return proxy
335+
}

0 commit comments

Comments
 (0)