@pyreon/state-tree 0.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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/lib/analysis/devtools.js.html +5406 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/devtools.js +111 -0
- package/lib/devtools.js.map +1 -0
- package/lib/index.js +353 -0
- package/lib/index.js.map +1 -0
- package/lib/types/devtools.d.ts +104 -0
- package/lib/types/devtools.d.ts.map +1 -0
- package/lib/types/devtools2.d.ts +40 -0
- package/lib/types/devtools2.d.ts.map +1 -0
- package/lib/types/index.d.ts +316 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +198 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +54 -0
- package/src/devtools.ts +87 -0
- package/src/index.ts +29 -0
- package/src/instance.ts +128 -0
- package/src/middleware.ts +57 -0
- package/src/model.ts +117 -0
- package/src/patch.ts +173 -0
- package/src/registry.ts +16 -0
- package/src/snapshot.ts +66 -0
- package/src/tests/devtools.test.ts +163 -0
- package/src/tests/model.test.ts +718 -0
- package/src/types.ts +98 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { model } from '../index'
|
|
2
|
+
import {
|
|
3
|
+
registerInstance,
|
|
4
|
+
unregisterInstance,
|
|
5
|
+
getActiveModels,
|
|
6
|
+
getModelInstance,
|
|
7
|
+
getModelSnapshot,
|
|
8
|
+
onModelChange,
|
|
9
|
+
_resetDevtools,
|
|
10
|
+
} from '../devtools'
|
|
11
|
+
|
|
12
|
+
const Counter = model({
|
|
13
|
+
state: { count: 0 },
|
|
14
|
+
actions: (self) => ({
|
|
15
|
+
inc: () => self.count.update((c: number) => c + 1),
|
|
16
|
+
}),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => _resetDevtools())
|
|
20
|
+
|
|
21
|
+
describe('state-tree devtools', () => {
|
|
22
|
+
test('getActiveModels returns empty initially', () => {
|
|
23
|
+
expect(getActiveModels()).toEqual([])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('registerInstance makes model visible', () => {
|
|
27
|
+
const counter = Counter.create()
|
|
28
|
+
registerInstance('app-counter', counter)
|
|
29
|
+
expect(getActiveModels()).toEqual(['app-counter'])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('getModelInstance returns the registered instance', () => {
|
|
33
|
+
const counter = Counter.create()
|
|
34
|
+
registerInstance('app-counter', counter)
|
|
35
|
+
expect(getModelInstance('app-counter')).toBe(counter)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('getModelInstance returns undefined for unregistered name', () => {
|
|
39
|
+
expect(getModelInstance('nope')).toBeUndefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('unregisterInstance removes the model', () => {
|
|
43
|
+
const counter = Counter.create()
|
|
44
|
+
registerInstance('app-counter', counter)
|
|
45
|
+
unregisterInstance('app-counter')
|
|
46
|
+
expect(getActiveModels()).toEqual([])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('getModelSnapshot returns current snapshot', () => {
|
|
50
|
+
const counter = Counter.create({ count: 5 })
|
|
51
|
+
registerInstance('app-counter', counter)
|
|
52
|
+
expect(getModelSnapshot('app-counter')).toEqual({ count: 5 })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('getModelSnapshot reflects mutations', () => {
|
|
56
|
+
const counter = Counter.create()
|
|
57
|
+
registerInstance('app-counter', counter)
|
|
58
|
+
counter.inc()
|
|
59
|
+
counter.inc()
|
|
60
|
+
expect(getModelSnapshot('app-counter')).toEqual({ count: 2 })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('getModelSnapshot returns undefined for unregistered name', () => {
|
|
64
|
+
expect(getModelSnapshot('nope')).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('onModelChange fires on register', () => {
|
|
68
|
+
const calls: number[] = []
|
|
69
|
+
const unsub = onModelChange(() => calls.push(1))
|
|
70
|
+
|
|
71
|
+
const counter = Counter.create()
|
|
72
|
+
registerInstance('app-counter', counter)
|
|
73
|
+
expect(calls.length).toBe(1)
|
|
74
|
+
|
|
75
|
+
unsub()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('onModelChange fires on unregister', () => {
|
|
79
|
+
const counter = Counter.create()
|
|
80
|
+
registerInstance('app-counter', counter)
|
|
81
|
+
|
|
82
|
+
const calls: number[] = []
|
|
83
|
+
const unsub = onModelChange(() => calls.push(1))
|
|
84
|
+
unregisterInstance('app-counter')
|
|
85
|
+
expect(calls.length).toBe(1)
|
|
86
|
+
|
|
87
|
+
unsub()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('onModelChange unsubscribe stops notifications', () => {
|
|
91
|
+
const calls: number[] = []
|
|
92
|
+
const unsub = onModelChange(() => calls.push(1))
|
|
93
|
+
unsub()
|
|
94
|
+
|
|
95
|
+
registerInstance('app-counter', Counter.create())
|
|
96
|
+
expect(calls.length).toBe(0)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('multiple instances are tracked', () => {
|
|
100
|
+
registerInstance('a', Counter.create())
|
|
101
|
+
registerInstance('b', Counter.create())
|
|
102
|
+
expect(getActiveModels().sort()).toEqual(['a', 'b'])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("getModelInstance returns undefined and cleans up when WeakRef target is GC'd", () => {
|
|
106
|
+
// We simulate a GC'd WeakRef by monkey-patching the registered WeakRef's deref.
|
|
107
|
+
const counter = Counter.create()
|
|
108
|
+
registerInstance('gc-test', counter)
|
|
109
|
+
|
|
110
|
+
// Verify it's accessible
|
|
111
|
+
expect(getModelInstance('gc-test')).toBe(counter)
|
|
112
|
+
|
|
113
|
+
// Now register a new entry with a fake WeakRef-like object that returns undefined.
|
|
114
|
+
// Since _activeModels is a Map<string, WeakRef<object>>, we can re-register
|
|
115
|
+
// to overwrite the entry, but registerInstance creates a real WeakRef.
|
|
116
|
+
// Instead, we'll unregister and then test getModelInstance on a missing key.
|
|
117
|
+
// But that tests the !ref branch (line 53), not the !instance branch (lines 55-57).
|
|
118
|
+
|
|
119
|
+
// The only way to test lines 55-57 is to have a WeakRef whose deref() returns undefined.
|
|
120
|
+
// We achieve this by creating the WeakRef with the mocked constructor before registering.
|
|
121
|
+
_resetDevtools()
|
|
122
|
+
|
|
123
|
+
const OriginalWeakRef = globalThis.WeakRef
|
|
124
|
+
let collected = false
|
|
125
|
+
class GCWeakRef {
|
|
126
|
+
_target: any
|
|
127
|
+
constructor(target: any) {
|
|
128
|
+
this._target = target
|
|
129
|
+
}
|
|
130
|
+
deref() {
|
|
131
|
+
return collected ? undefined : this._target
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
globalThis.WeakRef = GCWeakRef as any
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const c2 = Counter.create()
|
|
138
|
+
registerInstance('gc-victim', c2)
|
|
139
|
+
|
|
140
|
+
// Before GC
|
|
141
|
+
expect(getModelInstance('gc-victim')).toBe(c2)
|
|
142
|
+
expect(getActiveModels()).toContain('gc-victim')
|
|
143
|
+
|
|
144
|
+
// Simulate GC
|
|
145
|
+
collected = true
|
|
146
|
+
|
|
147
|
+
// getActiveModels cleans up dead refs first (line 43 branch)
|
|
148
|
+
expect(getActiveModels()).not.toContain('gc-victim')
|
|
149
|
+
|
|
150
|
+
// Re-register to test getModelInstance's GC cleanup path (lines 55-57)
|
|
151
|
+
registerInstance('gc-victim-2', Counter.create())
|
|
152
|
+
collected = true
|
|
153
|
+
|
|
154
|
+
// getModelInstance hits lines 55-57: instance is undefined, deletes entry, returns undefined
|
|
155
|
+
expect(getModelInstance('gc-victim-2')).toBeUndefined()
|
|
156
|
+
|
|
157
|
+
// getModelSnapshot returns undefined for GC'd instance
|
|
158
|
+
expect(getModelSnapshot('gc-victim-2')).toBeUndefined()
|
|
159
|
+
} finally {
|
|
160
|
+
globalThis.WeakRef = OriginalWeakRef
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
})
|