Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"default": "./dist/cjs/index.js"
},
"./prepare-store": {
"types": "./dist/cjs/prepare-store.d.ts",
"import": "./dist/esm/prepare-store.js",
"require": "./dist/cjs/prepare-store.js",
"default": "./dist/cjs/prepare-store.js"
}
},
"files": [
Expand Down
18 changes: 9 additions & 9 deletions package/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export type GlobalStore<Store extends BaseStore> = {
* @template Store - The store state type
* @returns Optional cleanup function that receives the store
*/
interface LifecycleMountHook<Store extends BaseStore> {
export interface LifecycleMountHook<Store extends BaseStore> {
(
store: Store,
setStore: GlobalStore<Store>["setStore"],
Expand All @@ -107,16 +107,17 @@ interface LifecycleMountHook<Store extends BaseStore> {
* Lifecycle hook that runs during unmount phase
* @template Store - The store state type
*/
interface LifecycleUnmountHook<Store extends BaseStore> {
export interface LifecycleUnmountHook<Store extends BaseStore> {
(store: Store): void;
}

export interface BaseAdapter<Store extends BaseStore> {
beforeInit(store: Store): Store;
afterInit(store: Store, setStore: GlobalStore<Store>["setStore"]): void | ((store: Store) => void);
beforeUpdate(store: Store, part: Partial<Store>): Partial<Store>;
afterUpdate(store: Store, part: Partial<Store>): void;
beforeDestroy(store: Store): void;
getServerSnapshot?: (store: Store) => Store | Promise<Store>;
beforeInit?: (store: Store) => Store;
afterInit?: (store: Store, setStore: GlobalStore<Store>["setStore"]) => void | ((store: Store) => void);
beforeUpdate?: (store: Store, part: Partial<Store>) => Partial<Store>;
afterUpdate?: (store: Store, part: Partial<Store>) => void;
beforeDestroy?: (store: Store) => void;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -139,8 +140,7 @@ export type CreateStoreOptions<Store extends BaseStore> = {
/** Runs asynchronously during component unmount. Use for async cleanup operations */
storeWillUnmountAsync?: LifecycleUnmountHook<Store>;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
adapter?: BaseAdapter<any>;
adapter?: BaseAdapter<Store>;
validate?: Validate;
};

Expand Down
16 changes: 16 additions & 0 deletions package/src/prepare-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type CreateStoreOptions, type BaseStore } from "./core/types";

export const prepareStore = <Store extends BaseStore>(initialData: Store, options?: CreateStoreOptions<Store>) => {
const { adapter, ...restOptions } = options || {};
const { getServerSnapshot, ...restAdapter } = adapter || {};
const getStore = getServerSnapshot ? () => getServerSnapshot(initialData) : () => initialData;

return {
initialData,
options: {
...restOptions,
adapter: restAdapter,
},
getStore,
};
};
49 changes: 48 additions & 1 deletion tests/src/types/type-tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from "react";

import { createStore, useStore, useStoreReducer } from "contection";
import { createStore, useStore, useStoreReducer } from "contection/src";
import { prepareStore } from "contection/src/prepare-store";

interface TestStore {
count: number;
Expand Down Expand Up @@ -399,4 +400,50 @@ function testValidateOption() {
</Store.Provider>;
}

// Test 12: prepareStore with getServerSnapshot (async)
async function testPrepareStoreWithAsyncGetServerSnapshot() {
const { getStore } = prepareStore<TestStore>(
{
count: 0,
name: "Test",
user: { id: 1, email: "[email protected]" },
theme: "light",
items: [],
},
{
adapter: {
getServerSnapshot: async (store) => ({ ...store, count: 1 }),
},
},
);

const storePromise = await getStore();
const storeType: TestStore = storePromise;
}

// Test 13: prepareStore integration with createStore
async function testPrepareStoreIntegrationWithCreateStore() {
const { initialData, options, getStore } = prepareStore<TestStore>(
{
count: 0,
name: "Test",
user: { id: 1, email: "[email protected]" },
theme: "light",
items: [],
},
{
adapter: {
getServerSnapshot: async (store) => ({ ...store, count: 42 }),
beforeInit: (store) => store,
},
},
);

const Store = createStore(initialData, options);
const serverStorePromise = await getStore();

const initial: TestStore = Store._initial;
const storeType: TestStore = serverStorePromise;
}

export {};
204 changes: 204 additions & 0 deletions tests/src/unit/__tests__/prepare-store.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import React from "react";
import { prepareStore } from "contection/src/prepare-store";
import { createStore, useStore } from "contection/src";

import { render, screen } from "../../setup/test-utils";

type TestStore = {
count: number;
name: string;
};

describe("prepareStore", () => {
const initialData: TestStore = {
count: 0,
name: "initial",
};

describe("basic functionality", () => {
it("should return initialData, options, and getStore", () => {
const result = prepareStore(initialData);

expect(result).toHaveProperty("initialData");
expect(result).toHaveProperty("options");
expect(result).toHaveProperty("getStore");
expect(typeof result.getStore).toBe("function");
});

it("should preserve initialData", () => {
const result = prepareStore(initialData);

expect(result.initialData).toEqual(initialData);
});
});

describe("getStore with getServerSnapshot", () => {
it("should call getServerSnapshot with initialData", () => {
const getServerSnapshot = jest.fn((store: TestStore) => ({
...store,
count: 10,
}));

const { getStore } = prepareStore(initialData, {
adapter: {
getServerSnapshot,
},
});

const result = getStore();

expect(getServerSnapshot).toHaveBeenCalledWith(initialData);
expect(result).toEqual({ count: 10, name: "initial" });
});

it("should support async getServerSnapshot", async () => {
const getServerSnapshot = jest.fn(async (store: TestStore) => {
await Promise.resolve();
return {
...store,
count: 20,
};
});

const { getStore } = prepareStore(initialData, {
adapter: {
getServerSnapshot,
},
});

const result = await getStore();

expect(getServerSnapshot).toHaveBeenCalledWith(initialData);
expect(result).toEqual({ count: 20, name: "initial" });
});

it("should allow getServerSnapshot to transform store", async () => {
const getServerSnapshot = jest.fn((store: TestStore) => ({
...store,
name: "transformed",
}));

const { getStore } = prepareStore(initialData, {
adapter: {
getServerSnapshot,
},
});

const result = await getStore();

expect(result.name).toBe("transformed");
expect(result.count).toBe(0);
});
});

describe("options handling", () => {
it("should remove getServerSnapshot from adapter in returned options", () => {
const { options } = prepareStore(initialData, {
adapter: {
getServerSnapshot: () => initialData,
beforeInit: (store) => store,
},
});

expect(options.adapter).not.toHaveProperty("getServerSnapshot");
expect(options.adapter?.beforeInit).toBeDefined();
});

it("should preserve other adapter options", () => {
const beforeInit = jest.fn((store: TestStore) => store);
const afterInit = jest.fn();
const beforeUpdate = jest.fn((store: TestStore, part) => part);
const afterUpdate = jest.fn();
const beforeDestroy = jest.fn();

const { options } = prepareStore(initialData, {
adapter: {
getServerSnapshot: () => initialData,
beforeInit,
afterInit,
beforeUpdate,
afterUpdate,
beforeDestroy,
},
});

expect(options.adapter?.beforeInit).toBe(beforeInit);
expect(options.adapter?.afterInit).toBe(afterInit);
expect(options.adapter?.beforeUpdate).toBe(beforeUpdate);
expect(options.adapter?.afterUpdate).toBe(afterUpdate);
expect(options.adapter?.beforeDestroy).toBe(beforeDestroy);
});

it("should preserve non-adapter options", () => {
const storeWillMount = jest.fn();
const validate = jest.fn(() => true);

const { options } = prepareStore(initialData, {
adapter: {
getServerSnapshot: () => initialData,
},
lifecycleHooks: {
storeWillMount,
},
validate,
});

expect(options.lifecycleHooks?.storeWillMount).toBe(storeWillMount);
expect(options.validate).toBe(validate);
});

it("should return options with empty adapter when no options provided", () => {
const { options } = prepareStore(initialData);

expect(options).toEqual({ adapter: {} });
});

it("should return empty adapter when only getServerSnapshot is provided", () => {
const { options } = prepareStore(initialData, {
adapter: {
getServerSnapshot: () => initialData,
},
});

expect(options.adapter).toEqual({});
});
});

describe("integration with createStore and Provider", () => {
it("should work with createStore, async getServerSnapshot, and Provider", async () => {
const {
getStore,
initialData: initial,
options,
} = prepareStore(initialData, {
adapter: {
getServerSnapshot: async (store) => ({ ...store, count: 42, name: "server" }),
beforeInit: (store) => store,
beforeUpdate: (store, part) => part,
},
});

const Store = createStore(initial, options);

const TestComponent = () => {
const store = useStore(Store, { keys: ["count", "name"] });
return (
<div>
<span data-testid="count">{store.count}</span>
<span data-testid="name">{store.name}</span>
</div>
);
};

const serverStore = await getStore();
render(
<Store.Provider value={serverStore}>
<TestComponent />
</Store.Provider>,
);

expect(screen.getByTestId("count")).toHaveTextContent("42");
expect(screen.getByTestId("name")).toHaveTextContent("server");
});
});
});