vestjs-runtime 1.7.0 → 2.0.2

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.
Files changed (136) hide show
  1. package/IsolateSerializer/package.json +12 -8
  2. package/README.md +3 -1
  3. package/dist/IsolateKeys-B21aPuBk.mjs +23 -0
  4. package/dist/IsolateKeys-B21aPuBk.mjs.map +1 -0
  5. package/dist/IsolateKeys-CCvALpZC.cjs +35 -0
  6. package/dist/IsolateKeys-CCvALpZC.cjs.map +1 -0
  7. package/dist/IsolateSerializer-B1hE3gmT.mjs +1004 -0
  8. package/dist/IsolateSerializer-B1hE3gmT.mjs.map +1 -0
  9. package/dist/IsolateSerializer-pbEf5gB2.cjs +1121 -0
  10. package/dist/IsolateSerializer-pbEf5gB2.cjs.map +1 -0
  11. package/dist/chunk-CLMFDpHK.mjs +18 -0
  12. package/dist/exports/IsolateSerializer.cjs +4 -0
  13. package/dist/exports/IsolateSerializer.mjs +4 -0
  14. package/dist/exports/test-utils.cjs +21 -0
  15. package/dist/exports/test-utils.cjs.map +1 -0
  16. package/dist/exports/test-utils.mjs +21 -0
  17. package/dist/exports/test-utils.mjs.map +1 -0
  18. package/dist/vestjs-runtime.cjs +153 -0
  19. package/dist/vestjs-runtime.cjs.map +1 -0
  20. package/dist/vestjs-runtime.mjs +117 -0
  21. package/dist/vestjs-runtime.mjs.map +1 -0
  22. package/docs/IsolateRegistry.docs.md +146 -0
  23. package/docs/Isolates.md +97 -0
  24. package/package.json +43 -88
  25. package/src/Bus.ts +46 -0
  26. package/src/Isolate/Isolate.ts +163 -0
  27. package/src/Isolate/IsolateFocused.ts +93 -0
  28. package/src/Isolate/IsolateIndexer.ts +42 -0
  29. package/src/Isolate/IsolateInspector.ts +93 -0
  30. package/src/Isolate/IsolateKeys.ts +18 -0
  31. package/src/Isolate/IsolateMutator.ts +165 -0
  32. package/src/Isolate/IsolateRegistry.ts +176 -0
  33. package/src/Isolate/IsolateReorderable.ts +11 -0
  34. package/src/Isolate/IsolateSelectors.ts +25 -0
  35. package/src/Isolate/IsolateStateMachine.ts +30 -0
  36. package/src/Isolate/IsolateStatus.ts +8 -0
  37. package/src/Isolate/IsolateTransient.ts +27 -0
  38. package/src/Isolate/IsolateTypes.ts +33 -0
  39. package/src/Isolate/__tests__/Isolate.test.ts +123 -0
  40. package/src/Isolate/__tests__/IsolateFocused.test.ts +199 -0
  41. package/src/Isolate/__tests__/IsolateInspector.test.ts +136 -0
  42. package/src/Isolate/__tests__/IsolateMutator.test.ts +164 -0
  43. package/src/Isolate/__tests__/IsolatePropagation.test.ts +170 -0
  44. package/src/Isolate/__tests__/IsolateReorderable.test.ts +111 -0
  45. package/src/Isolate/__tests__/IsolateSelectors.test.ts +72 -0
  46. package/src/Isolate/__tests__/IsolateStatus.test.ts +44 -0
  47. package/src/Isolate/__tests__/IsolateTransient.test.ts +58 -0
  48. package/src/Isolate/__tests__/__snapshots__/asyncIsolate.test.ts.snap +71 -0
  49. package/src/Isolate/__tests__/asyncIsolate.test.ts +85 -0
  50. package/src/IsolateWalker.ts +359 -0
  51. package/src/Orchestrator/RuntimeStates.ts +4 -0
  52. package/src/Reconciler.ts +178 -0
  53. package/src/RuntimeEvents.ts +9 -0
  54. package/src/VestRuntime.ts +421 -0
  55. package/src/__tests__/Bus.test.ts +57 -0
  56. package/src/__tests__/IsolateWalker.iterative.test.ts +77 -0
  57. package/src/__tests__/IsolateWalker.test.ts +418 -0
  58. package/src/__tests__/Reconciler.test.ts +193 -0
  59. package/src/__tests__/Reconciler.transient.test.ts +166 -0
  60. package/src/__tests__/VestRuntime.test.ts +212 -0
  61. package/src/__tests__/VestRuntimeStateMachine.test.ts +36 -0
  62. package/src/__tests__/vestjs-runtime.test.ts +19 -0
  63. package/src/errors/ErrorStrings.ts +6 -0
  64. package/src/exports/IsolateSerializer.ts +131 -0
  65. package/src/exports/__tests__/IsolateSerializer.test.ts +334 -0
  66. package/src/exports/__tests__/IsolateSerializer.transient.test.ts +101 -0
  67. package/src/exports/__tests__/__snapshots__/IsolateSerializer.test.ts.snap +5 -0
  68. package/src/exports/test-utils.ts +17 -0
  69. package/src/vestjs-runtime.ts +28 -0
  70. package/test-utils/package.json +12 -8
  71. package/types/Isolate-DChR7h5K.d.mts +58 -0
  72. package/types/Isolate-DChR7h5K.d.mts.map +1 -0
  73. package/types/Isolate-HYIh82M8.d.cts +58 -0
  74. package/types/Isolate-HYIh82M8.d.cts.map +1 -0
  75. package/types/IsolateSerializer-BCg01Px5.d.mts +13 -0
  76. package/types/IsolateSerializer-BCg01Px5.d.mts.map +1 -0
  77. package/types/IsolateSerializer-CQpP6A4m.d.cts +13 -0
  78. package/types/IsolateSerializer-CQpP6A4m.d.cts.map +1 -0
  79. package/types/exports/IsolateSerializer.d.cts +3 -0
  80. package/types/exports/IsolateSerializer.d.mts +3 -0
  81. package/types/exports/test-utils.d.cts +7 -0
  82. package/types/exports/test-utils.d.cts.map +1 -0
  83. package/types/exports/test-utils.d.mts +7 -0
  84. package/types/exports/test-utils.d.mts.map +1 -0
  85. package/types/vestjs-runtime.d.cts +372 -0
  86. package/types/vestjs-runtime.d.cts.map +1 -0
  87. package/types/vestjs-runtime.d.mts +370 -0
  88. package/types/vestjs-runtime.d.mts.map +1 -0
  89. package/types/vestjs-runtime.d.ts +351 -257
  90. package/vitest.config.ts +9 -17
  91. package/dist/cjs/IsolateSerializer.development.js +0 -135
  92. package/dist/cjs/IsolateSerializer.development.js.map +0 -1
  93. package/dist/cjs/IsolateSerializer.js +0 -6
  94. package/dist/cjs/IsolateSerializer.production.js +0 -2
  95. package/dist/cjs/IsolateSerializer.production.js.map +0 -1
  96. package/dist/cjs/package.json +0 -1
  97. package/dist/cjs/test-utils.development.js +0 -61
  98. package/dist/cjs/test-utils.development.js.map +0 -1
  99. package/dist/cjs/test-utils.js +0 -6
  100. package/dist/cjs/test-utils.production.js +0 -2
  101. package/dist/cjs/test-utils.production.js.map +0 -1
  102. package/dist/cjs/vestjs-runtime.development.js +0 -686
  103. package/dist/cjs/vestjs-runtime.development.js.map +0 -1
  104. package/dist/cjs/vestjs-runtime.js +0 -6
  105. package/dist/cjs/vestjs-runtime.production.js +0 -2
  106. package/dist/cjs/vestjs-runtime.production.js.map +0 -1
  107. package/dist/es/IsolateSerializer.development.js +0 -133
  108. package/dist/es/IsolateSerializer.development.js.map +0 -1
  109. package/dist/es/IsolateSerializer.production.js +0 -2
  110. package/dist/es/IsolateSerializer.production.js.map +0 -1
  111. package/dist/es/package.json +0 -1
  112. package/dist/es/test-utils.development.js +0 -59
  113. package/dist/es/test-utils.development.js.map +0 -1
  114. package/dist/es/test-utils.production.js +0 -2
  115. package/dist/es/test-utils.production.js.map +0 -1
  116. package/dist/es/vestjs-runtime.development.js +0 -675
  117. package/dist/es/vestjs-runtime.development.js.map +0 -1
  118. package/dist/es/vestjs-runtime.production.js +0 -2
  119. package/dist/es/vestjs-runtime.production.js.map +0 -1
  120. package/dist/umd/IsolateSerializer.development.js +0 -138
  121. package/dist/umd/IsolateSerializer.development.js.map +0 -1
  122. package/dist/umd/IsolateSerializer.production.js +0 -2
  123. package/dist/umd/IsolateSerializer.production.js.map +0 -1
  124. package/dist/umd/test-utils.development.js +0 -67
  125. package/dist/umd/test-utils.development.js.map +0 -1
  126. package/dist/umd/test-utils.production.js +0 -2
  127. package/dist/umd/test-utils.production.js.map +0 -1
  128. package/dist/umd/vestjs-runtime.development.js +0 -688
  129. package/dist/umd/vestjs-runtime.development.js.map +0 -1
  130. package/dist/umd/vestjs-runtime.production.js +0 -2
  131. package/dist/umd/vestjs-runtime.production.js.map +0 -1
  132. package/types/IsolateSerializer.d.ts +0 -42
  133. package/types/IsolateSerializer.d.ts.map +0 -1
  134. package/types/test-utils.d.ts +0 -37
  135. package/types/test-utils.d.ts.map +0 -1
  136. package/types/vestjs-runtime.d.ts.map +0 -1
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { CB } from 'vest-utils';
3
+
4
+ import { IReconciler } from '../Reconciler';
5
+ import { Isolate, TIsolate } from '../Isolate/Isolate';
6
+ import * as VestRuntime from '../VestRuntime';
7
+ import { StateRefType } from '../VestRuntime';
8
+
9
+ /**
10
+ * A reconciler that returns the history node when types match,
11
+ * causing Isolate.create to skip the callback (reconciled, not re-run).
12
+ * When types don't match or history is missing, it returns null
13
+ * so the node runs as new.
14
+ */
15
+ function reuseReconciler(
16
+ currentNode: TIsolate,
17
+ historyNode: TIsolate,
18
+ ): TIsolate | null {
19
+ // Always re-run Root so its callback executes and creates children.
20
+ if (currentNode.$type === 'Root') {
21
+ return null;
22
+ }
23
+ if (currentNode.$type === historyNode.$type) {
24
+ return historyNode;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ describe('Reconciler: Transient Isolates', () => {
30
+ function withRunTime<T>(stateRef: StateRefType, fn: CB<T>) {
31
+ return VestRuntime.Run(stateRef, () => fn());
32
+ }
33
+
34
+ /**
35
+ * Creates a fresh history tree. Uses a pass-through reconciler
36
+ * (returns null → BaseReconciler → runs as new) so all callbacks execute.
37
+ */
38
+ function createHistory(fn: CB): TIsolate {
39
+ const tempRef = VestRuntime.createRef(
40
+ (() => null) as unknown as IReconciler,
41
+ v => v,
42
+ );
43
+ let root!: TIsolate;
44
+ withRunTime(tempRef, () => {
45
+ root = fn();
46
+ });
47
+ return root;
48
+ }
49
+
50
+ /**
51
+ * Creates a stateRef wired to the `reuseReconciler` and seeds it
52
+ * with the given history root.
53
+ */
54
+ function createReconcilerRef(historyRoot: TIsolate): StateRefType {
55
+ const ref = VestRuntime.createRef(
56
+ reuseReconciler as unknown as IReconciler,
57
+ v => v,
58
+ );
59
+ const [, setHistory] = ref.historyRoot();
60
+ setHistory(historyRoot);
61
+ return ref;
62
+ }
63
+
64
+ type NodeSpec = { type: string; key?: string; transient?: boolean };
65
+
66
+ function buildTree(specs: NodeSpec[]): CB {
67
+ return () =>
68
+ Isolate.create('Root', () => {
69
+ for (const spec of specs) {
70
+ Isolate.create(
71
+ spec.type,
72
+ () => {},
73
+ spec.transient ? { transient: true } : {},
74
+ spec.key ?? null,
75
+ );
76
+ }
77
+ });
78
+ }
79
+
80
+ const A: NodeSpec = { type: 'Test', key: 'A' };
81
+ const B: NodeSpec = { type: 'Test', key: 'B' };
82
+ const T: NodeSpec = { type: 'Skip', transient: true };
83
+
84
+ it.each([
85
+ {
86
+ label: 'Adding: History [A, B] → Current [A, T, B]',
87
+ history: [A, B],
88
+ current: [A, T, B],
89
+ callsA: 0,
90
+ callsB: 0,
91
+ },
92
+ {
93
+ label: 'Removing: History [A, T, B] → Current [A, B]',
94
+ history: [A, T, B],
95
+ current: [A, B],
96
+ callsA: 0,
97
+ callsB: 0,
98
+ },
99
+ {
100
+ label: 'Moving: History [T, A, B] → Current [A, T, B]',
101
+ history: [T, A, B],
102
+ current: [A, T, B],
103
+ callsA: 0,
104
+ callsB: 0,
105
+ },
106
+ {
107
+ label: 'Baseline: History [A, B] → Current [A, B]',
108
+ history: [A, B],
109
+ current: [A, B],
110
+ callsA: 0,
111
+ callsB: 0,
112
+ },
113
+ {
114
+ label: 'New node: History [A] → Current [A, B]',
115
+ history: [A],
116
+ current: [A, B],
117
+ callsA: 0,
118
+ callsB: 1,
119
+ },
120
+ ])(
121
+ '$label',
122
+ ({ history, current, callsA: expectedCallsA, callsB: expectedCallsB }) => {
123
+ const cbA = vi.fn();
124
+ const cbB = vi.fn();
125
+
126
+ const historyRoot = createHistory(buildTree(history));
127
+
128
+ const ref = createReconcilerRef(historyRoot);
129
+ withRunTime(ref, () => {
130
+ Isolate.create('Root', () => {
131
+ for (const spec of current) {
132
+ const cb =
133
+ spec.key === 'A' ? cbA : spec.key === 'B' ? cbB : () => {};
134
+ Isolate.create(
135
+ spec.type,
136
+ cb,
137
+ spec.transient ? { transient: true } : {},
138
+ spec.key ?? null,
139
+ );
140
+ }
141
+ });
142
+ });
143
+
144
+ expect(cbA).toHaveBeenCalledTimes(expectedCallsA);
145
+ expect(cbB).toHaveBeenCalledTimes(expectedCallsB);
146
+ },
147
+ );
148
+
149
+ it('Transient callback itself DOES run (it has no history)', () => {
150
+ const cbTrans = vi.fn();
151
+
152
+ const historyRoot = createHistory(buildTree([A, B]));
153
+
154
+ const ref = createReconcilerRef(historyRoot);
155
+ withRunTime(ref, () => {
156
+ Isolate.create('Root', () => {
157
+ Isolate.create('Test', () => {}, {}, 'A');
158
+ Isolate.create('Skip', cbTrans, { transient: true });
159
+ Isolate.create('Test', () => {}, {}, 'B');
160
+ });
161
+ });
162
+
163
+ // Transient nodes always run as new
164
+ expect(cbTrans).toHaveBeenCalledOnce();
165
+ });
166
+ });
@@ -0,0 +1,212 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ Run,
4
+ createRef,
5
+ useCurrentCursor,
6
+ useSetIsolateKey,
7
+ } from '../VestRuntime';
8
+ import { IReconciler } from '../Reconciler';
9
+ import * as VestRuntime from '../VestRuntime';
10
+ import { IsolateInspector } from '../Isolate/IsolateInspector';
11
+ import { IsolateMutator } from '../Isolate/IsolateMutator';
12
+
13
+ import { ErrorStrings } from '../errors/ErrorStrings';
14
+ import { deferThrow, text } from 'vest-utils';
15
+ import { FocusModes, VestIsolateTypeFocused } from '../Isolate/IsolateFocused';
16
+
17
+ vi.mock('../Isolate/IsolateInspector', () => ({
18
+ IsolateInspector: {
19
+ cursor: vi.fn(),
20
+ getChildByKey: vi.fn(),
21
+ },
22
+ }));
23
+
24
+ vi.mock('../IsolateWalker', () => ({
25
+ findClosest: vi.fn(),
26
+ }));
27
+
28
+ vi.mock('../Isolate/IsolateMutator', () => ({
29
+ IsolateMutator: {
30
+ addChildKey: vi.fn(),
31
+ addChild: vi.fn(),
32
+ setParent: vi.fn(),
33
+ },
34
+ }));
35
+
36
+ vi.mock('vest-utils', async importOriginal => {
37
+ const actual = await importOriginal();
38
+ return {
39
+ ...(actual as any),
40
+ deferThrow: vi.fn(),
41
+ text: vi.fn(str => str),
42
+ invariant: vi.fn(),
43
+ };
44
+ });
45
+
46
+ vi.mock('context', () => ({
47
+ createCascade: () => ({
48
+ run: (stateRef: any, fn: () => void) => {
49
+ // Create a mutable object
50
+ const ctx = { stateRef, runtimeNode: null };
51
+ // Make it accessible to useX via closure?
52
+ // This closure is per createCascade call.
53
+ // VestRuntime calls createCascade once at top level.
54
+ // So this works perfectly.
55
+ (global as any).__mockCtx = ctx;
56
+ return fn();
57
+ },
58
+ useX: () => (global as any).__mockCtx,
59
+ use: () => (global as any).__mockCtx,
60
+ }),
61
+ }));
62
+
63
+ describe('VestRuntime', () => {
64
+ let reconciler: IReconciler;
65
+
66
+ beforeEach(() => {
67
+ reconciler = vi.fn();
68
+ vi.resetAllMocks();
69
+ });
70
+
71
+ const withRun = (fn: () => void, runtimeNode: any = null) => {
72
+ const ref = createRef(reconciler, {} as any);
73
+ // Determine how to inject runtimeNode?
74
+ // ref is internal state.
75
+ // 'runtimeNode' property in context.
76
+ // Run(ref, fn) -> PersistedContext.run sets the context.
77
+
78
+ // We can't easily inject runtimeNode via `createRef`.
79
+ // But we can use `PersistedContext.run(ctx, ...)` if we had access.
80
+ // `createCascade` returns an object with `run`.
81
+
82
+ // We can use a trick:
83
+ // Run(ref, () => {
84
+ // // Inside here, useX().runtimeNode = runtimeNode;
85
+ // // But `runtimeNode` is on `CTXType` which `useX()` returns.
86
+ // // In `VestRuntime.ts`, `useX` returns `CTXType`.
87
+ // // We can mutate it!
88
+ // if (runtimeNode) {
89
+ // const ctx = VestRuntime.useX();
90
+ // ctx.runtimeNode = runtimeNode;
91
+ // }
92
+ // fn();
93
+ // });
94
+
95
+ Run(ref, () => {
96
+ if (runtimeNode) {
97
+ // @ts-ignore
98
+ VestRuntime.useX().runtimeNode = runtimeNode;
99
+ }
100
+ fn();
101
+ });
102
+ };
103
+
104
+ describe('useCurrentCursor', () => {
105
+ it('Should return 0 when there is no active isolate', () => {
106
+ withRun(() => {
107
+ expect(useCurrentCursor()).toBe(0);
108
+ });
109
+ });
110
+
111
+ it('Should return the cursor of the active isolate', () => {
112
+ const node = { $type: 'test' };
113
+ vi.mocked(IsolateInspector.cursor).mockReturnValue(10);
114
+
115
+ withRun(() => {
116
+ expect(useCurrentCursor()).toBe(10);
117
+ expect(IsolateInspector.cursor).toHaveBeenCalledWith(node);
118
+ }, node);
119
+ });
120
+ });
121
+
122
+ describe('useSetIsolateKey', () => {
123
+ it('Should return early if key is null', () => {
124
+ withRun(() => {
125
+ useSetIsolateKey(null, {} as any);
126
+ expect(true).toBe(true);
127
+ });
128
+ });
129
+
130
+ it('Should add child key if not exists', () => {
131
+ const node = { $type: 'child' };
132
+ const parent = { $type: 'parent' };
133
+
134
+ vi.mocked(IsolateInspector.getChildByKey).mockReturnValue(null);
135
+
136
+ withRun(() => {
137
+ useSetIsolateKey('key1', node as any);
138
+ expect(IsolateMutator.addChildKey).toHaveBeenCalledWith(
139
+ parent,
140
+ 'key1',
141
+ node,
142
+ );
143
+ }, parent);
144
+ });
145
+
146
+ it('Should deferThrow if key exists', () => {
147
+ const node = { $type: 'child' };
148
+ const parent = { $type: 'parent' };
149
+
150
+ vi.mocked(IsolateInspector.getChildByKey).mockReturnValue({} as any);
151
+
152
+ withRun(() => {
153
+ useSetIsolateKey('key1', node as any);
154
+ expect(deferThrow).toHaveBeenCalled();
155
+ expect(text).toHaveBeenCalledWith(
156
+ ErrorStrings.ENCOUNTERED_THE_SAME_KEY_TWICE,
157
+ { key: 'key1' },
158
+ );
159
+ expect(IsolateMutator.addChildKey).not.toHaveBeenCalled();
160
+ }, parent);
161
+ });
162
+ });
163
+
164
+ describe('useSetNextIsolateChild', () => {
165
+ it('Should add child to current isolate and set parent', () => {
166
+ const parent = { $type: 'Parent' };
167
+ const child = { $type: 'Child' };
168
+ withRun(() => {
169
+ VestRuntime.useSetNextIsolateChild(child as any);
170
+ expect(IsolateMutator.addChild).toHaveBeenCalledWith(parent, child);
171
+ expect(IsolateMutator.setParent).toHaveBeenCalledWith(child, parent);
172
+ }, parent);
173
+ });
174
+
175
+ it('Should add parent to implicitOnlyNodes if child is ONLY focused', () => {
176
+ const parent = { $type: 'Parent' };
177
+ const child = {
178
+ $type: VestIsolateTypeFocused,
179
+ data: { focusMode: FocusModes.ONLY },
180
+ };
181
+ withRun(() => {
182
+ VestRuntime.useSetNextIsolateChild(child as any);
183
+ const ctx = (global as any).__mockCtx;
184
+ expect(ctx.stateRef.implicitOnlyNodes.has(parent)).toBe(true);
185
+ }, parent);
186
+ });
187
+ });
188
+
189
+ describe('hasImplicitOnly', () => {
190
+ it('Should return true if an ancestor is in implicitOnlyNodes', () => {
191
+ const grandparent = { $type: 'Grandparent' };
192
+ const parent = { $type: 'Parent', parent: grandparent };
193
+ const child = { $type: 'Child', parent };
194
+
195
+ withRun(() => {
196
+ const ctx = (global as any).__mockCtx;
197
+ ctx.stateRef.implicitOnlyNodes.add(grandparent);
198
+ // The current node is `child`
199
+ expect(VestRuntime.useIsFocusedOut()).toBe(true);
200
+ }, child);
201
+ });
202
+
203
+ it('Should return false if no ancestor is in implicitOnlyNodes', () => {
204
+ const parent = { $type: 'Parent' };
205
+ const child = { $type: 'Child', parent };
206
+
207
+ withRun(() => {
208
+ expect(VestRuntime.useIsFocusedOut()).toBe(false);
209
+ }, child);
210
+ });
211
+ });
212
+ });
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { VestRuntime } from '../vestjs-runtime';
3
+ import { IsolateMutator } from '../Isolate/IsolateMutator';
4
+ import { Isolate } from '../Isolate/Isolate';
5
+ import { RuntimeState } from '../Orchestrator/RuntimeStates';
6
+
7
+ describe('Runtime Orchestration', () => {
8
+ it('Should transition to PENDING if an isolate is pending after mount', () => {
9
+ VestRuntime.Run(VestRuntime.createRef({} as any, {} as any), () => {
10
+ // Create a pending isolate
11
+ const isolate = Isolate.create('test', async () => {
12
+ await new Promise(() => {});
13
+ });
14
+
15
+ IsolateMutator.setParent(isolate, VestRuntime.useAvailableRoot());
16
+ IsolateMutator.setPending(isolate);
17
+
18
+ expect(VestRuntime.useRuntimeState()).toBe(RuntimeState.PENDING);
19
+ });
20
+ });
21
+
22
+ it('Should transition to STABLE when pending isolate is done', () => {
23
+ VestRuntime.Run(VestRuntime.createRef({} as any, {} as any), () => {
24
+ const isolate = Isolate.create('test', async () => {
25
+ await new Promise(() => {});
26
+ });
27
+ IsolateMutator.setParent(isolate, VestRuntime.useAvailableRoot());
28
+ IsolateMutator.setPending(isolate);
29
+
30
+ // Mark as done
31
+ IsolateMutator.setDone(isolate);
32
+
33
+ expect(VestRuntime.useRuntimeState()).toBe(RuntimeState.STABLE);
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as VestJSRuntime from '../vestjs-runtime';
3
+
4
+ describe('vestjs-runtime exports', () => {
5
+ it('should export public API', () => {
6
+ expect(VestJSRuntime.Isolate).toBeDefined();
7
+ expect(VestJSRuntime.IsolateKeys).toBeDefined();
8
+ expect(VestJSRuntime.Reconciler).toBeDefined();
9
+ expect(VestJSRuntime.Walker).toBeDefined();
10
+ expect(VestJSRuntime.VestRuntime).toBeDefined();
11
+ expect(VestJSRuntime.IsolateInspector).toBeDefined();
12
+ expect(VestJSRuntime.IsolateMutator).toBeDefined();
13
+ expect(VestJSRuntime.Bus).toBeDefined();
14
+ expect(VestJSRuntime.IsolateSelectors).toBeDefined();
15
+ expect(VestJSRuntime.IsolateSerializer).toBeDefined();
16
+ expect(VestJSRuntime.IsolateStatus).toBeDefined();
17
+ expect(VestJSRuntime.IsolateStateMachine).toBeDefined();
18
+ });
19
+ });
@@ -0,0 +1,6 @@
1
+ export enum ErrorStrings {
2
+ NO_ACTIVE_ISOLATE = 'Not within an active isolate',
3
+ UNABLE_TO_PICK_NEXT_ISOLATE = 'Unable to pick next isolate. This is a bug, please report it to the Vest maintainers.',
4
+ ENCOUNTERED_THE_SAME_KEY_TWICE = `Encountered the same key "{key}" twice. This may lead to inconsistent or overriding of results.`,
5
+ INVALID_ISOLATE_CANNOT_PARSE = `Invalid isolate was passed to IsolateSerializer. Cannot proceed.`,
6
+ }
@@ -0,0 +1,131 @@
1
+ import {
2
+ Nullable,
3
+ hasOwnProperty,
4
+ isNullish,
5
+ isStringValue,
6
+ isFailure,
7
+ text,
8
+ makeResult,
9
+ Result,
10
+ isUnsafeKey,
11
+ } from 'vest-utils';
12
+ import { expandObject, minifyObject } from 'vest-utils/minifyObject';
13
+
14
+ import { TIsolate } from '../Isolate/Isolate';
15
+ import { ExcludedFromDump, IsolateKeys } from '../Isolate/IsolateKeys';
16
+ import { IsolateMutator } from '../Isolate/IsolateMutator';
17
+ import { ErrorStrings } from '../errors/ErrorStrings';
18
+
19
+ export class IsolateSerializer {
20
+ static safeDeserialize(
21
+ node: Record<string, any> | TIsolate | string,
22
+ ): Result<TIsolate, Error> {
23
+ try {
24
+ const expanded = expandNode(node);
25
+ if (isFailure(IsolateSerializer.validateIsolate(expanded))) {
26
+ return makeResult.Err(
27
+ new Error(ErrorStrings.INVALID_ISOLATE_CANNOT_PARSE),
28
+ );
29
+ }
30
+ return makeResult.Ok(hydrateIsolate(expanded));
31
+ } catch (error) {
32
+ return makeResult.Err(
33
+ error instanceof Error ? error : new Error(String(error)),
34
+ );
35
+ }
36
+ }
37
+
38
+ static deserialize(node: Record<string, any> | TIsolate | string): TIsolate {
39
+ const result = IsolateSerializer.safeDeserialize(node);
40
+ return result.unwrap();
41
+ }
42
+
43
+ static serialize(
44
+ isolate: Nullable<TIsolate>,
45
+ replacer?: (value: any, key: string) => any,
46
+ ): string {
47
+ if (isNullish(isolate)) {
48
+ return '';
49
+ }
50
+
51
+ const minified = minifyObject(isolate, (value: any, key: string) => {
52
+ if (ExcludedFromDump.has(key as any)) {
53
+ return undefined;
54
+ }
55
+
56
+ // Drop transient nodes — returning undefined causes
57
+ // minifyObject to skip the entry entirely.
58
+ // Transient nodes are not part of the persistent state
59
+ // and should not be serialized.
60
+ if (value?.transient) {
61
+ return undefined;
62
+ }
63
+
64
+ if (replacer) {
65
+ return replacer(value, key);
66
+ }
67
+ return value;
68
+ });
69
+
70
+ return JSON.stringify(minified);
71
+ }
72
+
73
+ static validateIsolate(
74
+ node: Record<string, any> | TIsolate,
75
+ ): Result<TIsolate, string> {
76
+ return hasOwnProperty(node, IsolateKeys.Type)
77
+ ? makeResult.Ok(node as TIsolate)
78
+ : makeResult.Err(text(ErrorStrings.INVALID_ISOLATE_CANNOT_PARSE));
79
+ }
80
+ }
81
+
82
+ function processChildren(current: TIsolate, queue: TIsolate[]): void {
83
+ const children = current.children;
84
+
85
+ if (!children) {
86
+ return;
87
+ }
88
+
89
+ current.children = children.map(child => {
90
+ const nextChild = { ...child };
91
+
92
+ IsolateMutator.setParent(nextChild, current);
93
+ queue.push(nextChild);
94
+
95
+ if (nextChild.key) {
96
+ current.keys = current.keys ?? {};
97
+ current.keys[nextChild.key] = nextChild;
98
+ }
99
+
100
+ return nextChild;
101
+ });
102
+ }
103
+
104
+ function hydrateIsolate(root: TIsolate): TIsolate {
105
+ const queue = [root];
106
+
107
+ while (queue.length) {
108
+ const current = queue.shift();
109
+ if (current) {
110
+ processChildren(current, queue);
111
+ }
112
+ }
113
+
114
+ return root;
115
+ }
116
+
117
+ function expandNode(node: Record<string, any> | TIsolate | string): TIsolate {
118
+ const parsed = isStringValue(node)
119
+ ? JSON.parse(node, safeReviver)
120
+ : ({ ...node } as TIsolate);
121
+ const root = Array.isArray(parsed) ? parsed : [parsed, {}];
122
+ const expanded = expandObject(root[0], root[1]);
123
+ return expanded as TIsolate;
124
+ }
125
+
126
+ function safeReviver(key: string, value: any): any {
127
+ if (isUnsafeKey(key)) {
128
+ return;
129
+ }
130
+ return value;
131
+ }