ivue 1.5.8 → 2.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/README.md +170 -33
- package/bin/ivue.mjs +152 -0
- package/dist/Reactive.d.ts +131 -0
- package/dist/env.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +90 -81
- package/dist/index.umd.js +1 -1
- package/lib/Reactive.ts +509 -0
- package/lib/__tests__/Reactive.vitest.spec.ts +1056 -0
- package/lib/__tests__/ReactiveAdversarial.vitest.spec.ts +182 -0
- package/lib/__tests__/coverage-completion.vitest.spec.ts +24 -0
- package/lib/__tests__/ivue.vitest.spec.ts +1460 -150
- package/lib/env.d.ts +1 -0
- package/lib/index.ts +1 -1
- package/lib/ivue.ts +661 -241
- package/lib/kernel.ts +18 -0
- package/package.json +51 -13
- package/skills/ivue/SKILL.md +726 -0
- package/dist/ivue.d.ts +0 -234
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { reactive, ref } from 'vue';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { Reactive } from '../Reactive';
|
|
4
|
+
import { Kernel } from '../kernel';
|
|
5
|
+
|
|
6
|
+
describe('Reactive adversarial boundaries', () => {
|
|
7
|
+
it('richer cleanup composes as an ordinary method calling $stopEffects', () => {
|
|
8
|
+
const observed: number[] = [];
|
|
9
|
+
let closed = 0;
|
|
10
|
+
|
|
11
|
+
class $Store {
|
|
12
|
+
get count() {
|
|
13
|
+
return ref(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// the composition pattern: no reserved names, no auto-calls —
|
|
17
|
+
// the class's own dispose() does its work, then resets the engine
|
|
18
|
+
dispose() {
|
|
19
|
+
this.closeSocket();
|
|
20
|
+
(this as any).$stopEffects();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
closeSocket() {
|
|
24
|
+
closed++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Store = Reactive($Store);
|
|
29
|
+
const store: any = new Store();
|
|
30
|
+
const originalCount = store.count;
|
|
31
|
+
store.$watch(
|
|
32
|
+
() => store.count.value,
|
|
33
|
+
(count: number) => observed.push(count),
|
|
34
|
+
{ flush: 'sync' },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
originalCount.value = 1;
|
|
38
|
+
expect(observed).toEqual([1]);
|
|
39
|
+
|
|
40
|
+
store.dispose();
|
|
41
|
+
|
|
42
|
+
expect(closed).toBe(1);
|
|
43
|
+
originalCount.value = 2;
|
|
44
|
+
expect(observed).toEqual([1]); // watcher dead
|
|
45
|
+
expect(store.count).not.toBe(originalCount); // cells reset
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('teardown clears only engine cache keys — consumer symbols survive', () => {
|
|
49
|
+
const consumerSymbol = Symbol('consumer-metadata');
|
|
50
|
+
|
|
51
|
+
class $Parent {
|
|
52
|
+
get inherited() {
|
|
53
|
+
return ref('parent');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class $Child extends $Parent {
|
|
58
|
+
get own() {
|
|
59
|
+
return ref('child');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
grow() {
|
|
63
|
+
return this.own.value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const Child = Reactive($Child);
|
|
68
|
+
const child: any = new Child();
|
|
69
|
+
|
|
70
|
+
// materialize cells on BOTH prototype layers + a bound method
|
|
71
|
+
const inheritedCell = child.inherited;
|
|
72
|
+
const ownCell = child.own;
|
|
73
|
+
const boundGrow = child.grow;
|
|
74
|
+
// a symbol the engine did NOT create — foreign metadata on the instance
|
|
75
|
+
child[consumerSymbol] = 'must survive';
|
|
76
|
+
|
|
77
|
+
child.$stopEffects();
|
|
78
|
+
|
|
79
|
+
// engine cells across the whole chain are gone: fresh identities
|
|
80
|
+
expect(child.inherited).not.toBe(inheritedCell);
|
|
81
|
+
expect(child.own).not.toBe(ownCell);
|
|
82
|
+
expect(child.grow).not.toBe(boundGrow);
|
|
83
|
+
// the consumer's symbol is untouched
|
|
84
|
+
expect(child[consumerSymbol]).toBe('must survive');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('can allocate a fresh watcher scope after teardown', () => {
|
|
88
|
+
class $Store {
|
|
89
|
+
get count() {
|
|
90
|
+
return ref(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const Store = Reactive($Store);
|
|
95
|
+
const store: any = new Store();
|
|
96
|
+
const firstObserved = vi.fn();
|
|
97
|
+
store.$watch(() => store.count.value, firstObserved, { flush: 'sync' });
|
|
98
|
+
store.count.value = 1;
|
|
99
|
+
store.$stopEffects();
|
|
100
|
+
|
|
101
|
+
const secondObserved = vi.fn();
|
|
102
|
+
store.$watch(() => store.count.value, secondObserved, { flush: 'sync' });
|
|
103
|
+
store.count.value = 2;
|
|
104
|
+
|
|
105
|
+
expect(firstObserved).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(secondObserved).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('does not poison a lazy ref cache when its factory throws once', () => {
|
|
110
|
+
let attempts = 0;
|
|
111
|
+
|
|
112
|
+
class $Store {
|
|
113
|
+
get state() {
|
|
114
|
+
attempts++;
|
|
115
|
+
if (attempts === 1) throw new Error('dependency is not ready');
|
|
116
|
+
return ref(7);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const Store = Reactive($Store);
|
|
121
|
+
const store = new Store();
|
|
122
|
+
|
|
123
|
+
expect(() => store.state).toThrow('dependency is not ready');
|
|
124
|
+
const recovered = store.state;
|
|
125
|
+
expect(recovered.value).toBe(7);
|
|
126
|
+
expect(store.state).toBe(recovered);
|
|
127
|
+
expect(attempts).toBe(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('caches an undefined $-singleton result instead of rerunning it', () => {
|
|
131
|
+
const factory = vi.fn(() => undefined);
|
|
132
|
+
|
|
133
|
+
class $Store {
|
|
134
|
+
get $optionalService() {
|
|
135
|
+
return factory();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const Store = Reactive($Store);
|
|
140
|
+
const store = new Store();
|
|
141
|
+
|
|
142
|
+
expect(store.$optionalService).toBeUndefined();
|
|
143
|
+
expect(store.$optionalService).toBeUndefined();
|
|
144
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('tears down the raw instance when invoked through a Vue proxy', () => {
|
|
148
|
+
const observed = vi.fn();
|
|
149
|
+
|
|
150
|
+
class $Store {
|
|
151
|
+
get count() {
|
|
152
|
+
return ref(0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const Store = Reactive($Store);
|
|
157
|
+
const rawStore: any = new Store();
|
|
158
|
+
const proxiedStore: any = reactive(rawStore);
|
|
159
|
+
const originalCount = rawStore.count;
|
|
160
|
+
proxiedStore.$watch(() => rawStore.count.value, observed, { flush: 'sync' });
|
|
161
|
+
|
|
162
|
+
originalCount.value = 1;
|
|
163
|
+
proxiedStore.$stopEffects();
|
|
164
|
+
originalCount.value = 2;
|
|
165
|
+
|
|
166
|
+
expect(observed).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(rawStore.count).not.toBe(originalCount);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('Kernel adversarial values', () => {
|
|
172
|
+
it('preserves stored falsy values instead of taking the fallback', () => {
|
|
173
|
+
const kernel = new Kernel();
|
|
174
|
+
kernel.set('zero', 0);
|
|
175
|
+
kernel.set('false', false);
|
|
176
|
+
kernel.set('empty', '');
|
|
177
|
+
|
|
178
|
+
expect(kernel.get('zero', 1)).toBe(0);
|
|
179
|
+
expect(kernel.get('false', true)).toBe(false);
|
|
180
|
+
expect(kernel.get('empty', 'fallback')).toBe('');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage completion for the package entry and Kernel container.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import * as packageEntry from '../index';
|
|
6
|
+
import { Kernel, kernel } from '../kernel';
|
|
7
|
+
|
|
8
|
+
describe('package entry (index.ts)', () => {
|
|
9
|
+
it('re-exports the engine surface', () => {
|
|
10
|
+
expect(typeof packageEntry.Reactive).toBe('function');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('Kernel', () => {
|
|
15
|
+
it('sets, gets, falls back, clears', () => {
|
|
16
|
+
const container = new Kernel();
|
|
17
|
+
container.set('answer', 42);
|
|
18
|
+
expect(container.get('answer', 0)).toBe(42);
|
|
19
|
+
expect(container.get('missing', 'fallback')).toBe('fallback');
|
|
20
|
+
container.clear();
|
|
21
|
+
expect(container.get('answer', 0)).toBe(0);
|
|
22
|
+
expect(kernel).toBeInstanceOf(Kernel);
|
|
23
|
+
});
|
|
24
|
+
});
|