@quark-fw/store 0.0.1
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/dist/errors.d.ts +5 -0
- package/dist/errors.js +18 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +173 -0
- package/dist/serialize.d.ts +27 -0
- package/dist/serialize.js +3 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/dist/usePromise.d.ts +1 -0
- package/dist/usePromise.js +21 -0
- package/package.json +37 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type ErrorHandler = (error: unknown, context?: string) => void;
|
|
2
|
+
export declare function setErrorHandler(next: ErrorHandler): void;
|
|
3
|
+
export declare function reportError(error: unknown, context?: string): void;
|
|
4
|
+
/** Достаёт человекочитаемое сообщение из ошибки tRPC/Error/строки. */
|
|
5
|
+
export declare function errorMessage(error: unknown): string;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
let handler = (error, context) => {
|
|
2
|
+
console.error(context ? `[${context}]` : "[quark]", error);
|
|
3
|
+
};
|
|
4
|
+
export function setErrorHandler(next) {
|
|
5
|
+
handler = next;
|
|
6
|
+
}
|
|
7
|
+
export function reportError(error, context) {
|
|
8
|
+
handler(error, context);
|
|
9
|
+
}
|
|
10
|
+
/** Достаёт человекочитаемое сообщение из ошибки tRPC/Error/строки. */
|
|
11
|
+
export function errorMessage(error) {
|
|
12
|
+
if (typeof error === "string")
|
|
13
|
+
return error;
|
|
14
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
15
|
+
return String(error.message);
|
|
16
|
+
}
|
|
17
|
+
return "Неизвестная ошибка";
|
|
18
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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 };
|
|
76
|
+
export default Store;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
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 };
|
|
173
|
+
export default Store;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type FilterKeys<TObj extends object, TFilter> = {
|
|
2
|
+
[TKey in keyof TObj]: TObj[TKey] extends TFilter ? TKey : never;
|
|
3
|
+
}[keyof TObj];
|
|
4
|
+
export type Simplify<TType> = TType extends unknown[] | Date ? TType : {
|
|
5
|
+
[K in keyof TType]: TType[K];
|
|
6
|
+
};
|
|
7
|
+
type JsonPrimitive = boolean | number | string | null;
|
|
8
|
+
type FilterDefinedKeys<TObj extends object> = Exclude<{
|
|
9
|
+
[TKey in keyof TObj]: undefined extends TObj[TKey] ? never : TKey;
|
|
10
|
+
}[keyof TObj], undefined>;
|
|
11
|
+
type UndefinedToOptional<T extends object> = Pick<T, FilterDefinedKeys<T>> & {
|
|
12
|
+
[k in keyof Omit<T, FilterDefinedKeys<T>>]?: Exclude<T[k], undefined>;
|
|
13
|
+
};
|
|
14
|
+
type AnyFunction = (...args: never[]) => unknown;
|
|
15
|
+
type NonJsonPrimitive = AnyFunction | symbol | undefined;
|
|
16
|
+
type IsAny<T> = 0 extends T & 1 ? true : false;
|
|
17
|
+
type JsonReturnable = JsonPrimitive | undefined;
|
|
18
|
+
type SerializeTuple<T extends [unknown, ...unknown[]]> = {
|
|
19
|
+
[k in keyof T]: T[k] extends NonJsonPrimitive ? null : Serialize<T[k]>;
|
|
20
|
+
};
|
|
21
|
+
export type SerializeObject<T extends object> = {
|
|
22
|
+
[k in keyof Omit<T, FilterKeys<T, NonJsonPrimitive>>]: Serialize<T[k]>;
|
|
23
|
+
};
|
|
24
|
+
export type Serialize<T> = IsAny<T> extends true ? any : T extends JsonReturnable ? T : T extends Map<unknown, unknown> | Set<unknown> ? object : T extends NonJsonPrimitive ? never : T extends {
|
|
25
|
+
toJSON(): infer U;
|
|
26
|
+
} ? U : T extends [] ? [] : T extends [unknown, ...unknown[]] ? SerializeTuple<T> : T extends readonly (infer U)[] ? (U extends NonJsonPrimitive ? null : Serialize<U>)[] : T extends object ? Simplify<SerializeObject<UndefinedToOptional<T>>> : never;
|
|
27
|
+
export {};
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function usePromise<T>(promise: Promise<T>): () => T;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function usePromise(promise) {
|
|
2
|
+
let status = "pending";
|
|
3
|
+
let response;
|
|
4
|
+
const suspender = promise.then((res) => {
|
|
5
|
+
status = "success";
|
|
6
|
+
response = res;
|
|
7
|
+
}, (err) => {
|
|
8
|
+
status = "error";
|
|
9
|
+
response = err;
|
|
10
|
+
});
|
|
11
|
+
return () => {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case "pending":
|
|
14
|
+
throw suspender;
|
|
15
|
+
case "error":
|
|
16
|
+
throw response;
|
|
17
|
+
default:
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quark-fw/store",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Isomorphic store and tRPC client hooks for the Quark framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./src/*": "./dist/*",
|
|
15
|
+
"./*": "./dist/*"
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/postbuild.mjs"
|
|
20
|
+
},
|
|
21
|
+
"keywords": ["quark"],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@trpc/client": "^11.0.0",
|
|
29
|
+
"@trpc/server": "^11.0.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^19.2.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.2.14"
|
|
36
|
+
}
|
|
37
|
+
}
|