@fragno-dev/core 0.2.0 → 0.2.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 (146) hide show
  1. package/.turbo/turbo-build.log +72 -62
  2. package/CHANGELOG.md +28 -0
  3. package/dist/api/api.d.ts +3 -2
  4. package/dist/api/api.d.ts.map +1 -1
  5. package/dist/api/api.js +2 -1
  6. package/dist/api/api.js.map +1 -1
  7. package/dist/api/bind-services.d.ts +0 -1
  8. package/dist/api/bind-services.d.ts.map +1 -1
  9. package/dist/api/bind-services.js.map +1 -1
  10. package/dist/api/error.d.ts.map +1 -1
  11. package/dist/api/error.js.map +1 -1
  12. package/dist/api/fragment-definition-builder.d.ts +26 -44
  13. package/dist/api/fragment-definition-builder.d.ts.map +1 -1
  14. package/dist/api/fragment-definition-builder.js +15 -22
  15. package/dist/api/fragment-definition-builder.js.map +1 -1
  16. package/dist/api/fragment-instantiator.d.ts +51 -37
  17. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  18. package/dist/api/fragment-instantiator.js +74 -69
  19. package/dist/api/fragment-instantiator.js.map +1 -1
  20. package/dist/api/request-context-storage.d.ts +4 -0
  21. package/dist/api/request-context-storage.d.ts.map +1 -1
  22. package/dist/api/request-context-storage.js +6 -0
  23. package/dist/api/request-context-storage.js.map +1 -1
  24. package/dist/api/request-input-context.d.ts.map +1 -1
  25. package/dist/api/request-input-context.js.map +1 -1
  26. package/dist/api/request-middleware.d.ts +1 -1
  27. package/dist/api/request-middleware.d.ts.map +1 -1
  28. package/dist/api/request-middleware.js.map +1 -1
  29. package/dist/api/request-output-context.d.ts +1 -1
  30. package/dist/api/request-output-context.d.ts.map +1 -1
  31. package/dist/api/request-output-context.js.map +1 -1
  32. package/dist/api/route-caller.d.ts +30 -0
  33. package/dist/api/route-caller.d.ts.map +1 -0
  34. package/dist/api/route-caller.js +63 -0
  35. package/dist/api/route-caller.js.map +1 -0
  36. package/dist/api/route-handler-input-options.d.ts.map +1 -1
  37. package/dist/api/route.d.ts +1 -1
  38. package/dist/api/route.d.ts.map +1 -1
  39. package/dist/api/route.js.map +1 -1
  40. package/dist/api/shared-types.d.ts.map +1 -1
  41. package/dist/client/client-error.d.ts.map +1 -1
  42. package/dist/client/client-error.js.map +1 -1
  43. package/dist/client/client.d.ts +91 -52
  44. package/dist/client/client.d.ts.map +1 -1
  45. package/dist/client/client.js +25 -9
  46. package/dist/client/client.js.map +1 -1
  47. package/dist/client/client.svelte.d.ts +6 -5
  48. package/dist/client/client.svelte.d.ts.map +1 -1
  49. package/dist/client/client.svelte.js +10 -2
  50. package/dist/client/client.svelte.js.map +1 -1
  51. package/dist/client/internal/ndjson-streaming.js.map +1 -1
  52. package/dist/client/react.d.ts +5 -4
  53. package/dist/client/react.d.ts.map +1 -1
  54. package/dist/client/react.js +104 -12
  55. package/dist/client/react.js.map +1 -1
  56. package/dist/client/solid.d.ts +7 -5
  57. package/dist/client/solid.d.ts.map +1 -1
  58. package/dist/client/solid.js +23 -9
  59. package/dist/client/solid.js.map +1 -1
  60. package/dist/client/vanilla.d.ts +16 -4
  61. package/dist/client/vanilla.d.ts.map +1 -1
  62. package/dist/client/vanilla.js +21 -1
  63. package/dist/client/vanilla.js.map +1 -1
  64. package/dist/client/vue.d.ts +7 -5
  65. package/dist/client/vue.d.ts.map +1 -1
  66. package/dist/client/vue.js +18 -10
  67. package/dist/client/vue.js.map +1 -1
  68. package/dist/id.d.ts +2 -0
  69. package/dist/id.js +3 -0
  70. package/dist/internal/cuid.d.ts +16 -0
  71. package/dist/internal/cuid.d.ts.map +1 -0
  72. package/dist/internal/cuid.js +82 -0
  73. package/dist/internal/cuid.js.map +1 -0
  74. package/dist/mod-client.d.ts +5 -4
  75. package/dist/mod-client.d.ts.map +1 -1
  76. package/dist/mod-client.js +7 -5
  77. package/dist/mod-client.js.map +1 -1
  78. package/dist/mod.d.ts +6 -5
  79. package/dist/mod.js +2 -1
  80. package/dist/runtime.js +1 -1
  81. package/dist/runtime.js.map +1 -1
  82. package/dist/test/test.d.ts +6 -6
  83. package/dist/test/test.d.ts.map +1 -1
  84. package/dist/test/test.js.map +1 -1
  85. package/dist/util/ssr.js.map +1 -1
  86. package/package.json +24 -40
  87. package/src/api/api.test.ts +3 -1
  88. package/src/api/api.ts +6 -0
  89. package/src/api/bind-services.ts +0 -5
  90. package/src/api/error.ts +1 -0
  91. package/src/api/fragment-definition-builder.extend.test.ts +2 -1
  92. package/src/api/fragment-definition-builder.test.ts +2 -1
  93. package/src/api/fragment-definition-builder.ts +49 -124
  94. package/src/api/fragment-instantiator.test.ts +92 -233
  95. package/src/api/fragment-instantiator.ts +228 -196
  96. package/src/api/fragment-services.test.ts +1 -0
  97. package/src/api/internal/path-runtime.test.ts +1 -0
  98. package/src/api/internal/path-type.test.ts +3 -1
  99. package/src/api/internal/route.test.ts +1 -0
  100. package/src/api/request-context-storage.ts +7 -0
  101. package/src/api/request-input-context.test.ts +4 -2
  102. package/src/api/request-input-context.ts +2 -1
  103. package/src/api/request-middleware.test.ts +9 -14
  104. package/src/api/request-middleware.ts +3 -2
  105. package/src/api/request-output-context.test.ts +3 -1
  106. package/src/api/request-output-context.ts +2 -1
  107. package/src/api/route-caller.test.ts +195 -0
  108. package/src/api/route-caller.ts +167 -0
  109. package/src/api/route-handler-input-options.ts +2 -1
  110. package/src/api/route.test.ts +4 -2
  111. package/src/api/route.ts +2 -1
  112. package/src/api/shared-types.ts +2 -1
  113. package/src/client/client-builder.test.ts +4 -2
  114. package/src/client/client-error.test.ts +2 -1
  115. package/src/client/client-error.ts +1 -1
  116. package/src/client/client-types.test.ts +19 -5
  117. package/src/client/client.ssr.test.ts +6 -4
  118. package/src/client/client.svelte.test.ts +18 -9
  119. package/src/client/client.svelte.ts +38 -13
  120. package/src/client/client.test.ts +49 -10
  121. package/src/client/client.ts +291 -141
  122. package/src/client/internal/ndjson-streaming.test.ts +6 -3
  123. package/src/client/internal/ndjson-streaming.ts +1 -0
  124. package/src/client/react.test.ts +176 -6
  125. package/src/client/react.ts +226 -31
  126. package/src/client/solid.test.ts +29 -5
  127. package/src/client/solid.ts +60 -22
  128. package/src/client/vanilla.test.ts +148 -6
  129. package/src/client/vanilla.ts +63 -9
  130. package/src/client/vue.test.ts +223 -84
  131. package/src/client/vue.ts +57 -30
  132. package/src/id.ts +1 -0
  133. package/src/internal/cuid.test.ts +164 -0
  134. package/src/internal/cuid.ts +133 -0
  135. package/src/mod-client.ts +4 -2
  136. package/src/mod.ts +3 -2
  137. package/src/runtime.ts +1 -1
  138. package/src/test/test.test.ts +4 -2
  139. package/src/test/test.ts +7 -9
  140. package/src/util/async.test.ts +1 -0
  141. package/src/util/content-type.test.ts +1 -0
  142. package/src/util/nanostores.test.ts +3 -1
  143. package/src/util/ssr.ts +1 -0
  144. package/tsconfig.json +1 -1
  145. package/tsdown.config.ts +1 -0
  146. package/vitest.config.ts +2 -1
@@ -1,13 +1,15 @@
1
- import { test, expect, describe, vi, beforeEach, afterEach } from "vitest";
2
- import { atom } from "nanostores";
1
+ import { test, expect, describe, vi, beforeEach, afterEach, expectTypeOf } from "vitest";
2
+
3
+ import { atom, type ReadableAtom } from "nanostores";
3
4
  import { z } from "zod";
4
- import { createClientBuilder } from "./client";
5
- import { useFragno } from "./vanilla";
6
- import { defineRoute } from "../api/route";
5
+
7
6
  import { defineFragment } from "../api/fragment-definition-builder";
7
+ import { defineRoute } from "../api/route";
8
+ import { waitForAsyncIterator } from "../util/async";
9
+ import { createClientBuilder } from "./client";
8
10
  import type { FragnoPublicClientConfig } from "./client";
9
11
  import { FragnoClientFetchNetworkError } from "./client-error";
10
- import { waitForAsyncIterator } from "../util/async";
12
+ import { useFragno } from "./vanilla";
11
13
 
12
14
  // Mock fetch globally
13
15
  global.fetch = vi.fn();
@@ -620,6 +622,56 @@ describe("createVanillaMutator", () => {
620
622
  });
621
623
  });
622
624
 
625
+ describe("useFragno - createStore", () => {
626
+ const clientConfig: FragnoPublicClientConfig = {
627
+ baseUrl: "http://localhost:3000",
628
+ };
629
+
630
+ test("unwraps store wrappers to the raw store object", () => {
631
+ const stringAtom = atom("hello");
632
+ const numberAtom = atom(42);
633
+ const cb = createClientBuilder(defineFragment("test-fragment-store"), clientConfig, []);
634
+ const client = {
635
+ useStore: cb.createStore({
636
+ message: stringAtom,
637
+ count: numberAtom,
638
+ constant: 7,
639
+ }),
640
+ };
641
+
642
+ const { useStore } = useFragno(client);
643
+
644
+ expectTypeOf(useStore).toExtend<{
645
+ message: typeof stringAtom;
646
+ count: typeof numberAtom;
647
+ constant: number;
648
+ }>();
649
+
650
+ if (!("obj" in client.useStore)) {
651
+ throw new Error("Expected object-backed store");
652
+ }
653
+
654
+ expect(useStore).toBe(client.useStore.obj);
655
+ expect(useStore.message.get()).toBe("hello");
656
+ expect(useStore.count.get()).toBe(42);
657
+ expect(useStore.constant).toBe(7);
658
+ });
659
+
660
+ test("unwraps single-store wrappers to the underlying atom", () => {
661
+ const singleAtom = atom("single");
662
+ const cb = createClientBuilder(defineFragment("test-fragment-single-store"), clientConfig, []);
663
+ const client = {
664
+ useSingle: cb.createStore(singleAtom),
665
+ };
666
+
667
+ const { useSingle } = useFragno(client);
668
+
669
+ expectTypeOf(useSingle).toExtend<typeof singleAtom>();
670
+ expect(useSingle).toBe(singleAtom);
671
+ expect(useSingle.get()).toBe("single");
672
+ });
673
+ });
674
+
623
675
  describe("useFragno", () => {
624
676
  const testFragmentDefinition = defineFragment("test-fragment-useFragno");
625
677
  const testRoutes = [
@@ -771,6 +823,96 @@ describe("useFragno", () => {
771
823
  });
772
824
  });
773
825
 
826
+ describe("createVanillaStore", () => {
827
+ const clientConfig: FragnoPublicClientConfig = {
828
+ baseUrl: "http://localhost:3000",
829
+ };
830
+
831
+ test("should support createStore factory callbacks", () => {
832
+ const dispose = vi.fn();
833
+ const builder = createClientBuilder(defineFragment("test-fragment-store"), clientConfig, []);
834
+ const clientObj = {
835
+ useSession: builder.createStore(({ path }: { path: { sessionId: string } }) => ({
836
+ sessionId: atom(path.sessionId),
837
+ sendMessage: (text: string) => text,
838
+ [Symbol.dispose]: dispose,
839
+ })),
840
+ };
841
+
842
+ const { useSession } = useFragno(clientObj);
843
+ const session = useSession({ path: { sessionId: "abc" } });
844
+
845
+ expect(session.sessionId.get()).toBe("abc");
846
+ expect(session.sendMessage("hello")).toBe("hello");
847
+ expect(typeof session.destroy).toBe("function");
848
+
849
+ session.destroy?.();
850
+ expect(dispose).toHaveBeenCalledTimes(1);
851
+ });
852
+
853
+ test("should preserve prototype methods when adding destroy to factory store instances", () => {
854
+ const dispose = vi.fn();
855
+ const builder = createClientBuilder(
856
+ defineFragment("test-fragment-store-instance"),
857
+ clientConfig,
858
+ [],
859
+ );
860
+
861
+ class SessionStore {
862
+ sessionId: ReadableAtom<string>;
863
+
864
+ constructor(sessionId: string) {
865
+ this.sessionId = atom(sessionId);
866
+ }
867
+
868
+ sendMessage(text: string) {
869
+ return `${this.sessionId.get()}:${text}`;
870
+ }
871
+
872
+ [Symbol.dispose]() {
873
+ dispose();
874
+ }
875
+ }
876
+
877
+ const clientObj = {
878
+ useSession: builder.createStore(({ path }: { path: { sessionId: string } }) => {
879
+ return new SessionStore(path.sessionId);
880
+ }),
881
+ };
882
+
883
+ const { useSession } = useFragno(clientObj);
884
+ const session = useSession({ path: { sessionId: "abc" } });
885
+
886
+ expect(session).toBeInstanceOf(SessionStore);
887
+ expect(session.sendMessage("hello")).toBe("abc:hello");
888
+ expect(typeof session.destroy).toBe("function");
889
+ expect(Object.keys(session)).not.toContain("destroy");
890
+
891
+ session.destroy?.();
892
+ expect(dispose).toHaveBeenCalledTimes(1);
893
+ });
894
+
895
+ test("should type and return zero-argument factory stores as callable", () => {
896
+ const builder = createClientBuilder(
897
+ defineFragment("test-fragment-zero-arg-store"),
898
+ clientConfig,
899
+ [],
900
+ );
901
+ const countAtom = atom(0);
902
+ const clientObj = {
903
+ useCounter: builder.createStore(() => ({
904
+ count: countAtom,
905
+ })),
906
+ };
907
+
908
+ const { useCounter } = useFragno(clientObj);
909
+
910
+ expectTypeOf(useCounter).toExtend<() => { count: typeof countAtom }>();
911
+ expect(typeof useCounter).toBe("function");
912
+ expect(useCounter().count.get()).toBe(0);
913
+ });
914
+ });
915
+
774
916
  describe("error handling", () => {
775
917
  const testFragmentDefinition = defineFragment("test-fragment-errors");
776
918
  const testRoutes = [
@@ -1,20 +1,26 @@
1
- import type { StandardSchemaV1 } from "@standard-schema/spec";
2
1
  import type { ReadableAtom } from "nanostores";
2
+
3
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
4
+
3
5
  import type { NonGetHTTPMethod } from "../api/api";
6
+ import type {
7
+ ExtractPathParamsOrWiden,
8
+ HasPathParams,
9
+ MaybeExtractPathParamsOrWiden,
10
+ } from "../api/internal/path";
11
+ import { createAsyncIteratorFromCallback } from "../util/async";
12
+ import type { InferOr } from "../util/types-util";
4
13
  import {
5
14
  isGetHook,
6
15
  isMutatorHook,
16
+ isStore,
7
17
  type FragnoClientMutatorData,
8
18
  type FragnoClientHookData,
19
+ type FragnoStoreData,
20
+ type FragnoStoreFactoryData,
21
+ type FragnoStoreObjectData,
9
22
  } from "./client";
10
23
  import type { FragnoClientError } from "./client-error";
11
- import { createAsyncIteratorFromCallback } from "../util/async";
12
- import type { InferOr } from "../util/types-util";
13
- import type {
14
- ExtractPathParamsOrWiden,
15
- HasPathParams,
16
- MaybeExtractPathParamsOrWiden,
17
- } from "../api/internal/path";
18
24
 
19
25
  export type StoreData<
20
26
  TOutputSchema extends StandardSchemaV1 | undefined,
@@ -213,6 +219,48 @@ function createVanillaMutator<
213
219
  };
214
220
  }
215
221
 
222
+ export type FragnoVanillaStore<T extends object, TArgs extends unknown[] = []> = TArgs extends []
223
+ ? T
224
+ : (...args: TArgs) => T & { destroy?: () => void };
225
+
226
+ export type FragnoVanillaStoreFromData<TStore> = TStore extends {
227
+ factory: (...args: infer TArgs) => infer TObject extends object;
228
+ }
229
+ ? (...args: TArgs) => TObject & { destroy?: () => void }
230
+ : TStore extends { obj: infer TObject extends object }
231
+ ? TObject
232
+ : never;
233
+
234
+ const getStoreDisposer = (value: object): (() => void) | undefined => {
235
+ const disposer = (value as { [Symbol.dispose]?: (() => void) | undefined })[Symbol.dispose];
236
+ return typeof disposer === "function" ? disposer.bind(value) : undefined;
237
+ };
238
+
239
+ function createVanillaStore<const TStore extends FragnoStoreData<object, unknown[]>>(
240
+ hook: TStore,
241
+ ): FragnoVanillaStoreFromData<TStore> {
242
+ if ("obj" in hook) {
243
+ return hook.obj as FragnoVanillaStoreFromData<TStore>;
244
+ }
245
+
246
+ return ((...args: Parameters<typeof hook.factory>) => {
247
+ const value = hook.factory(...args);
248
+ const disposer = getStoreDisposer(value);
249
+ if (!disposer) {
250
+ return value as ReturnType<typeof hook.factory> & { destroy?: () => void };
251
+ }
252
+
253
+ Object.defineProperty(value, "destroy", {
254
+ value: disposer,
255
+ enumerable: false,
256
+ configurable: true,
257
+ writable: true,
258
+ });
259
+
260
+ return value as ReturnType<typeof hook.factory> & { destroy?: () => void };
261
+ }) as FragnoVanillaStoreFromData<TStore>;
262
+ }
263
+
216
264
  export function useFragno<T extends Record<string, unknown>>(
217
265
  clientObj: T,
218
266
  ): {
@@ -240,7 +288,11 @@ export function useFragno<T extends Record<string, unknown>>(
240
288
  TErrorCode,
241
289
  TQueryParameters
242
290
  >
243
- : T[K];
291
+ : T[K] extends FragnoStoreObjectData<infer TStoreObj>
292
+ ? TStoreObj
293
+ : T[K] extends FragnoStoreFactoryData<infer TStoreObj, infer TStoreArgs>
294
+ ? (...args: TStoreArgs) => TStoreObj & { destroy?: () => void }
295
+ : T[K];
244
296
  } {
245
297
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
298
  const result = {} as any; // We need one any cast here due to TypeScript's limitations with mapped types
@@ -255,6 +307,8 @@ export function useFragno<T extends Record<string, unknown>>(
255
307
  result[key] = createVanillaListeners(hook);
256
308
  } else if (isMutatorHook(hook)) {
257
309
  result[key] = createVanillaMutator(hook);
310
+ } else if (isStore(hook)) {
311
+ result[key] = createVanillaStore(hook);
258
312
  } else {
259
313
  // Pass through non-hook values unchanged
260
314
  result[key] = hook;
@@ -1,14 +1,17 @@
1
1
  import { test, expect, describe, vi, beforeEach, afterEach, assert, expectTypeOf } from "vitest";
2
- import { type FragnoPublicClientConfig } from "./client";
3
- import { createClientBuilder } from "./client";
4
- import { defineRoute } from "../api/route";
5
- import { defineFragment } from "../api/fragment-definition-builder";
2
+
3
+ import { atom, computed, type ReadableAtom } from "nanostores";
4
+ import { nextTick, ref, watch, effectScope } from "vue";
6
5
  import { z } from "zod";
7
- import { refToAtom, useFragno } from "./vue";
6
+
8
7
  import { waitFor } from "@testing-library/vue";
9
- import { nextTick, ref, watch, effectScope } from "vue";
8
+
9
+ import { defineFragment } from "../api/fragment-definition-builder";
10
+ import { defineRoute } from "../api/route";
11
+ import { type FragnoPublicClientConfig } from "./client";
12
+ import { createClientBuilder } from "./client";
10
13
  import { FragnoClientUnknownApiError } from "./client-error";
11
- import { atom, computed, type ReadableAtom } from "nanostores";
14
+ import { refToAtom, useFragno } from "./vue";
12
15
 
13
16
  global.fetch = vi.fn();
14
17
 
@@ -768,7 +771,7 @@ describe("useFragno - createStore", () => {
768
771
  });
769
772
 
770
773
  test("FragnoVueStore type test - ReadableAtom fields", () => {
771
- // Test that ReadableAtom fields are properly unwrapped to their value types
774
+ // Test that ReadableAtom fields are exposed as reactive refs
772
775
  const stringAtom: ReadableAtom<string> = atom("hello");
773
776
  const numberAtom: ReadableAtom<number> = atom(42);
774
777
  const booleanAtom: ReadableAtom<boolean> = atom(true);
@@ -788,32 +791,27 @@ describe("useFragno - createStore", () => {
788
791
 
789
792
  const { useStore } = useFragno(client);
790
793
 
791
- // Type assertions to ensure the types are correctly inferred
792
- expectTypeOf(useStore).toExtend<
793
- () => {
794
- message: string;
795
- count: number;
796
- isActive: boolean;
797
- data: { count: number };
798
- items: string[];
799
- }
800
- >();
801
-
802
- // Runtime test - need to run in effect scope for Vue reactivity
803
794
  const scope = effectScope();
804
795
  scope.run(() => {
805
796
  const result = useStore();
806
- expect(result.message).toBe("hello");
807
- expect(result.count).toBe(42);
808
- expect(result.isActive).toBe(true);
809
- expect(result.data).toEqual({ count: 0 });
810
- expect(result.items).toEqual(["a", "b", "c"]);
797
+
798
+ expectTypeOf(result.message.value).toEqualTypeOf<string>();
799
+ expectTypeOf(result.count.value).toEqualTypeOf<number>();
800
+ expectTypeOf(result.isActive.value).toEqualTypeOf<boolean>();
801
+ expectTypeOf(result.data.value.count).toEqualTypeOf<number>();
802
+ expectTypeOf(result.items.value).toExtend<readonly string[]>();
803
+
804
+ expect(result.message.value).toBe("hello");
805
+ expect(result.count.value).toBe(42);
806
+ expect(result.isActive.value).toBe(true);
807
+ expect(result.data.value).toEqual({ count: 0 });
808
+ expect(result.items.value).toEqual(["a", "b", "c"]);
811
809
  });
812
810
  scope.stop();
813
811
  });
814
812
 
815
813
  test("FragnoVueStore type test - computed stores", () => {
816
- // Test that computed stores (which are also ReadableAtom) are properly unwrapped
814
+ // Test that computed stores (which are also ReadableAtom) are exposed as refs
817
815
  const baseNumber = atom(10);
818
816
  const doubled = computed(baseNumber, (n) => n * 2);
819
817
  const tripled = computed(baseNumber, (n) => n * 3);
@@ -831,30 +829,26 @@ describe("useFragno - createStore", () => {
831
829
 
832
830
  const { useComputedValues } = useFragno(client);
833
831
 
834
- // Type assertions
835
- expectTypeOf(useComputedValues).toExtend<
836
- () => {
837
- base: number;
838
- doubled: number;
839
- tripled: number;
840
- combined: { doubled: number; tripled: number };
841
- }
842
- >();
843
-
844
- // Runtime test
845
832
  const scope = effectScope();
846
833
  scope.run(() => {
847
834
  const result = useComputedValues();
848
- expect(result.base).toBe(10);
849
- expect(result.doubled).toBe(20);
850
- expect(result.tripled).toBe(30);
851
- expect(result.combined).toEqual({ doubled: 20, tripled: 30 });
835
+
836
+ expectTypeOf(result.base.value).toEqualTypeOf<number>();
837
+ expectTypeOf(result.doubled.value).toEqualTypeOf<number>();
838
+ expectTypeOf(result.tripled.value).toEqualTypeOf<number>();
839
+ expectTypeOf(result.combined.value.doubled).toEqualTypeOf<number>();
840
+ expectTypeOf(result.combined.value.tripled).toEqualTypeOf<number>();
841
+
842
+ expect(result.base.value).toBe(10);
843
+ expect(result.doubled.value).toBe(20);
844
+ expect(result.tripled.value).toBe(30);
845
+ expect(result.combined.value).toEqual({ doubled: 20, tripled: 30 });
852
846
  });
853
847
  scope.stop();
854
848
  });
855
849
 
856
850
  test("FragnoVueStore type test - mixed store and non-store fields", () => {
857
- // Test that non-store fields are passed through unchanged
851
+ // Test that store fields are refs and non-store fields are passed through unchanged
858
852
  const messageAtom: ReadableAtom<string> = atom("test");
859
853
  const regularFunction = (x: number) => x * 2;
860
854
  const regularObject = { foo: "bar", baz: 123 };
@@ -871,21 +865,19 @@ describe("useFragno - createStore", () => {
871
865
 
872
866
  const { useMixed } = useFragno(client);
873
867
 
874
- // Type assertions
875
- expectTypeOf(useMixed).toExtend<
876
- () => {
877
- message: string;
878
- multiply: (x: number) => number;
879
- config: { foo: string; baz: number };
880
- constant: number;
881
- }
882
- >();
883
-
884
- // Runtime test
885
868
  const scope = effectScope();
886
869
  scope.run(() => {
887
870
  const result = useMixed();
888
- expect(result.message).toBe("test");
871
+
872
+ expectTypeOf(result.message.value).toEqualTypeOf<string>();
873
+ expectTypeOf(result.multiply).toExtend<(x: number) => number>();
874
+ expectTypeOf(result.config.foo).toEqualTypeOf<string>();
875
+ expectTypeOf(result.config.baz).toEqualTypeOf<number>();
876
+ expectTypeOf(result.constant).toExtend<number>();
877
+
878
+ expect(result.message.value).toBe("test");
879
+ expect(result.multiply).toBe(regularFunction);
880
+ expect(result.config).toBe(regularObject);
889
881
  expect(result.multiply(5)).toBe(10);
890
882
  expect(result.config).toEqual({ foo: "bar", baz: 123 });
891
883
  expect(result.constant).toBe(42);
@@ -894,7 +886,7 @@ describe("useFragno - createStore", () => {
894
886
  });
895
887
 
896
888
  test("FragnoVueStore type test - single store vs object with stores", () => {
897
- // Test that a single store is unwrapped directly
889
+ // Test that a single store is exposed as a ref directly
898
890
  const singleAtom: ReadableAtom<string> = atom("single");
899
891
  const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
900
892
 
@@ -903,7 +895,6 @@ describe("useFragno - createStore", () => {
903
895
  useSingle: cb.createStore(singleAtom),
904
896
  };
905
897
  const { useSingle } = useFragno(clientSingle);
906
- expectTypeOf(useSingle).toExtend<() => string>();
907
898
 
908
899
  // Object with stores case
909
900
  const clientObject = {
@@ -912,16 +903,17 @@ describe("useFragno - createStore", () => {
912
903
  }),
913
904
  };
914
905
  const { useObject } = useFragno(clientObject);
915
- expectTypeOf(useObject).toExtend<() => { value: string }>();
916
906
 
917
- // Runtime test
907
+ // Runtime and type test
918
908
  const scope = effectScope();
919
909
  scope.run(() => {
920
910
  const singleResult = useSingle();
921
- expect(singleResult).toBe("single");
911
+ expectTypeOf(singleResult.value).toEqualTypeOf<string>();
912
+ expect(singleResult.value).toBe("single");
922
913
 
923
914
  const objectResult = useObject();
924
- expect(objectResult).toEqual({ value: "single" });
915
+ expectTypeOf(objectResult.value.value).toEqualTypeOf<string>();
916
+ expect(objectResult.value.value).toBe("single");
925
917
  });
926
918
  scope.stop();
927
919
  });
@@ -948,30 +940,108 @@ describe("useFragno - createStore", () => {
948
940
 
949
941
  const { useAppState } = useFragno(client);
950
942
 
951
- // Type assertions for complex nested structure
952
- expectTypeOf(useAppState).toExtend<
953
- () => {
954
- user: User;
955
- settings: Settings;
956
- loading: boolean;
957
- error: string | null;
958
- }
959
- >();
960
-
961
- // Runtime test
962
943
  const scope = effectScope();
963
944
  scope.run(() => {
964
945
  const result = useAppState();
965
- expect(result.user).toEqual({ id: 1, name: "John", email: "john@example.com" });
966
- expect(result.settings).toEqual({ theme: "light", notifications: true });
967
- expect(result.loading).toBe(false);
968
- expect(result.error).toBeNull();
946
+
947
+ expectTypeOf(result.user.value.id).toEqualTypeOf<number>();
948
+ expectTypeOf(result.user.value.name).toEqualTypeOf<string>();
949
+ expectTypeOf(result.settings.value.theme).toEqualTypeOf<"light" | "dark">();
950
+ expectTypeOf(result.settings.value.notifications).toEqualTypeOf<boolean>();
951
+ expectTypeOf(result.loading.value).toEqualTypeOf<boolean>();
952
+ expectTypeOf(result.error.value).toEqualTypeOf<string | null>();
953
+
954
+ expect(result.user.value).toEqual({ id: 1, name: "John", email: "john@example.com" });
955
+ expect(result.settings.value).toEqual({ theme: "light", notifications: true });
956
+ expect(result.loading.value).toBe(false);
957
+ expect(result.error.value).toBeNull();
969
958
  });
970
959
  scope.stop();
971
960
  });
972
961
 
973
- test("FragnoVueStore - reactivity with atom updates", async () => {
974
- // Test that stores are reactive and update when atoms change
962
+ test("FragnoVueStore - top-level atom stays reactive", async () => {
963
+ const countAtom = atom(0);
964
+
965
+ const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
966
+ const client = {
967
+ useCounter: cb.createStore(countAtom),
968
+ };
969
+
970
+ const { useCounter } = useFragno(client);
971
+
972
+ const scope = effectScope();
973
+ const observed: number[] = [];
974
+
975
+ scope.run(() => {
976
+ const result = useCounter();
977
+ watch(
978
+ () => result.value,
979
+ (value) => {
980
+ observed.push(value);
981
+ },
982
+ { immediate: true },
983
+ );
984
+ });
985
+
986
+ countAtom.set(5);
987
+ await nextTick();
988
+
989
+ expect(observed).toEqual([0, 5]);
990
+
991
+ scope.stop();
992
+ });
993
+
994
+ test("FragnoVueStore - computed refs stay reactive", async () => {
995
+ const baseNumber = atom(2);
996
+ const doubled = computed(baseNumber, (n) => n * 2);
997
+ const combined = computed([baseNumber, doubled], (base, double) => ({ base, double }));
998
+
999
+ const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
1000
+ const client = {
1001
+ useComputedValues: cb.createStore({
1002
+ base: baseNumber,
1003
+ doubled,
1004
+ combined,
1005
+ }),
1006
+ };
1007
+
1008
+ const { useComputedValues } = useFragno(client);
1009
+
1010
+ const scope = effectScope();
1011
+ const observedDoubled: number[] = [];
1012
+ const observedCombined: Array<{ base: number; double: number }> = [];
1013
+
1014
+ scope.run(() => {
1015
+ const result = useComputedValues();
1016
+ watch(
1017
+ () => result.doubled.value,
1018
+ (value) => {
1019
+ observedDoubled.push(value);
1020
+ },
1021
+ { immediate: true },
1022
+ );
1023
+ watch(
1024
+ () => result.combined.value,
1025
+ (value) => {
1026
+ observedCombined.push(value);
1027
+ },
1028
+ { immediate: true },
1029
+ );
1030
+ });
1031
+
1032
+ baseNumber.set(3);
1033
+ await nextTick();
1034
+
1035
+ expect(observedDoubled).toEqual([4, 6]);
1036
+ expect(observedCombined).toEqual([
1037
+ { base: 2, double: 4 },
1038
+ { base: 3, double: 6 },
1039
+ ]);
1040
+
1041
+ scope.stop();
1042
+ });
1043
+
1044
+ test("FragnoVueStore - unsubscribes atom refs on scope disposal", async () => {
975
1045
  const countAtom = atom(0);
976
1046
 
977
1047
  const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
@@ -984,21 +1054,90 @@ describe("useFragno - createStore", () => {
984
1054
  const { useCounter } = useFragno(client);
985
1055
 
986
1056
  const scope = effectScope();
987
- let result: { count: number } | undefined;
1057
+ const observed: number[] = [];
1058
+
988
1059
  scope.run(() => {
989
- result = useCounter();
990
- expect(result.count).toBe(0);
1060
+ const result = useCounter();
1061
+ watch(
1062
+ () => result.count.value,
1063
+ (value) => {
1064
+ observed.push(value);
1065
+ },
1066
+ { immediate: true },
1067
+ );
991
1068
  });
992
1069
 
993
- // Update the atom
1070
+ expect(observed).toEqual([0]);
1071
+
1072
+ scope.stop();
994
1073
  countAtom.set(5);
1074
+ await nextTick();
1075
+
1076
+ expect(observed).toEqual([0]);
1077
+ });
1078
+
1079
+ test("FragnoVueStore - factory stores dispose on scope disposal", () => {
1080
+ const dispose = vi.fn();
1081
+
1082
+ const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
1083
+ const client = {
1084
+ useSession: cb.createStore((sessionId: string) => ({
1085
+ sessionId: atom(sessionId),
1086
+ sendMessage: (text: string) => text,
1087
+ [Symbol.dispose]: dispose,
1088
+ })),
1089
+ };
1090
+
1091
+ const { useSession } = useFragno(client);
1092
+
1093
+ const scope = effectScope();
1094
+ scope.run(() => {
1095
+ const result = useSession("session-1");
1096
+
1097
+ expectTypeOf(result.sessionId.value).toEqualTypeOf<string>();
1098
+ expect(result.sessionId.value).toBe("session-1");
1099
+ expect(result.sendMessage("hi")).toBe("hi");
1100
+ });
1101
+
1102
+ expect(dispose).not.toHaveBeenCalled();
1103
+
1104
+ scope.stop();
1105
+
1106
+ expect(dispose).toHaveBeenCalledTimes(1);
1107
+ });
1108
+
1109
+ test("FragnoVueStore - reactivity with atom updates", async () => {
1110
+ // Test that returned refs stay reactive when atoms change
1111
+ const countAtom = atom(0);
1112
+
1113
+ const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
1114
+ const client = {
1115
+ useCounter: cb.createStore({
1116
+ count: countAtom,
1117
+ }),
1118
+ };
1119
+
1120
+ const { useCounter } = useFragno(client);
1121
+
1122
+ const scope = effectScope();
1123
+ const observed: number[] = [];
995
1124
 
996
- // Re-run in scope to get updated value
997
1125
  scope.run(() => {
998
- result = useCounter();
999
- expect(result.count).toBe(5);
1126
+ const result = useCounter();
1127
+ watch(
1128
+ () => result.count.value,
1129
+ (value) => {
1130
+ observed.push(value);
1131
+ },
1132
+ { immediate: true },
1133
+ );
1000
1134
  });
1001
1135
 
1136
+ countAtom.set(5);
1137
+ await nextTick();
1138
+
1139
+ expect(observed).toEqual([0, 5]);
1140
+
1002
1141
  scope.stop();
1003
1142
  });
1004
1143
  });