ivue 2.2.2 → 2.4.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.
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { LazyShared } from '../LazyShared';
3
+ import { Static } from '../Static';
4
+
5
+ describe('LazyShared', () => {
6
+ it('evaluates nothing at definition, constructs once, shares the result', () => {
7
+ let constructionCount = 0;
8
+ const cell = new LazyShared(() => {
9
+ constructionCount += 1;
10
+ return { count: constructionCount };
11
+ });
12
+ expect(constructionCount).toBe(0); // load-safe: the thunk is inert
13
+ expect(cell.value).toBe(cell.value);
14
+ expect(constructionCount).toBe(1);
15
+ });
16
+
17
+ it('every receiver converges on the ONE singleton — forking the access path is harmless', () => {
18
+ let constructionCount = 0;
19
+ class $Owner {
20
+ protected static readonly sharedStore = new LazyShared(() => {
21
+ constructionCount += 1;
22
+ return new Map<string, number>();
23
+ });
24
+ static store(): Map<string, number> {
25
+ return this.sharedStore.value;
26
+ }
27
+ }
28
+ class $SubReceiver extends $Owner {}
29
+ $Owner.store().set('planted', 7);
30
+ expect($SubReceiver.store().get('planted')).toBe(7);
31
+ expect($SubReceiver.store()).toBe($Owner.store());
32
+ expect(constructionCount).toBe(1);
33
+ });
34
+
35
+ it('even a per-receiver $-cache over the cell returns the same singleton', () => {
36
+ // The registry-fork trap: Static()'s $-getters cache per receiver,
37
+ // so parent and subclass each run the getter body once. With the
38
+ // body reading a LazyShared cell, both receivers cache the SAME
39
+ // constructed value — the fork exists only in the access path.
40
+ let constructionCount = 0;
41
+ class $Registry {
42
+ protected static readonly sharedEntries = new LazyShared(() => {
43
+ constructionCount += 1;
44
+ return new Map<string, number>();
45
+ });
46
+ static get $entries(): Map<string, number> {
47
+ return this.sharedEntries.value;
48
+ }
49
+ }
50
+ const Registry = Static($Registry);
51
+ class $SubRegistry extends Registry {}
52
+ const SubRegistry = Static($SubRegistry);
53
+ Registry.$entries.set('planted', 7);
54
+ expect(SubRegistry.$entries).toBe(Registry.$entries);
55
+ expect(SubRegistry.$entries.get('planted')).toBe(7);
56
+ expect(constructionCount).toBe(1);
57
+ });
58
+
59
+ it('reset drops the value and the next read constructs again', () => {
60
+ let constructionCount = 0;
61
+ const cell = new LazyShared(() => (constructionCount += 1));
62
+ expect(cell.value).toBe(1);
63
+ cell.reset();
64
+ expect(cell.value).toBe(2);
65
+ });
66
+
67
+ it('a thunk cycle throws a named error and leaves the cell retryable', () => {
68
+ const cellA = new LazyShared((): number => cellB.value + 1);
69
+ const cellB = new LazyShared((): number => cellA.value + 1);
70
+ expect(() => cellA.value).toThrow('LazyShared thunk cycle');
71
+ // not poisoned: break the cycle and the same cell constructs fine
72
+ let repaired = 0;
73
+ const cellC = new LazyShared((): number => (repaired += 1));
74
+ expect(cellC.value).toBe(1);
75
+ // the failed cell itself retries once its dependency resolves
76
+ expect(() => cellB.value).toThrow('LazyShared thunk cycle');
77
+ });
78
+
79
+ it('a throwing thunk does not poison the cell — the next read retries', () => {
80
+ let attempts = 0;
81
+ const cell = new LazyShared(() => {
82
+ attempts += 1;
83
+ if (attempts === 1) throw new Error('backend not ready');
84
+ return 'ready';
85
+ });
86
+ expect(() => cell.value).toThrow('backend not ready');
87
+ expect(cell.value).toBe('ready');
88
+ expect(attempts).toBe(2);
89
+ });
90
+ });
@@ -735,6 +735,64 @@ describe('Reactive()', () => {
735
735
  expect(() => instance.$stopEffects()).not.toThrow();
736
736
  });
737
737
 
738
+ it('{ reset: false } stops watchers but every cached cell survives', async () => {
739
+ class Session {
740
+ get counter() {
741
+ return ref(0);
742
+ }
743
+ get doubled() {
744
+ return computed(() => (this as any).counter.value * 2);
745
+ }
746
+ }
747
+ const instance: any = new (Reactive(Session))();
748
+ let observed = 0;
749
+ instance.$watchEffect(() => {
750
+ observed = instance.doubled.value; // first-touches the computed IN the scope
751
+ });
752
+ const counterCell = instance.counter;
753
+ const doubledCell = instance.doubled;
754
+ instance.counter.value = 21;
755
+ await nextTick();
756
+ expect(observed).toBe(42);
757
+
758
+ instance.$stopEffects({ reset: false });
759
+
760
+ // watchers are dead…
761
+ instance.counter.value = 100;
762
+ await nextTick();
763
+ expect(observed).toBe(42);
764
+ // …but the cells survive with their CURRENT values (no re-init)
765
+ expect(instance.counter).toBe(counterCell);
766
+ expect(instance.doubled).toBe(doubledCell);
767
+ expect(instance.counter.value).toBe(100);
768
+ // the surviving computed still evaluates (pull-based past scope death)
769
+ expect(instance.doubled.value).toBe(200);
770
+
771
+ // and a SECOND life works: a fresh scope tracks the old cells
772
+ let observedAgain = 0;
773
+ instance.$watch(
774
+ () => instance.doubled.value,
775
+ (doubledValue: number) => {
776
+ observedAgain = doubledValue;
777
+ },
778
+ );
779
+ instance.counter.value = 7;
780
+ await nextTick();
781
+ expect(observedAgain).toBe(14);
782
+ });
783
+
784
+ it('default call still resets — cells re-initialize after teardown', () => {
785
+ class Session {
786
+ get counter() {
787
+ return ref(0);
788
+ }
789
+ }
790
+ const instance: any = new (Reactive(Session))();
791
+ instance.counter.value = 41;
792
+ instance.$stopEffects();
793
+ expect(instance.counter.value).toBe(0);
794
+ });
795
+
738
796
  it('$stopEffects is injected only once (idempotent re-Reactive)', () => {
739
797
  class Store {
740
798
  m() {
package/lib/extras.ts CHANGED
@@ -7,3 +7,4 @@
7
7
  * import { Static } from 'ivue/extras';
8
8
  */
9
9
  export { Static, type ClassConstructor } from './Static';
10
+ export { LazyShared } from './LazyShared';
package/lib/ivue.ts CHANGED
@@ -1,3 +1,20 @@
1
+ /**
2
+ * ============================================================
3
+ * DEPRECATED — ivue v1 (the reactive-proxy engine)
4
+ * ============================================================
5
+ *
6
+ * This file is the ORIGINAL v1 implementation: `ivue(Class, ...args)`
7
+ * wraps instances in `reactive()` and converts accessors to computeds.
8
+ * It is kept in-tree for reference and migration only.
9
+ *
10
+ * v2 is `Reactive()` in ./Reactive.ts (the package's main entry) — a
11
+ * one-time prototype transform: instances stay plain objects, plain
12
+ * getters cost zero bytes, creation measures 55-253x faster. New code
13
+ * must never import from this file.
14
+ *
15
+ * Migration guide: https://ivue.dev/guide/standard
16
+ */
17
+
1
18
  import type { ComputedRef, ExtractPropTypes, Ref, ToRef } from 'vue';
2
19
  import { computed, markRaw, reactive, ref, shallowRef, toRef } from 'vue';
3
20
  import type { Ref as DemiRef } from 'vue-demi';
@@ -399,6 +416,8 @@ const getClassPropertiesAccessorsMap = (obj: object): PropsMapValue => {
399
416
  * get getter () { return 'hello world'; }
400
417
  * }
401
418
  *
419
+ * @deprecated v1 engine — use `Reactive()` from the main `ivue` entry
420
+ * instead; see https://ivue.dev/guide/standard for the migration.
402
421
  * @param className Any Class
403
422
  * @param args Class constructor arguments that you would pass to a `new AnyClass(args...)`
404
423
  * @returns {IVue<T>}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ivue",
3
- "version": "2.2.2",
3
+ "version": "2.4.0",
4
4
  "description": "Infinite Vue – Class Based Architecture for Vue 3",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,6 +29,8 @@
29
29
  "measure:formula": "node demo/formula/measure.mjs",
30
30
  "measure:grid": "node demo/grid/measure.mjs",
31
31
  "dev:docs": "npm --prefix docs_v2 run dev -- --port 5174",
32
+ "admin": "vite newsletter/dashboard --host",
33
+ "build:admin": "vite build newsletter/dashboard",
32
34
  "test": "vitest --coverage.enabled=true --reporter=verbose --ui",
33
35
  "coverage": "vitest run --coverage",
34
36
  "bench": "vitest bench --run",
@@ -37,11 +39,17 @@
37
39
  "cypress:headed": "cypress run --component --browser chrome --headed --no-exit",
38
40
  "build": "tsc --version;tsc --p ./tsconfig.json && vite build",
39
41
  "build:demo": "tsc --p ./tsconfig.json && vite build demo",
40
- "build:docs": "npm run sync:examples && npm --prefix docs_v2 run build && npm run check:links",
42
+ "build:docs": "npm run sync:examples && npm run sync:releases && npm run sync:blog-index && npm --prefix docs_v2 run build && npm run check:links && node docs_v2/scripts/check-related-posts.mjs",
43
+ "sync:releases": "node docs_v2/scripts/releases-page-generator.mjs",
44
+ "sync:blog-index": "node docs_v2/scripts/blog-index-generator.mjs",
41
45
  "check:links": "node docs_v2/scripts/check-links.mjs",
42
46
  "render:og": "node docs_v2/scripts/brand-image-generator.mjs og",
43
47
  "render:form-header": "node docs_v2/scripts/brand-image-generator.mjs form-header",
44
48
  "render:banner": "node docs_v2/scripts/brand-image-generator.mjs blog",
49
+ "render:diagram": "node docs_v2/scripts/brand-image-generator.mjs diagram",
50
+ "render:page-og": "node docs_v2/scripts/page-og-generator.mjs",
51
+ "render:embeds": "node docs_v2/scripts/blog-embed-shots.mjs",
52
+ "render:code-shots": "node docs_v2/scripts/blog-code-shots.mjs",
45
53
  "sync:blog-dates": "node docs_v2/scripts/blog-dates-generator.mjs",
46
54
  "preview:demo": "npm run build:demo && vite preview demo --host",
47
55
  "preview:docs": "npm --prefix docs_v2 run preview",
@@ -51,7 +59,8 @@
51
59
  "sync:examples": "node -e \"require('fs').copyFileSync('lib/Reactive.ts','examples/playground/src/ivue.ts')\"",
52
60
  "dev:playground": "vite examples/playground --host",
53
61
  "build:playground": "vite build examples/playground",
54
- "preview:playground": "vite preview examples/playground --host"
62
+ "preview:playground": "vite preview examples/playground --host",
63
+ "rename:blog-slug": "node docs_v2/scripts/rename-blog-slug.mjs"
55
64
  },
56
65
  "peerDependencies": {
57
66
  "vue": "^3.2.0",