@quark-fw/store 0.1.6 → 0.1.8

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.
@@ -0,0 +1,20 @@
1
+ import type { CreateTRPCClient } from "@trpc/client";
2
+ import { createTinyRPCClient, type StoreListenersDict, type StorePromisesDict, type StoreReactPromisesDict, type StoreSubscribersDict, type StoreValuesDict } from "./createTinyRPCClient.js";
3
+ import type { AppRouter } from "./types.js";
4
+ export declare class Store<TRouter extends AppRouter = AppRouter, TClient extends CreateTRPCClient<TRouter> = CreateTRPCClient<TRouter>> {
5
+ values: StoreValuesDict;
6
+ trpcClient: TClient;
7
+ tiny: ReturnType<typeof createTinyRPCClient<TRouter, TClient>>;
8
+ promises: StorePromisesDict;
9
+ reactPromises: StoreReactPromisesDict;
10
+ listeners: StoreListenersDict;
11
+ subscribers: StoreSubscribersDict;
12
+ callback?: (key: string, data: unknown) => void;
13
+ constructor({ trpcClient, values, promises, callback, }: {
14
+ trpcClient: TClient;
15
+ values?: StoreValuesDict;
16
+ promises?: StorePromisesDict;
17
+ callback?: (key: string, data: unknown) => void;
18
+ });
19
+ emit(key: string, data: unknown): void;
20
+ }
package/dist/Store.js ADDED
@@ -0,0 +1,34 @@
1
+ import { createTinyRPCClient, } from "./createTinyRPCClient.js";
2
+ export class Store {
3
+ values;
4
+ trpcClient;
5
+ tiny;
6
+ promises;
7
+ reactPromises;
8
+ listeners = {};
9
+ subscribers = {};
10
+ callback;
11
+ constructor({ trpcClient, values, promises, callback, }) {
12
+ this.trpcClient = trpcClient;
13
+ this.values = values ?? {};
14
+ this.promises = promises ?? {};
15
+ this.reactPromises = {};
16
+ this.callback = callback;
17
+ this.tiny = createTinyRPCClient({
18
+ trpcClient,
19
+ values: this.values,
20
+ promises: this.promises,
21
+ reactPromises: this.reactPromises,
22
+ callback,
23
+ listeners: this.listeners,
24
+ subscribers: this.subscribers,
25
+ });
26
+ }
27
+ emit(key, data) {
28
+ const callbacks = this.listeners[key];
29
+ // слушатели одноразовые (резолверы подвисших Suspense-чтений):
30
+ // снимаем ДО вызова, чтобы не потерять переподписку из колбэка
31
+ delete this.listeners[key];
32
+ callbacks?.forEach((cb) => cb(data));
33
+ }
34
+ }
@@ -0,0 +1,8 @@
1
+ import { Context } from "react";
2
+ import { Store } from "./Store.js";
3
+ import type { TRPCClient } from "./trpcClient.js";
4
+ export declare const StoreContext: Context<Store<import("./types.js").AppRouter, import("@trpc/client").CreateTRPCClient<import("./types.js").AppRouter>>>;
5
+ export declare const useStore: () => Store;
6
+ export type TStore = ReturnType<typeof useStore>;
7
+ export declare const TRPCContext: Context<TRPCClient>;
8
+ export declare const useTrpc: () => TRPCClient;
@@ -0,0 +1,7 @@
1
+ import { createContext, useContext } from "react";
2
+ export const StoreContext = createContext({});
3
+ export const useStore = () => useContext(StoreContext);
4
+ // аннотации явные: без них d.ts разворачивает тип клиента структурно и
5
+ // спотыкается о непубличный символ tRPC (untypedClientSymbol)
6
+ export const TRPCContext = createContext({});
7
+ export const useTrpc = () => useContext(TRPCContext);
@@ -0,0 +1,47 @@
1
+ import { createTRPCClient } from "@trpc/client";
2
+ import type { AnyProcedure, inferProcedureInput, inferProcedureOutput, TRPCRouterRecord } from "@trpc/server";
3
+ import { Dispatch, SetStateAction } from "react";
4
+ import type { Serialize } from "./serialize.js";
5
+ import type { AppRouter } from "./types.js";
6
+ type ProcedureArgs<TProcedure extends AnyProcedure> = [
7
+ input?: inferProcedureInput<TProcedure>
8
+ ];
9
+ type ProcedureOutput<TProcedure extends AnyProcedure> = Serialize<inferProcedureOutput<TProcedure>>;
10
+ export type StoreValuesDict = Record<string, unknown>;
11
+ export type StorePromisesDict = Record<string, Promise<unknown>>;
12
+ export type StoreReactPromisesDict = Record<string, () => unknown>;
13
+ /** подписчики useSyncExternalStore на изменения значения по ключу кэша */
14
+ export type StoreSubscribersDict = Record<string, Set<() => void>>;
15
+ /** подписчики на готовность значения по ключу кэша */
16
+ export type StoreListenersDict = Record<string, ((data: unknown) => void)[]>;
17
+ type DecorateProcedure<TProcedure extends AnyProcedure> = {
18
+ get(...args: ProcedureArgs<TProcedure>): ProcedureOutput<TProcedure>;
19
+ mutate(...args: ProcedureArgs<TProcedure>): ProcedureOutput<TProcedure>;
20
+ mutatePromise(...args: ProcedureArgs<TProcedure>): Promise<ProcedureOutput<TProcedure>>;
21
+ getPromise(...args: ProcedureArgs<TProcedure>): Promise<ProcedureOutput<TProcedure>>;
22
+ reFetch(...args: ProcedureArgs<TProcedure>): ProcedureOutput<TProcedure>;
23
+ clearCache(...args: ProcedureArgs<TProcedure>): void;
24
+ setCache(...args: ProcedureArgs<TProcedure>): (data: ProcedureOutput<TProcedure>) => void;
25
+ getState(...args: ProcedureArgs<TProcedure>): [
26
+ ProcedureOutput<TProcedure>,
27
+ Dispatch<SetStateAction<ProcedureOutput<TProcedure>>>
28
+ ];
29
+ };
30
+ type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = {
31
+ [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends AnyProcedure ? DecorateProcedure<$Value> : $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : never : never;
32
+ };
33
+ type DecoratedProcedureSSGRecord<TRouter extends AppRouter> = TRouter extends {
34
+ _def: {
35
+ record: infer TRecord extends TRPCRouterRecord;
36
+ };
37
+ } ? DecorateRouterRecord<TRecord> : {};
38
+ export declare const createTinyRPCClient: <TRouter extends AppRouter, TClient extends ReturnType<typeof createTRPCClient<TRouter>>>({ callback, trpcClient, values, promises, reactPromises, listeners, subscribers, }: {
39
+ callback?: (key: string, value: unknown) => void;
40
+ trpcClient: TClient;
41
+ values: StoreValuesDict;
42
+ promises: StorePromisesDict;
43
+ reactPromises: StoreReactPromisesDict;
44
+ listeners: StoreListenersDict;
45
+ subscribers: StoreSubscribersDict;
46
+ }) => DecoratedProcedureSSGRecord<TRouter>;
47
+ export {};
@@ -0,0 +1,161 @@
1
+ // Порт @ugliest/store под quark: изоморфный tiny-кэш поверх tRPC
2
+ // с потоковой SSR-гидратацией через window.cache.
3
+ import { createFlatProxy } from "@trpc/server/shared";
4
+ // createRecursiveProxy убран из shared в tRPC 11.18 — только unstable core
5
+ import { createRecursiveProxy } from "@trpc/server/unstable-core-do-not-import";
6
+ import { getUntypedClient } from "@trpc/client";
7
+ import { useSyncExternalStore } from "react";
8
+ import { ERROR_STATE, NOT_READY_STATE } from "./sentinels.js";
9
+ import { stableStringify } from "./stableStringify.js";
10
+ import { usePromise } from "./usePromise.js";
11
+ export const createTinyRPCClient = ({ callback, trpcClient, values, promises, reactPromises, listeners, subscribers, }) => {
12
+ const untypedClient = getUntypedClient(trpcClient);
13
+ /** будит все компоненты, подписанные на ключ через getState */
14
+ const notify = (key) => subscribers[key]?.forEach((cb) => cb());
15
+ return createFlatProxy((key) => createRecursiveProxy(({ path, args, }) => {
16
+ const pathCopy = [key, ...path];
17
+ const utilName = pathCopy.pop();
18
+ const fullPath = pathCopy.join(".");
19
+ // хвостовые undefined отбрасываются: get() и get(undefined) —
20
+ // один и тот же вызов и обязаны делить ключ кэша
21
+ const keyArgs = [...args];
22
+ while (keyArgs.length &&
23
+ keyArgs[keyArgs.length - 1] === undefined) {
24
+ keyArgs.pop();
25
+ }
26
+ const fullKey = `${fullPath}:${stableStringify(keyArgs)}`;
27
+ const getPromise = (type = "query") => {
28
+ if (!promises[fullKey]) {
29
+ const promise = untypedClient[type](fullPath, ...args);
30
+ promises[fullKey] = promise;
31
+ callback?.(fullKey, NOT_READY_STATE);
32
+ promise
33
+ .then((value) => {
34
+ // values — источник снапшотов для подписок
35
+ // getState; без этой записи резолв жил бы
36
+ // только в замыкании reactPromises
37
+ values[fullKey] = value;
38
+ notify(fullKey);
39
+ callback?.(fullKey, value);
40
+ })
41
+ .catch((e) => {
42
+ console.error(e);
43
+ // иначе SSR-стрим не закрыл бы ключ и клиент
44
+ // висел бы в NOT_READY_STATE навсегда
45
+ callback?.(fullKey, ERROR_STATE);
46
+ });
47
+ }
48
+ return promises[fullKey];
49
+ };
50
+ /*
51
+ * usePromise — это хук, и вызывается он отсюда, из прокси, а не
52
+ * из компонента. Так и задумано: чтение значения происходит в
53
+ * теле рендера потребителя, поэтому порядок вызовов совпадает с
54
+ * порядком чтений. Отвечает за это вызывающий: читать значения
55
+ * нужно безусловно, как обычные хуки.
56
+ */
57
+ /* eslint-disable react-hooks/rules-of-hooks */
58
+ const get = (type = "query") => {
59
+ if (values[fullKey] === NOT_READY_STATE) {
60
+ if (!reactPromises[fullKey]) {
61
+ reactPromises[fullKey] = usePromise(new Promise((resolve) => {
62
+ if (!listeners[fullKey])
63
+ listeners[fullKey] = [];
64
+ listeners[fullKey].push((data) => {
65
+ if (data === ERROR_STATE) {
66
+ // серверный запрос упал: забываем
67
+ // ключ, ниже клиент перезапросит
68
+ delete values[fullKey];
69
+ delete promises[fullKey];
70
+ delete reactPromises[fullKey];
71
+ }
72
+ else {
73
+ values[fullKey] = data;
74
+ }
75
+ notify(fullKey);
76
+ resolve(data);
77
+ });
78
+ }));
79
+ }
80
+ return reactPromises[fullKey]();
81
+ }
82
+ if (values[fullKey] === ERROR_STATE) {
83
+ // ключ с упавшим серверным запросом (из window.cache):
84
+ // забываем и перезапрашиваем с клиента
85
+ delete values[fullKey];
86
+ delete promises[fullKey];
87
+ delete reactPromises[fullKey];
88
+ }
89
+ // `in`, а не truthiness: null/0/""/false — легитимные
90
+ // закэшированные ответы, их нельзя перезапрашивать
91
+ if (fullKey in values)
92
+ return values[fullKey];
93
+ if (!reactPromises[fullKey]) {
94
+ reactPromises[fullKey] = usePromise(getPromise(type));
95
+ }
96
+ return reactPromises[fullKey]?.();
97
+ };
98
+ /* eslint-enable react-hooks/rules-of-hooks */
99
+ switch (utilName) {
100
+ case "get": {
101
+ return get();
102
+ }
103
+ case "mutate": {
104
+ return get("mutation");
105
+ }
106
+ case "getPromise": {
107
+ return getPromise();
108
+ }
109
+ case "mutatePromise": {
110
+ return getPromise("mutation");
111
+ }
112
+ case "clearCache": {
113
+ // delete по отсутствующему ключу — no-op, проверка
114
+ // была лишней
115
+ delete values[fullKey];
116
+ delete promises[fullKey];
117
+ delete reactPromises[fullKey];
118
+ break;
119
+ }
120
+ case "reFetch": {
121
+ delete values[fullKey];
122
+ delete promises[fullKey];
123
+ delete reactPromises[fullKey];
124
+ return get();
125
+ }
126
+ case "setCache": {
127
+ return (value) => {
128
+ values[fullKey] = value;
129
+ // в каждом словаре лежит то, что он объявляет:
130
+ // раньше сюда клали сырое значение, и повторный
131
+ // getPromise вернул бы не промис
132
+ promises[fullKey] = Promise.resolve(value);
133
+ reactPromises[fullKey] = () => value;
134
+ notify(fullKey);
135
+ };
136
+ }
137
+ case "getState": {
138
+ // хук зовётся безусловно на каждый рендер; прежний
139
+ // useState кэшировал кортеж первого рендера в словаре,
140
+ // из-за чего значение замораживалось, а сеттер был
141
+ // привязан к одному-единственному компоненту
142
+ const initial = get();
143
+ const getSnapshot = () => fullKey in values ? values[fullKey] : initial;
144
+ const value = useSyncExternalStore((onChange) => {
145
+ (subscribers[fullKey] ??= new Set()).add(onChange);
146
+ return () => subscribers[fullKey]?.delete(onChange);
147
+ }, getSnapshot, getSnapshot);
148
+ const setValue = (next) => {
149
+ const resolved = typeof next === "function"
150
+ ? next(values[fullKey])
151
+ : next;
152
+ values[fullKey] = resolved;
153
+ promises[fullKey] = Promise.resolve(resolved);
154
+ reactPromises[fullKey] = () => resolved;
155
+ notify(fullKey);
156
+ };
157
+ return [value, setValue];
158
+ }
159
+ }
160
+ }));
161
+ };
package/dist/index.d.ts CHANGED
@@ -1,76 +1,7 @@
1
- import { CreateTRPCClient, createTRPCClient } from "@trpc/client";
2
- import type { AnyProcedure, inferProcedureInput, inferProcedureOutput, TRPCRouterRecord } from "@trpc/server";
3
- import { Dispatch, SetStateAction } from "react";
4
- import type { Serialize } from "./serialize.js";
5
- import type { AppRouter } from "./types.js";
6
- type ProcedureArgs<TProcedure extends AnyProcedure> = [
7
- input?: inferProcedureInput<TProcedure>
8
- ];
9
- type ProcedureOutput<TProcedure extends AnyProcedure> = Serialize<inferProcedureOutput<TProcedure>>;
10
- export type StoreValuesDict = Record<string, unknown>;
11
- export type StorePromisesDict = Record<string, Promise<unknown>>;
12
- type StoreReactPromisesDict = Record<string, () => unknown>;
13
- type StoreStatesDict = Record<string, [
14
- unknown,
15
- Dispatch<SetStateAction<unknown>>
16
- ]>;
17
- /** подписчики на готовность значения по ключу кэша */
18
- type StoreListenersDict = Record<string, ((data: unknown) => void)[]>;
19
- type DecorateProcedure<TProcedure extends AnyProcedure> = {
20
- get(...args: ProcedureArgs<TProcedure>): ProcedureOutput<TProcedure>;
21
- mutate(...args: ProcedureArgs<TProcedure>): ProcedureOutput<TProcedure>;
22
- mutatePromise(...args: ProcedureArgs<TProcedure>): Promise<ProcedureOutput<TProcedure>>;
23
- getPromise(...args: ProcedureArgs<TProcedure>): Promise<ProcedureOutput<TProcedure>>;
24
- reFetch(...args: ProcedureArgs<TProcedure>): Serialize<Promise<inferProcedureOutput<TProcedure>>>;
25
- clearCache(...args: ProcedureArgs<TProcedure>): void;
26
- setCache(...args: ProcedureArgs<TProcedure>): (data: ProcedureOutput<TProcedure>) => void;
27
- getState(...args: ProcedureArgs<TProcedure>): [
28
- ProcedureOutput<TProcedure>,
29
- Dispatch<SetStateAction<ProcedureOutput<TProcedure>>>
30
- ];
31
- };
32
- type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = {
33
- [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends AnyProcedure ? DecorateProcedure<$Value> : $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : never : never;
34
- };
35
- type DecoratedProcedureSSGRecord<TRouter extends AppRouter> = TRouter extends {
36
- _def: {
37
- record: infer TRecord extends TRPCRouterRecord;
38
- };
39
- } ? DecorateRouterRecord<TRecord> : {};
40
- export declare const createTinyRPCClient: <TRouter extends AppRouter, TClient extends ReturnType<typeof createTRPCClient<TRouter>>>({ callback, trpcClient, values, promises, reactPromises, listeners, states, }: {
41
- callback?: (key: string, value: unknown) => void;
42
- trpcClient: TClient;
43
- values: StoreValuesDict;
44
- promises: StorePromisesDict;
45
- reactPromises: StoreReactPromisesDict;
46
- listeners: StoreListenersDict;
47
- states: StoreStatesDict;
48
- }) => DecoratedProcedureSSGRecord<TRouter>;
49
- declare class Store<TRouter extends AppRouter = AppRouter, TClient extends CreateTRPCClient<TRouter> = CreateTRPCClient<TRouter>> {
50
- values: StoreValuesDict;
51
- trpcClient: TClient;
52
- tiny: ReturnType<typeof createTinyRPCClient<TRouter, TClient>>;
53
- promises: StorePromisesDict;
54
- reactPromises: StoreReactPromisesDict;
55
- listeners: StoreListenersDict;
56
- states: StoreStatesDict;
57
- callback?: (key: string, data: unknown) => void;
58
- constructor({ trpcClient, values, promises, callback, }: {
59
- trpcClient: TClient;
60
- values?: StoreValuesDict;
61
- promises?: StorePromisesDict;
62
- callback?: (key: string, data: unknown) => void;
63
- });
64
- emit(key: string, data: unknown): void;
65
- }
66
- declare const StoreContext: import("react").Context<Store<AppRouter, CreateTRPCClient<AppRouter>>>;
67
- declare const useStore: () => Store;
68
- export type TStore = ReturnType<typeof useStore>;
69
- declare const useTrpc: () => AppTRPCClient;
70
- export declare const jsonSafeFetch: (url: RequestInfo | URL, options?: RequestInit) => Promise<Response>;
71
- type AppTRPCClient = CreateTRPCClient<AppRouter>;
72
- declare const trpcClient: AppTRPCClient;
73
- export type TRPCClient = AppTRPCClient;
74
- declare const TRPCContext: import("react").Context<AppTRPCClient>;
75
- export { useStore, StoreContext, useTrpc, TRPCContext, trpcClient };
1
+ import { Store } from "./Store.js";
2
+ export { createTinyRPCClient, type StorePromisesDict, type StoreValuesDict, } from "./createTinyRPCClient.js";
3
+ export { jsonSafeFetch } from "./jsonSafeFetch.js";
4
+ export { ERROR_STATE, NOT_READY_STATE } from "./sentinels.js";
5
+ export { trpcClient, type TRPCClient } from "./trpcClient.js";
6
+ export { StoreContext, TRPCContext, useStore, useTrpc, type TStore, } from "./context.js";
76
7
  export default Store;
package/dist/index.js CHANGED
@@ -1,173 +1,9 @@
1
- // Порт @ugliest/store под quark: изоморфный tiny-кэш поверх tRPC
2
- // с потоковой SSR-гидратацией через window.cache.
3
- import { createFlatProxy } from "@trpc/server/shared";
4
- // createRecursiveProxy убран из shared в tRPC 11.18 — только unstable core
5
- import { createRecursiveProxy } from "@trpc/server/unstable-core-do-not-import";
6
- import { createTRPCClient, getUntypedClient, httpLink, } from "@trpc/client";
7
- import { createContext, useContext, useState, } from "react";
8
- import { usePromise } from "./usePromise.js";
9
- export const createTinyRPCClient = ({ callback, trpcClient, values, promises, reactPromises, listeners, states, }) => {
10
- const untypedClient = getUntypedClient(trpcClient);
11
- return createFlatProxy((key) => createRecursiveProxy(({ path, args, }) => {
12
- const pathCopy = [key, ...path];
13
- const utilName = pathCopy.pop();
14
- const fullPath = pathCopy.join(".");
15
- const fullKey = `${fullPath}:${JSON.stringify(args)}`;
16
- const getPromise = (type = "query") => {
17
- if (!promises[fullKey]) {
18
- const promise = untypedClient[type](fullPath, ...args);
19
- promises[fullKey] = promise;
20
- callback?.(fullKey, "NOT_READY_STATE");
21
- promise
22
- .then((value) => callback?.(fullKey, value))
23
- .catch((e) => {
24
- console.error(e);
25
- });
26
- }
27
- return promises[fullKey];
28
- };
29
- /*
30
- * usePromise — это хук, и вызывается он отсюда, из прокси, а не
31
- * из компонента. Так и задумано: чтение значения происходит в
32
- * теле рендера потребителя, поэтому порядок вызовов совпадает с
33
- * порядком чтений. Отвечает за это вызывающий: читать значения
34
- * нужно безусловно, как обычные хуки.
35
- */
36
- /* eslint-disable react-hooks/rules-of-hooks */
37
- const get = (type = "query") => {
38
- if (values[fullKey]) {
39
- if (values[fullKey] === "NOT_READY_STATE") {
40
- if (!reactPromises[fullKey]) {
41
- reactPromises[fullKey] = usePromise(new Promise((resolve) => {
42
- if (!listeners[fullKey])
43
- listeners[fullKey] = [];
44
- listeners[fullKey].push((data) => {
45
- values[fullKey] = data;
46
- resolve(data);
47
- });
48
- }));
49
- }
50
- return reactPromises[fullKey]();
51
- }
52
- return values[fullKey];
53
- }
54
- if (!reactPromises[fullKey]) {
55
- reactPromises[fullKey] = usePromise(getPromise(type));
56
- }
57
- return reactPromises[fullKey]?.();
58
- };
59
- /* eslint-enable react-hooks/rules-of-hooks */
60
- switch (utilName) {
61
- case "get": {
62
- return get();
63
- }
64
- case "mutate": {
65
- return get("mutation");
66
- }
67
- case "getPromise": {
68
- return getPromise();
69
- }
70
- case "mutatePromise": {
71
- return getPromise("mutation");
72
- }
73
- case "clearCache": {
74
- // delete по отсутствующему ключу — no-op, проверка
75
- // была лишней
76
- delete values[fullKey];
77
- delete promises[fullKey];
78
- delete reactPromises[fullKey];
79
- break;
80
- }
81
- case "reFetch": {
82
- delete values[fullKey];
83
- delete promises[fullKey];
84
- delete reactPromises[fullKey];
85
- return get();
86
- }
87
- case "setCache": {
88
- return (value) => {
89
- values[fullKey] = value;
90
- // в каждом словаре лежит то, что он объявляет:
91
- // раньше сюда клали сырое значение, и повторный
92
- // getPromise вернул бы не промис
93
- promises[fullKey] = Promise.resolve(value);
94
- reactPromises[fullKey] = () => value;
95
- states[fullKey]?.[1](value);
96
- };
97
- }
98
- case "getState": {
99
- if (!states[fullKey]) {
100
- states[fullKey] = useState(get());
101
- }
102
- return states[fullKey];
103
- }
104
- }
105
- }));
106
- };
107
- class Store {
108
- values;
109
- trpcClient;
110
- tiny;
111
- promises;
112
- reactPromises;
113
- listeners = {};
114
- states = {};
115
- callback;
116
- constructor({ trpcClient, values, promises, callback, }) {
117
- this.trpcClient = trpcClient;
118
- this.values = values ?? {};
119
- this.promises = promises ?? {};
120
- this.reactPromises = {};
121
- this.callback = callback;
122
- this.tiny = createTinyRPCClient({
123
- trpcClient,
124
- values: this.values,
125
- promises: this.promises,
126
- reactPromises: this.reactPromises,
127
- callback,
128
- listeners: this.listeners,
129
- states: this.states,
130
- });
131
- }
132
- emit(key, data) {
133
- this.listeners[key]?.forEach((cb) => cb(data));
134
- }
135
- }
136
- const StoreContext = createContext({});
137
- const useStore = () => useContext(StoreContext);
138
- const useTrpc = () => useContext(TRPCContext);
139
- // tRPC ждёт JSON; прокси или express могут ответить HTML — превращаем такой
140
- // ответ в читаемую tRPC-ошибку вместо "Unexpected token '<'"
141
- export const jsonSafeFetch = async (url, options) => {
142
- const res = await fetch(url, options);
143
- const contentType = res.headers.get("content-type") || "";
144
- if (contentType.includes("application/json"))
145
- return res;
146
- const text = (await res.text())
147
- .replace(/<[^>]*>/g, " ")
148
- .replace(/\s+/g, " ")
149
- .trim();
150
- return new Response(JSON.stringify({
151
- error: {
152
- message: `API returned a non-JSON response (HTTP ${res.status}): ${text.slice(0, 300)}`,
153
- code: -32603,
154
- data: { code: "INTERNAL_SERVER_ERROR", httpStatus: res.status },
155
- },
156
- }), {
157
- status: res.status,
158
- headers: { "content-type": "application/json" },
159
- });
160
- };
161
- // Клиент создаётся до того, как приложение объявит свой роутер, поэтому
162
- // собирается на нетипизированном и приводится к AppRouter здесь.
163
- const trpcClient = createTRPCClient({
164
- links: [
165
- httpLink({
166
- url: "/api/trpc",
167
- fetch: jsonSafeFetch,
168
- }),
169
- ],
170
- });
171
- const TRPCContext = createContext({});
172
- export { useStore, StoreContext, useTrpc, TRPCContext, trpcClient };
1
+ // Публичная поверхность пакета: реализация разложена по файлам
2
+ // (1 функция = 1 файл), здесь — только реэкспорт под прежними именами.
3
+ import { Store } from "./Store.js";
4
+ export { createTinyRPCClient, } from "./createTinyRPCClient.js";
5
+ export { jsonSafeFetch } from "./jsonSafeFetch.js";
6
+ export { ERROR_STATE, NOT_READY_STATE } from "./sentinels.js";
7
+ export { trpcClient } from "./trpcClient.js";
8
+ export { StoreContext, TRPCContext, useStore, useTrpc, } from "./context.js";
173
9
  export default Store;
@@ -0,0 +1 @@
1
+ export declare const jsonSafeFetch: (url: RequestInfo | URL, options?: RequestInit) => Promise<Response>;
@@ -0,0 +1,22 @@
1
+ // tRPC ждёт JSON; прокси или express могут ответить HTML — превращаем такой
2
+ // ответ в читаемую tRPC-ошибку вместо "Unexpected token '<'"
3
+ export const jsonSafeFetch = async (url, options) => {
4
+ const res = await fetch(url, options);
5
+ const contentType = res.headers.get("content-type") || "";
6
+ if (contentType.includes("application/json"))
7
+ return res;
8
+ const text = (await res.text())
9
+ .replace(/<[^>]*>/g, " ")
10
+ .replace(/\s+/g, " ")
11
+ .trim();
12
+ return new Response(JSON.stringify({
13
+ error: {
14
+ message: `API returned a non-JSON response (HTTP ${res.status}): ${text.slice(0, 300)}`,
15
+ code: -32603,
16
+ data: { code: "INTERNAL_SERVER_ERROR", httpStatus: res.status },
17
+ },
18
+ }), {
19
+ status: res.status,
20
+ headers: { "content-type": "application/json" },
21
+ });
22
+ };
@@ -0,0 +1,4 @@
1
+ /** значение ещё не готово — сервер стримит его позже */
2
+ export declare const NOT_READY_STATE = "__quark:not-ready__";
3
+ /** серверный запрос упал — клиент должен перезапросить сам */
4
+ export declare const ERROR_STATE = "__quark:error__";
@@ -0,0 +1,7 @@
1
+ // Сентинелы протокола SSR-гидратации (window.cache). Строки, а не символы:
2
+ // значения переживают JSON-сериализацию в стрим-скриптах. Значения намеренно
3
+ // «непроизносимые», чтобы не совпасть с легитимным ответом процедуры.
4
+ /** значение ещё не готово — сервер стримит его позже */
5
+ export const NOT_READY_STATE = "__quark:not-ready__";
6
+ /** серверный запрос упал — клиент должен перезапросить сам */
7
+ export const ERROR_STATE = "__quark:error__";
@@ -0,0 +1 @@
1
+ export declare const stableStringify: (value: unknown) => string;
@@ -0,0 +1,14 @@
1
+ // Стабильная сериализация аргументов для ключа кэша: вызовы, одинаковые по
2
+ // смыслу, обязаны давать один ключ независимо от порядка ключей объекта.
3
+ const sortValue = (value) => {
4
+ if (Array.isArray(value))
5
+ return value.map(sortValue);
6
+ if (value && typeof value === "object") {
7
+ const source = value;
8
+ return Object.fromEntries(Object.keys(source)
9
+ .sort()
10
+ .map((key) => [key, sortValue(source[key])]));
11
+ }
12
+ return value;
13
+ };
14
+ export const stableStringify = (value) => JSON.stringify(sortValue(value));
@@ -0,0 +1,6 @@
1
+ import { CreateTRPCClient } from "@trpc/client";
2
+ import type { AppRouter } from "./types.js";
3
+ type AppTRPCClient = CreateTRPCClient<AppRouter>;
4
+ export type TRPCClient = AppTRPCClient;
5
+ export declare const trpcClient: AppTRPCClient;
6
+ export {};
@@ -0,0 +1,12 @@
1
+ import { createTRPCClient, httpLink } from "@trpc/client";
2
+ import { jsonSafeFetch } from "./jsonSafeFetch.js";
3
+ // Клиент создаётся до того, как приложение объявит свой роутер, поэтому
4
+ // собирается на нетипизированном и приводится к AppRouter здесь.
5
+ export const trpcClient = createTRPCClient({
6
+ links: [
7
+ httpLink({
8
+ url: "/api/trpc",
9
+ fetch: jsonSafeFetch,
10
+ }),
11
+ ],
12
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quark-fw/store",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Isomorphic store and tRPC client hooks for the Quark framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,8 @@
18
18
  "dist"
19
19
  ],
20
20
  "scripts": {
21
- "build": "tsc -p tsconfig.build.json && node ../../scripts/postbuild.mjs"
21
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/postbuild.mjs",
22
+ "test": "vitest run"
22
23
  },
23
24
  "keywords": [
24
25
  "quark"
@@ -36,6 +37,10 @@
36
37
  "react": "^19.2.4"
37
38
  },
38
39
  "devDependencies": {
39
- "@types/react": "^19.2.14"
40
+ "@types/react": "^19.2.14",
41
+ "happy-dom": "^20.14.0",
42
+ "react": "^19.2.8",
43
+ "react-dom": "^19.2.8",
44
+ "vitest": "^5.0.0"
40
45
  }
41
46
  }