@fragno-dev/core 0.1.11 → 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.
- package/.turbo/turbo-build.log +87 -69
- package/CHANGELOG.md +79 -0
- package/dist/api/api.d.ts +21 -2
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +2 -1
- package/dist/api/api.js.map +1 -1
- package/dist/api/bind-services.d.ts +0 -1
- package/dist/api/bind-services.d.ts.map +1 -1
- package/dist/api/bind-services.js.map +1 -1
- package/dist/api/error.d.ts.map +1 -1
- package/dist/api/error.js.map +1 -1
- package/dist/api/fragment-definition-builder.d.ts +32 -40
- package/dist/api/fragment-definition-builder.d.ts.map +1 -1
- package/dist/api/fragment-definition-builder.js +15 -21
- package/dist/api/fragment-definition-builder.js.map +1 -1
- package/dist/api/fragment-instantiator.d.ts +51 -30
- package/dist/api/fragment-instantiator.d.ts.map +1 -1
- package/dist/api/fragment-instantiator.js +201 -52
- package/dist/api/fragment-instantiator.js.map +1 -1
- package/dist/api/request-context-storage.d.ts +4 -0
- package/dist/api/request-context-storage.d.ts.map +1 -1
- package/dist/api/request-context-storage.js +6 -0
- package/dist/api/request-context-storage.js.map +1 -1
- package/dist/api/request-input-context.d.ts +57 -1
- package/dist/api/request-input-context.d.ts.map +1 -1
- package/dist/api/request-input-context.js +67 -0
- package/dist/api/request-input-context.js.map +1 -1
- package/dist/api/request-middleware.d.ts +2 -2
- package/dist/api/request-middleware.d.ts.map +1 -1
- package/dist/api/request-middleware.js.map +1 -1
- package/dist/api/request-output-context.d.ts +1 -1
- package/dist/api/request-output-context.d.ts.map +1 -1
- package/dist/api/request-output-context.js.map +1 -1
- package/dist/api/route-caller.d.ts +30 -0
- package/dist/api/route-caller.d.ts.map +1 -0
- package/dist/api/route-caller.js +63 -0
- package/dist/api/route-caller.js.map +1 -0
- package/dist/api/route-handler-input-options.d.ts.map +1 -1
- package/dist/api/route.d.ts +8 -8
- package/dist/api/route.d.ts.map +1 -1
- package/dist/api/route.js.map +1 -1
- package/dist/api/shared-types.d.ts.map +1 -1
- package/dist/client/client-error.d.ts.map +1 -1
- package/dist/client/client-error.js.map +1 -1
- package/dist/client/client.d.ts +90 -50
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +128 -16
- package/dist/client/client.js.map +1 -1
- package/dist/client/client.svelte.d.ts +6 -5
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +10 -2
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/internal/ndjson-streaming.js.map +1 -1
- package/dist/client/react.d.ts +5 -4
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +104 -12
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +7 -5
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +23 -9
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +16 -4
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +21 -1
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +10 -4
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +24 -1
- package/dist/client/vue.js.map +1 -1
- package/dist/id.d.ts +2 -0
- package/dist/id.js +3 -0
- package/dist/internal/cuid.d.ts +16 -0
- package/dist/internal/cuid.d.ts.map +1 -0
- package/dist/internal/cuid.js +82 -0
- package/dist/internal/cuid.js.map +1 -0
- package/dist/internal/trace-context.d.ts +23 -0
- package/dist/internal/trace-context.d.ts.map +1 -0
- package/dist/internal/trace-context.js +14 -0
- package/dist/internal/trace-context.js.map +1 -0
- package/dist/mod-client.d.ts +7 -20
- package/dist/mod-client.d.ts.map +1 -1
- package/dist/mod-client.js +25 -13
- package/dist/mod-client.js.map +1 -1
- package/dist/mod.d.ts +8 -6
- package/dist/mod.js +3 -1
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +33 -0
- package/dist/runtime.js.map +1 -0
- package/dist/test/test.d.ts +6 -6
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js.map +1 -1
- package/dist/util/ssr.js.map +1 -1
- package/package.json +42 -52
- package/src/api/api.test.ts +3 -1
- package/src/api/api.ts +28 -0
- package/src/api/bind-services.ts +0 -5
- package/src/api/error.ts +1 -0
- package/src/api/fragment-definition-builder.extend.test.ts +2 -1
- package/src/api/fragment-definition-builder.test.ts +2 -1
- package/src/api/fragment-definition-builder.ts +56 -112
- package/src/api/fragment-instantiator.test.ts +311 -166
- package/src/api/fragment-instantiator.ts +470 -131
- package/src/api/fragment-services.test.ts +1 -0
- package/src/api/internal/path-runtime.test.ts +8 -0
- package/src/api/internal/path-type.test.ts +3 -1
- package/src/api/internal/route.test.ts +1 -0
- package/src/api/request-context-storage.ts +7 -0
- package/src/api/request-input-context.test.ts +156 -2
- package/src/api/request-input-context.ts +87 -1
- package/src/api/request-middleware.test.ts +43 -2
- package/src/api/request-middleware.ts +4 -3
- package/src/api/request-output-context.test.ts +3 -1
- package/src/api/request-output-context.ts +2 -1
- package/src/api/route-caller.test.ts +195 -0
- package/src/api/route-caller.ts +167 -0
- package/src/api/route-handler-input-options.ts +2 -1
- package/src/api/route.test.ts +4 -2
- package/src/api/route.ts +9 -3
- package/src/api/shared-types.ts +2 -1
- package/src/client/client-builder.test.ts +4 -2
- package/src/client/client-error.test.ts +2 -1
- package/src/client/client-error.ts +1 -1
- package/src/client/client-types.test.ts +19 -5
- package/src/client/client.ssr.test.ts +6 -4
- package/src/client/client.svelte.test.ts +18 -9
- package/src/client/client.svelte.ts +38 -13
- package/src/client/client.test.ts +244 -10
- package/src/client/client.ts +473 -148
- package/src/client/internal/ndjson-streaming.test.ts +6 -3
- package/src/client/internal/ndjson-streaming.ts +1 -0
- package/src/client/react.test.ts +176 -6
- package/src/client/react.ts +226 -31
- package/src/client/solid.test.ts +29 -5
- package/src/client/solid.ts +60 -22
- package/src/client/vanilla.test.ts +148 -6
- package/src/client/vanilla.ts +63 -9
- package/src/client/vue.test.ts +397 -8
- package/src/client/vue.ts +74 -4
- package/src/id.ts +1 -0
- package/src/internal/cuid.test.ts +164 -0
- package/src/internal/cuid.ts +133 -0
- package/src/internal/trace-context.ts +35 -0
- package/src/mod-client.ts +55 -9
- package/src/mod.ts +9 -3
- package/src/runtime.ts +48 -0
- package/src/test/test.test.ts +4 -2
- package/src/test/test.ts +14 -7
- package/src/util/async.test.ts +1 -0
- package/src/util/content-type.test.ts +1 -0
- package/src/util/nanostores.test.ts +3 -1
- package/src/util/ssr.ts +1 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +2 -0
- package/vitest.config.ts +2 -1
package/src/client/vue.test.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { test, expect, describe, vi, beforeEach, afterEach, assert } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { defineFragment } from "../api/fragment-definition-builder";
|
|
1
|
+
import { test, expect, describe, vi, beforeEach, afterEach, assert, expectTypeOf } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { atom, computed, type ReadableAtom } from "nanostores";
|
|
4
|
+
import { nextTick, ref, watch, effectScope } from "vue";
|
|
6
5
|
import { z } from "zod";
|
|
7
|
-
|
|
6
|
+
|
|
8
7
|
import { waitFor } from "@testing-library/vue";
|
|
9
|
-
|
|
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 {
|
|
14
|
+
import { refToAtom, useFragno } from "./vue";
|
|
12
15
|
|
|
13
16
|
global.fetch = vi.fn();
|
|
14
17
|
|
|
@@ -752,3 +755,389 @@ describe("useFragno", () => {
|
|
|
752
755
|
expect(typeof result.usePostAction).toBe("function");
|
|
753
756
|
});
|
|
754
757
|
});
|
|
758
|
+
|
|
759
|
+
describe("useFragno - createStore", () => {
|
|
760
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
761
|
+
baseUrl: "http://localhost:3000",
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
beforeEach(() => {
|
|
765
|
+
vi.clearAllMocks();
|
|
766
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
afterEach(() => {
|
|
770
|
+
vi.restoreAllMocks();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("FragnoVueStore type test - ReadableAtom fields", () => {
|
|
774
|
+
// Test that ReadableAtom fields are exposed as reactive refs
|
|
775
|
+
const stringAtom: ReadableAtom<string> = atom("hello");
|
|
776
|
+
const numberAtom: ReadableAtom<number> = atom(42);
|
|
777
|
+
const booleanAtom: ReadableAtom<boolean> = atom(true);
|
|
778
|
+
const objectAtom: ReadableAtom<{ count: number }> = atom({ count: 0 });
|
|
779
|
+
const arrayAtom: ReadableAtom<string[]> = atom(["a", "b", "c"]);
|
|
780
|
+
|
|
781
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
782
|
+
const client = {
|
|
783
|
+
useStore: cb.createStore({
|
|
784
|
+
message: stringAtom,
|
|
785
|
+
count: numberAtom,
|
|
786
|
+
isActive: booleanAtom,
|
|
787
|
+
data: objectAtom,
|
|
788
|
+
items: arrayAtom,
|
|
789
|
+
}),
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const { useStore } = useFragno(client);
|
|
793
|
+
|
|
794
|
+
const scope = effectScope();
|
|
795
|
+
scope.run(() => {
|
|
796
|
+
const result = useStore();
|
|
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"]);
|
|
809
|
+
});
|
|
810
|
+
scope.stop();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test("FragnoVueStore type test - computed stores", () => {
|
|
814
|
+
// Test that computed stores (which are also ReadableAtom) are exposed as refs
|
|
815
|
+
const baseNumber = atom(10);
|
|
816
|
+
const doubled = computed(baseNumber, (n) => n * 2);
|
|
817
|
+
const tripled = computed(baseNumber, (n) => n * 3);
|
|
818
|
+
const combined = computed([doubled, tripled], (d, t) => ({ doubled: d, tripled: t }));
|
|
819
|
+
|
|
820
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
821
|
+
const client = {
|
|
822
|
+
useComputedValues: cb.createStore({
|
|
823
|
+
base: baseNumber,
|
|
824
|
+
doubled: doubled,
|
|
825
|
+
tripled: tripled,
|
|
826
|
+
combined: combined,
|
|
827
|
+
}),
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const { useComputedValues } = useFragno(client);
|
|
831
|
+
|
|
832
|
+
const scope = effectScope();
|
|
833
|
+
scope.run(() => {
|
|
834
|
+
const result = useComputedValues();
|
|
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 });
|
|
846
|
+
});
|
|
847
|
+
scope.stop();
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test("FragnoVueStore type test - mixed store and non-store fields", () => {
|
|
851
|
+
// Test that store fields are refs and non-store fields are passed through unchanged
|
|
852
|
+
const messageAtom: ReadableAtom<string> = atom("test");
|
|
853
|
+
const regularFunction = (x: number) => x * 2;
|
|
854
|
+
const regularObject = { foo: "bar", baz: 123 };
|
|
855
|
+
|
|
856
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
857
|
+
const client = {
|
|
858
|
+
useMixed: cb.createStore({
|
|
859
|
+
message: messageAtom,
|
|
860
|
+
multiply: regularFunction,
|
|
861
|
+
config: regularObject,
|
|
862
|
+
constant: 42,
|
|
863
|
+
}),
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
const { useMixed } = useFragno(client);
|
|
867
|
+
|
|
868
|
+
const scope = effectScope();
|
|
869
|
+
scope.run(() => {
|
|
870
|
+
const result = useMixed();
|
|
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);
|
|
881
|
+
expect(result.multiply(5)).toBe(10);
|
|
882
|
+
expect(result.config).toEqual({ foo: "bar", baz: 123 });
|
|
883
|
+
expect(result.constant).toBe(42);
|
|
884
|
+
});
|
|
885
|
+
scope.stop();
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
test("FragnoVueStore type test - single store vs object with stores", () => {
|
|
889
|
+
// Test that a single store is exposed as a ref directly
|
|
890
|
+
const singleAtom: ReadableAtom<string> = atom("single");
|
|
891
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
892
|
+
|
|
893
|
+
// Single store case
|
|
894
|
+
const clientSingle = {
|
|
895
|
+
useSingle: cb.createStore(singleAtom),
|
|
896
|
+
};
|
|
897
|
+
const { useSingle } = useFragno(clientSingle);
|
|
898
|
+
|
|
899
|
+
// Object with stores case
|
|
900
|
+
const clientObject = {
|
|
901
|
+
useObject: cb.createStore({
|
|
902
|
+
value: singleAtom,
|
|
903
|
+
}),
|
|
904
|
+
};
|
|
905
|
+
const { useObject } = useFragno(clientObject);
|
|
906
|
+
|
|
907
|
+
// Runtime and type test
|
|
908
|
+
const scope = effectScope();
|
|
909
|
+
scope.run(() => {
|
|
910
|
+
const singleResult = useSingle();
|
|
911
|
+
expectTypeOf(singleResult.value).toEqualTypeOf<string>();
|
|
912
|
+
expect(singleResult.value).toBe("single");
|
|
913
|
+
|
|
914
|
+
const objectResult = useObject();
|
|
915
|
+
expectTypeOf(objectResult.value.value).toEqualTypeOf<string>();
|
|
916
|
+
expect(objectResult.value.value).toBe("single");
|
|
917
|
+
});
|
|
918
|
+
scope.stop();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test("FragnoVueStore type test - complex nested atoms", () => {
|
|
922
|
+
// Test complex nested structures with atoms
|
|
923
|
+
type User = { id: number; name: string; email: string };
|
|
924
|
+
type Settings = { theme: "light" | "dark"; notifications: boolean };
|
|
925
|
+
|
|
926
|
+
const userAtom: ReadableAtom<User> = atom({ id: 1, name: "John", email: "john@example.com" });
|
|
927
|
+
const settingsAtom: ReadableAtom<Settings> = atom({ theme: "light", notifications: true });
|
|
928
|
+
const loadingAtom: ReadableAtom<boolean> = atom(false);
|
|
929
|
+
const errorAtom: ReadableAtom<string | null> = atom(null);
|
|
930
|
+
|
|
931
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
932
|
+
const client = {
|
|
933
|
+
useAppState: cb.createStore({
|
|
934
|
+
user: userAtom,
|
|
935
|
+
settings: settingsAtom,
|
|
936
|
+
loading: loadingAtom,
|
|
937
|
+
error: errorAtom,
|
|
938
|
+
}),
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
const { useAppState } = useFragno(client);
|
|
942
|
+
|
|
943
|
+
const scope = effectScope();
|
|
944
|
+
scope.run(() => {
|
|
945
|
+
const result = useAppState();
|
|
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();
|
|
958
|
+
});
|
|
959
|
+
scope.stop();
|
|
960
|
+
});
|
|
961
|
+
|
|
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 () => {
|
|
1045
|
+
const countAtom = atom(0);
|
|
1046
|
+
|
|
1047
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
1048
|
+
const client = {
|
|
1049
|
+
useCounter: cb.createStore({
|
|
1050
|
+
count: countAtom,
|
|
1051
|
+
}),
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const { useCounter } = useFragno(client);
|
|
1055
|
+
|
|
1056
|
+
const scope = effectScope();
|
|
1057
|
+
const observed: number[] = [];
|
|
1058
|
+
|
|
1059
|
+
scope.run(() => {
|
|
1060
|
+
const result = useCounter();
|
|
1061
|
+
watch(
|
|
1062
|
+
() => result.count.value,
|
|
1063
|
+
(value) => {
|
|
1064
|
+
observed.push(value);
|
|
1065
|
+
},
|
|
1066
|
+
{ immediate: true },
|
|
1067
|
+
);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
expect(observed).toEqual([0]);
|
|
1071
|
+
|
|
1072
|
+
scope.stop();
|
|
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[] = [];
|
|
1124
|
+
|
|
1125
|
+
scope.run(() => {
|
|
1126
|
+
const result = useCounter();
|
|
1127
|
+
watch(
|
|
1128
|
+
() => result.count.value,
|
|
1129
|
+
(value) => {
|
|
1130
|
+
observed.push(value);
|
|
1131
|
+
},
|
|
1132
|
+
{ immediate: true },
|
|
1133
|
+
);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
countAtom.set(5);
|
|
1137
|
+
await nextTick();
|
|
1138
|
+
|
|
1139
|
+
expect(observed).toEqual([0, 5]);
|
|
1140
|
+
|
|
1141
|
+
scope.stop();
|
|
1142
|
+
});
|
|
1143
|
+
});
|
package/src/client/vue.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
1
|
import { atom, type ReadableAtom, type Store, type StoreValue } from "nanostores";
|
|
3
2
|
import type { DeepReadonly, Ref, ShallowRef, UnwrapNestedRefs } from "vue";
|
|
4
3
|
import { computed, getCurrentScope, isRef, onScopeDispose, ref, shallowRef, watch } from "vue";
|
|
4
|
+
|
|
5
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
6
|
+
|
|
5
7
|
import type { NonGetHTTPMethod } from "../api/api";
|
|
8
|
+
import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
|
|
9
|
+
import { isReadableAtom } from "../util/nanostores";
|
|
10
|
+
import type { InferOr } from "../util/types-util";
|
|
6
11
|
import {
|
|
7
12
|
isGetHook,
|
|
8
13
|
isMutatorHook,
|
|
14
|
+
isStore,
|
|
9
15
|
type FragnoClientMutatorData,
|
|
10
16
|
type FragnoClientHookData,
|
|
17
|
+
type FragnoStoreData,
|
|
18
|
+
type FragnoStoreFactoryData,
|
|
19
|
+
type FragnoStoreObjectData,
|
|
11
20
|
} from "./client";
|
|
12
21
|
import type { FragnoClientError } from "./client-error";
|
|
13
|
-
import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
|
|
14
|
-
import type { InferOr } from "../util/types-util";
|
|
15
22
|
|
|
16
23
|
export type FragnoVueHook<
|
|
17
24
|
_TMethod extends "GET",
|
|
@@ -46,6 +53,21 @@ export type FragnoVueMutator<
|
|
|
46
53
|
data: Ref<InferOr<TOutputSchema, undefined>>;
|
|
47
54
|
};
|
|
48
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Type helper that wraps any Store fields of the object into reactive Vue refs.
|
|
58
|
+
*/
|
|
59
|
+
type FragnoVueStoreRef<T extends Store> = DeepReadonly<UnwrapNestedRefs<ShallowRef<StoreValue<T>>>>;
|
|
60
|
+
|
|
61
|
+
type FragnoVueStoreValue<T extends object> = T extends Store
|
|
62
|
+
? FragnoVueStoreRef<T>
|
|
63
|
+
: {
|
|
64
|
+
[K in keyof T]: T[K] extends Store ? FragnoVueStoreRef<T[K]> : T[K];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type FragnoVueStore<T extends object, TArgs extends unknown[] = []> = (
|
|
68
|
+
...args: TArgs
|
|
69
|
+
) => FragnoVueStoreValue<T>;
|
|
70
|
+
|
|
49
71
|
/**
|
|
50
72
|
* Converts a Vue Ref to a NanoStore Atom.
|
|
51
73
|
*
|
|
@@ -181,6 +203,48 @@ function createVueMutator<
|
|
|
181
203
|
};
|
|
182
204
|
}
|
|
183
205
|
|
|
206
|
+
function unwrapVueStoreValue<T extends object>(value: T): FragnoVueStoreValue<T> {
|
|
207
|
+
if (isReadableAtom(value)) {
|
|
208
|
+
return useStore(value as Store) as FragnoVueStoreValue<T>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
+
const result: any = {};
|
|
213
|
+
|
|
214
|
+
for (const key in value) {
|
|
215
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const fieldValue = value[key];
|
|
220
|
+
if (isReadableAtom(fieldValue)) {
|
|
221
|
+
result[key] = useStore(fieldValue);
|
|
222
|
+
} else {
|
|
223
|
+
result[key] = fieldValue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result as FragnoVueStoreValue<T>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Helper function to create a Vue composable from a store
|
|
231
|
+
function createVueStore<const T extends object, const TArgs extends unknown[]>(
|
|
232
|
+
hook: FragnoStoreData<T, TArgs>,
|
|
233
|
+
): FragnoVueStore<T, TArgs> {
|
|
234
|
+
return ((...args: TArgs) => {
|
|
235
|
+
const value = "factory" in hook ? hook.factory(...args) : hook.obj;
|
|
236
|
+
const disposer = value[Symbol.dispose as keyof typeof value];
|
|
237
|
+
|
|
238
|
+
if (typeof disposer === "function" && getCurrentScope()) {
|
|
239
|
+
onScopeDispose(() => {
|
|
240
|
+
disposer.call(value);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return unwrapVueStoreValue(value);
|
|
245
|
+
}) as FragnoVueStore<T, TArgs>;
|
|
246
|
+
}
|
|
247
|
+
|
|
184
248
|
export function useFragno<T extends Record<string, unknown>>(
|
|
185
249
|
clientObj: T,
|
|
186
250
|
): {
|
|
@@ -201,7 +265,11 @@ export function useFragno<T extends Record<string, unknown>>(
|
|
|
201
265
|
infer TQueryParameters
|
|
202
266
|
>
|
|
203
267
|
? FragnoVueMutator<M, TPath, TInputSchema, TOutputSchema, TErrorCode, TQueryParameters>
|
|
204
|
-
: T[K]
|
|
268
|
+
: T[K] extends FragnoStoreObjectData<infer TStoreObj>
|
|
269
|
+
? FragnoVueStore<TStoreObj, []>
|
|
270
|
+
: T[K] extends FragnoStoreFactoryData<infer TStoreObj, infer TStoreArgs>
|
|
271
|
+
? FragnoVueStore<TStoreObj, TStoreArgs>
|
|
272
|
+
: T[K];
|
|
205
273
|
} {
|
|
206
274
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
207
275
|
const result = {} as any;
|
|
@@ -216,6 +284,8 @@ export function useFragno<T extends Record<string, unknown>>(
|
|
|
216
284
|
result[key] = createVueHook(hook);
|
|
217
285
|
} else if (isMutatorHook(hook)) {
|
|
218
286
|
result[key] = createVueMutator(hook);
|
|
287
|
+
} else if (isStore(hook)) {
|
|
288
|
+
result[key] = createVueStore(hook);
|
|
219
289
|
} else {
|
|
220
290
|
// Pass through non-hook values unchanged
|
|
221
291
|
result[key] = hook;
|
package/src/id.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createId, init } from "./internal/cuid";
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createId, init } from "./cuid";
|
|
4
|
+
|
|
5
|
+
const idPattern = /^[a-z][0-9a-z]*$/;
|
|
6
|
+
|
|
7
|
+
const createSeededRandom = (seed: number) => {
|
|
8
|
+
let state = seed >>> 0;
|
|
9
|
+
|
|
10
|
+
return () => {
|
|
11
|
+
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
|
12
|
+
return state / 4294967296;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const createCounter = (start = 0) => {
|
|
17
|
+
let value = start;
|
|
18
|
+
return () => value++;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("cuid", () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("creates ids with the default format", () => {
|
|
28
|
+
const id = createId();
|
|
29
|
+
|
|
30
|
+
expect(id).toHaveLength(24);
|
|
31
|
+
expect(id).toMatch(idPattern);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("defers default state initialization until the first generated id", () => {
|
|
35
|
+
vi.useFakeTimers();
|
|
36
|
+
vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
|
|
37
|
+
|
|
38
|
+
const random = vi.fn(() => 0.5);
|
|
39
|
+
|
|
40
|
+
const generator = init({ random });
|
|
41
|
+
|
|
42
|
+
expect(random).not.toHaveBeenCalled();
|
|
43
|
+
|
|
44
|
+
generator();
|
|
45
|
+
|
|
46
|
+
expect(random).toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("uses web-standard crypto randomness by default", () => {
|
|
50
|
+
vi.useFakeTimers();
|
|
51
|
+
vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
|
|
52
|
+
|
|
53
|
+
const mathRandomSpy = vi.spyOn(Math, "random");
|
|
54
|
+
const getRandomValuesSpy = vi.spyOn(globalThis.crypto, "getRandomValues");
|
|
55
|
+
|
|
56
|
+
const generator = init();
|
|
57
|
+
const ids = Array.from({ length: 10 }, () => generator());
|
|
58
|
+
|
|
59
|
+
expect(ids[0]).toHaveLength(24);
|
|
60
|
+
expect(ids[0]).toMatch(idPattern);
|
|
61
|
+
expect(mathRandomSpy).not.toHaveBeenCalled();
|
|
62
|
+
expect(getRandomValuesSpy).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("supports exact custom lengths including very short ids", () => {
|
|
66
|
+
vi.useFakeTimers();
|
|
67
|
+
vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
|
|
68
|
+
|
|
69
|
+
for (const length of [1, 2, 8, 24, 32]) {
|
|
70
|
+
const id = init({
|
|
71
|
+
length,
|
|
72
|
+
random: createSeededRandom(123),
|
|
73
|
+
counter: createCounter(7),
|
|
74
|
+
fingerprint: "fingerprint",
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
expect(id).toHaveLength(length);
|
|
78
|
+
expect(id).toMatch(idPattern);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("creates deterministic sequences for the same configuration", () => {
|
|
83
|
+
vi.useFakeTimers();
|
|
84
|
+
vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
|
|
85
|
+
|
|
86
|
+
const first = init({
|
|
87
|
+
random: createSeededRandom(123),
|
|
88
|
+
counter: createCounter(10),
|
|
89
|
+
fingerprint: "fingerprint",
|
|
90
|
+
});
|
|
91
|
+
const second = init({
|
|
92
|
+
random: createSeededRandom(123),
|
|
93
|
+
counter: createCounter(10),
|
|
94
|
+
fingerprint: "fingerprint",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const firstIds = [first(), first(), first()];
|
|
98
|
+
const secondIds = [second(), second(), second()];
|
|
99
|
+
|
|
100
|
+
expect(firstIds).toEqual(secondIds);
|
|
101
|
+
expect(firstIds).toMatchInlineSnapshot(`
|
|
102
|
+
[
|
|
103
|
+
"h1bh5y4k2htp1bh5y4k2htp1",
|
|
104
|
+
"p1zxk77n8dqx1zxk77n8dqx1",
|
|
105
|
+
"u576un0v3is576un0v3is576",
|
|
106
|
+
]
|
|
107
|
+
`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("changes the output when the fingerprint changes", () => {
|
|
111
|
+
vi.useFakeTimers();
|
|
112
|
+
vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
|
|
113
|
+
|
|
114
|
+
const withFirstFingerprint = init({
|
|
115
|
+
random: createSeededRandom(123),
|
|
116
|
+
counter: createCounter(10),
|
|
117
|
+
fingerprint: "fingerprint-a",
|
|
118
|
+
});
|
|
119
|
+
const withSecondFingerprint = init({
|
|
120
|
+
random: createSeededRandom(123),
|
|
121
|
+
counter: createCounter(10),
|
|
122
|
+
fingerprint: "fingerprint-b",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(withFirstFingerprint()).not.toBe(withSecondFingerprint());
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("remains collision-free across many ids even when time and random are fixed", () => {
|
|
129
|
+
vi.useFakeTimers();
|
|
130
|
+
vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
|
|
131
|
+
|
|
132
|
+
const generator = init({
|
|
133
|
+
random: () => 0,
|
|
134
|
+
counter: createCounter(0),
|
|
135
|
+
fingerprint: "fingerprint",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const ids = Array.from({ length: 1000 }, () => generator());
|
|
139
|
+
|
|
140
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
141
|
+
expect(ids.every((id) => id.length === 24 && idPattern.test(id))).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("uses the configured random source for the leading character", () => {
|
|
145
|
+
vi.useFakeTimers();
|
|
146
|
+
vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
|
|
147
|
+
|
|
148
|
+
const leadingA = init({
|
|
149
|
+
length: 8,
|
|
150
|
+
random: () => 0,
|
|
151
|
+
counter: createCounter(0),
|
|
152
|
+
fingerprint: "fingerprint",
|
|
153
|
+
})();
|
|
154
|
+
const leadingZ = init({
|
|
155
|
+
length: 8,
|
|
156
|
+
random: () => 0.999999,
|
|
157
|
+
counter: createCounter(0),
|
|
158
|
+
fingerprint: "fingerprint",
|
|
159
|
+
})();
|
|
160
|
+
|
|
161
|
+
expect(leadingA[0]).toBe("a");
|
|
162
|
+
expect(leadingZ[0]).toBe("z");
|
|
163
|
+
});
|
|
164
|
+
});
|