@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.
- package/.turbo/turbo-build.log +72 -62
- package/CHANGELOG.md +28 -0
- package/dist/api/api.d.ts +3 -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 +26 -44
- package/dist/api/fragment-definition-builder.d.ts.map +1 -1
- package/dist/api/fragment-definition-builder.js +15 -22
- package/dist/api/fragment-definition-builder.js.map +1 -1
- package/dist/api/fragment-instantiator.d.ts +51 -37
- package/dist/api/fragment-instantiator.d.ts.map +1 -1
- package/dist/api/fragment-instantiator.js +74 -69
- 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.map +1 -1
- package/dist/api/request-input-context.js.map +1 -1
- package/dist/api/request-middleware.d.ts +1 -1
- 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 +1 -1
- 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 +91 -52
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +25 -9
- 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 +7 -5
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +18 -10
- 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/mod-client.d.ts +5 -4
- package/dist/mod-client.d.ts.map +1 -1
- package/dist/mod-client.js +7 -5
- package/dist/mod-client.js.map +1 -1
- package/dist/mod.d.ts +6 -5
- package/dist/mod.js +2 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- 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 +24 -40
- package/src/api/api.test.ts +3 -1
- package/src/api/api.ts +6 -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 +49 -124
- package/src/api/fragment-instantiator.test.ts +92 -233
- package/src/api/fragment-instantiator.ts +228 -196
- package/src/api/fragment-services.test.ts +1 -0
- package/src/api/internal/path-runtime.test.ts +1 -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 +4 -2
- package/src/api/request-input-context.ts +2 -1
- package/src/api/request-middleware.test.ts +9 -14
- package/src/api/request-middleware.ts +3 -2
- 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 +2 -1
- 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 +49 -10
- package/src/client/client.ts +291 -141
- 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 +223 -84
- package/src/client/vue.ts +57 -30
- package/src/id.ts +1 -0
- package/src/internal/cuid.test.ts +164 -0
- package/src/internal/cuid.ts +133 -0
- package/src/mod-client.ts +4 -2
- package/src/mod.ts +3 -2
- package/src/runtime.ts +1 -1
- package/src/test/test.test.ts +4 -2
- package/src/test/test.ts +7 -9
- 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 +1 -0
- package/vitest.config.ts +2 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { test, expect, describe, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
|
|
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
|
-
|
|
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 {
|
|
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 = [
|
package/src/client/vanilla.ts
CHANGED
|
@@ -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;
|
package/src/client/vue.test.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { test, expect, describe, vi, beforeEach, afterEach, assert, expectTypeOf } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
911
|
+
expectTypeOf(singleResult.value).toEqualTypeOf<string>();
|
|
912
|
+
expect(singleResult.value).toBe("single");
|
|
922
913
|
|
|
923
914
|
const objectResult = useObject();
|
|
924
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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 -
|
|
974
|
-
|
|
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
|
-
|
|
1057
|
+
const observed: number[] = [];
|
|
1058
|
+
|
|
988
1059
|
scope.run(() => {
|
|
989
|
-
result = useCounter();
|
|
990
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|