@shopbb/helium 0.1.0
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/README.md +50 -0
- package/dist/cache.d.ts +16 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +26 -0
- package/dist/cache.js.map +1 -0
- package/dist/cart-id.d.ts +33 -0
- package/dist/cart-id.d.ts.map +1 -0
- package/dist/cart-id.js +42 -0
- package/dist/cart-id.js.map +1 -0
- package/dist/createCartHandler.d.ts +17 -0
- package/dist/createCartHandler.d.ts.map +1 -0
- package/dist/createCartHandler.js +156 -0
- package/dist/createCartHandler.js.map +1 -0
- package/dist/createHeliumContext.d.ts +23 -0
- package/dist/createHeliumContext.d.ts.map +1 -0
- package/dist/createHeliumContext.js +66 -0
- package/dist/createHeliumContext.js.map +1 -0
- package/dist/createStorefrontClient.d.ts +19 -0
- package/dist/createStorefrontClient.d.ts.map +1 -0
- package/dist/createStorefrontClient.js +131 -0
- package/dist/createStorefrontClient.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/cache.ts +31 -0
- package/src/cart-id.ts +63 -0
- package/src/createCartHandler.ts +189 -0
- package/src/createHeliumContext.ts +72 -0
- package/src/createStorefrontClient.ts +164 -0
- package/src/index.ts +40 -0
- package/src/types.ts +133 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createStorefrontClient
|
|
3
|
+
*
|
|
4
|
+
* A thin GraphQL client wrapper around fetch(), with caching.
|
|
5
|
+
*
|
|
6
|
+
* 行为对齐 Shopify Hydrogen 的 storefront client:
|
|
7
|
+
* - query() 可缓存(CacheLong / CacheShort / etc.)
|
|
8
|
+
* - mutate() 不缓存
|
|
9
|
+
* - 自动注入 X-Storefront-Access-Token header
|
|
10
|
+
* - GraphQL errors 非空时抛出
|
|
11
|
+
*
|
|
12
|
+
* 内部缓存策略:
|
|
13
|
+
* - 用 caches.default 或传入的 cache 对象(Cache API)
|
|
14
|
+
* - cache key = SHA256(query + variables + storeId)
|
|
15
|
+
* - 命中 / SWR / MISS 模式区分
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
StorefrontClient,
|
|
20
|
+
StorefrontClientOptions,
|
|
21
|
+
QueryOptions,
|
|
22
|
+
MutateOptions,
|
|
23
|
+
CacheStrategy,
|
|
24
|
+
} from './types';
|
|
25
|
+
import { CacheNone, CacheShort, CacheLong, CacheCustom } from './cache';
|
|
26
|
+
|
|
27
|
+
export function createStorefrontClient(options: StorefrontClientOptions): StorefrontClient {
|
|
28
|
+
const {
|
|
29
|
+
apiUrl,
|
|
30
|
+
publicAccessToken,
|
|
31
|
+
privateAccessToken,
|
|
32
|
+
storeId,
|
|
33
|
+
cache,
|
|
34
|
+
waitUntil,
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
const headers = (): Record<string, string> => ({
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
Accept: 'application/json',
|
|
40
|
+
'X-Storefront-Access-Token': privateAccessToken || publicAccessToken,
|
|
41
|
+
'X-Store-Id': storeId,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compute a stable cache key for a query + variables.
|
|
46
|
+
*/
|
|
47
|
+
async function cacheKeyFor(query: string, variables: any): Promise<Request> {
|
|
48
|
+
const payload = JSON.stringify({ q: query, v: variables ?? {}, s: storeId });
|
|
49
|
+
const hashBuf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload));
|
|
50
|
+
const hashHex = Array.from(new Uint8Array(hashBuf))
|
|
51
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
52
|
+
.join('');
|
|
53
|
+
return new Request(`https://helium-cache.invalid/${storeId}/${hashHex}`, { method: 'GET' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function executeGraphQL<TData>(
|
|
57
|
+
query: string,
|
|
58
|
+
variables: any | undefined,
|
|
59
|
+
): Promise<TData> {
|
|
60
|
+
const body = JSON.stringify({ query, variables });
|
|
61
|
+
const res = await fetch(apiUrl, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: headers(),
|
|
64
|
+
body,
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(`Storefront API HTTP ${res.status}: ${await res.text()}`);
|
|
68
|
+
}
|
|
69
|
+
const json: any = await res.json();
|
|
70
|
+
if (json.errors && json.errors.length > 0) {
|
|
71
|
+
const messages = json.errors.map((e: any) => e.message).join('; ');
|
|
72
|
+
throw new Error(`Storefront GraphQL errors: ${messages}`);
|
|
73
|
+
}
|
|
74
|
+
return json.data as TData;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function query<TData = any>(q: string, opts: QueryOptions = {}): Promise<TData> {
|
|
78
|
+
const cacheStrategy: CacheStrategy = opts.cache ?? CacheNone();
|
|
79
|
+
|
|
80
|
+
// No caching → direct call
|
|
81
|
+
if (cacheStrategy.mode === 'NONE' || !cache) {
|
|
82
|
+
return executeGraphQL<TData>(q, opts.variables);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const key = await cacheKeyFor(q, opts.variables);
|
|
86
|
+
const cached = await cache.match(key);
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
|
|
89
|
+
if (cached) {
|
|
90
|
+
const ttl = Number(cached.headers.get('helium-ttl') ?? '0');
|
|
91
|
+
const cachedAt = Number(cached.headers.get('helium-cached-at') ?? '0');
|
|
92
|
+
const age = (now - cachedAt) / 1000;
|
|
93
|
+
|
|
94
|
+
if (age < ttl) {
|
|
95
|
+
// Fresh
|
|
96
|
+
return (await cached.json()) as TData;
|
|
97
|
+
}
|
|
98
|
+
// Stale, return stale-while-revalidate if configured
|
|
99
|
+
const swr = cacheStrategy.staleWhileRevalidate ?? 0;
|
|
100
|
+
if (age < ttl + swr) {
|
|
101
|
+
// Trigger background revalidation
|
|
102
|
+
if (waitUntil) {
|
|
103
|
+
waitUntil(refreshCache(q, opts.variables, key, cacheStrategy));
|
|
104
|
+
}
|
|
105
|
+
return (await cached.json()) as TData;
|
|
106
|
+
}
|
|
107
|
+
// Expired
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = await executeGraphQL<TData>(q, opts.variables);
|
|
111
|
+
// Async write to cache
|
|
112
|
+
if (waitUntil) {
|
|
113
|
+
waitUntil(writeCache(key, data, cacheStrategy));
|
|
114
|
+
} else {
|
|
115
|
+
// Best-effort sync
|
|
116
|
+
await writeCache(key, data, cacheStrategy);
|
|
117
|
+
}
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function refreshCache(
|
|
122
|
+
q: string,
|
|
123
|
+
variables: any,
|
|
124
|
+
key: Request,
|
|
125
|
+
strategy: CacheStrategy,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
const data = await executeGraphQL(q, variables);
|
|
129
|
+
await writeCache(key, data, strategy);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Swallow errors during background refresh
|
|
132
|
+
console.error('[helium] cache refresh failed', e);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function writeCache(key: Request, data: any, strategy: CacheStrategy): Promise<void> {
|
|
137
|
+
if (!cache) return;
|
|
138
|
+
const ttl = strategy.maxAge;
|
|
139
|
+
const swr = strategy.staleWhileRevalidate ?? 0;
|
|
140
|
+
const totalAge = ttl + swr;
|
|
141
|
+
const resp = new Response(JSON.stringify(data), {
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
'Cache-Control': `public, max-age=${totalAge}`,
|
|
145
|
+
'helium-ttl': String(ttl),
|
|
146
|
+
'helium-cached-at': String(Date.now()),
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
await cache.put(key, resp);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function mutate<TData = any>(m: string, opts: MutateOptions = {}): Promise<TData> {
|
|
153
|
+
return executeGraphQL<TData>(m, opts.variables);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
query,
|
|
158
|
+
mutate,
|
|
159
|
+
CacheNone,
|
|
160
|
+
CacheShort,
|
|
161
|
+
CacheLong,
|
|
162
|
+
CacheCustom,
|
|
163
|
+
};
|
|
164
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @shopbb/helium
|
|
3
|
+
*
|
|
4
|
+
* shopbb's storefront framework. Hydrogen-equivalent for the shopbb platform.
|
|
5
|
+
*
|
|
6
|
+
* Public API:
|
|
7
|
+
*
|
|
8
|
+
* - createHeliumContext Top-level factory
|
|
9
|
+
* - createStorefrontClient GraphQL client (used by createHeliumContext)
|
|
10
|
+
* - createCartHandler Cart operations (used by createHeliumContext)
|
|
11
|
+
* - cartGetIdDefault Default cart ID getter (reads cookie)
|
|
12
|
+
* - cartSetIdDefault Default cart ID setter (writes Set-Cookie)
|
|
13
|
+
* - CacheNone / CacheShort / CacheLong / CacheCustom
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { createHeliumContext } from './createHeliumContext';
|
|
17
|
+
export { createStorefrontClient } from './createStorefrontClient';
|
|
18
|
+
export { createCartHandler, DEFAULT_CART_FRAGMENT } from './createCartHandler';
|
|
19
|
+
export { cartGetIdDefault, cartSetIdDefault } from './cart-id';
|
|
20
|
+
export type { CartSetIdOptions } from './cart-id';
|
|
21
|
+
export { CacheNone, CacheShort, CacheLong, CacheCustom } from './cache';
|
|
22
|
+
|
|
23
|
+
// Re-export all types
|
|
24
|
+
export type {
|
|
25
|
+
CacheStrategy,
|
|
26
|
+
StorefrontClient,
|
|
27
|
+
StorefrontClientOptions,
|
|
28
|
+
QueryOptions,
|
|
29
|
+
MutateOptions,
|
|
30
|
+
CartHandler,
|
|
31
|
+
CartHandlerOptions,
|
|
32
|
+
CartLineInput,
|
|
33
|
+
CartLineUpdateInput,
|
|
34
|
+
CartUserError,
|
|
35
|
+
CartResult,
|
|
36
|
+
CartIdGetter,
|
|
37
|
+
CartIdSetter,
|
|
38
|
+
HeliumContext,
|
|
39
|
+
HeliumContextOptions,
|
|
40
|
+
} from './types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helium shared types.
|
|
3
|
+
*
|
|
4
|
+
* 这些类型在 storefront client / cart handler / context 之间共享。
|
|
5
|
+
* 对应 shopbb Storefront API GraphQL schema 的核心类型。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================
|
|
9
|
+
// Cache
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
export interface CacheStrategy {
|
|
13
|
+
mode: 'NONE' | 'PUBLIC';
|
|
14
|
+
/** CDN/edge cache TTL in seconds */
|
|
15
|
+
maxAge: number;
|
|
16
|
+
/** SWR window in seconds */
|
|
17
|
+
staleWhileRevalidate?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Storefront Client
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
export interface StorefrontClientOptions {
|
|
25
|
+
/** GraphQL endpoint URL */
|
|
26
|
+
apiUrl: string;
|
|
27
|
+
/** Public Storefront API access token */
|
|
28
|
+
publicAccessToken: string;
|
|
29
|
+
/** Private Storefront API access token (preferred when available) */
|
|
30
|
+
privateAccessToken?: string;
|
|
31
|
+
/** Store ID */
|
|
32
|
+
storeId: string;
|
|
33
|
+
/** Original request, used to forward identifying headers if needed */
|
|
34
|
+
request?: Request;
|
|
35
|
+
/** Cache instance (e.g. caches.default or caches.open(...)) */
|
|
36
|
+
cache?: Cache;
|
|
37
|
+
/** waitUntil from ExecutionContext for SWR */
|
|
38
|
+
waitUntil?: (promise: Promise<any>) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface QueryOptions {
|
|
42
|
+
variables?: Record<string, any>;
|
|
43
|
+
cache?: CacheStrategy;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MutateOptions {
|
|
47
|
+
variables?: Record<string, any>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StorefrontClient {
|
|
51
|
+
query<TData = any>(query: string, options?: QueryOptions): Promise<TData>;
|
|
52
|
+
mutate<TData = any>(mutation: string, options?: MutateOptions): Promise<TData>;
|
|
53
|
+
CacheNone(): CacheStrategy;
|
|
54
|
+
CacheShort(): CacheStrategy;
|
|
55
|
+
CacheLong(): CacheStrategy;
|
|
56
|
+
CacheCustom(seconds: number): CacheStrategy;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================
|
|
60
|
+
// Cart
|
|
61
|
+
// ============================================================
|
|
62
|
+
|
|
63
|
+
export type CartIdGetter = () => string | null;
|
|
64
|
+
export type CartIdSetter = (cartId: string, responseHeaders: Headers) => void;
|
|
65
|
+
|
|
66
|
+
export interface CartHandlerOptions {
|
|
67
|
+
storefront: StorefrontClient;
|
|
68
|
+
getCartId: CartIdGetter;
|
|
69
|
+
/** setCartId 会写入 responseHeaders(注入 Set-Cookie) */
|
|
70
|
+
setCartId: CartIdSetter;
|
|
71
|
+
responseHeaders: Headers;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CartLineInput {
|
|
75
|
+
merchandiseId: string;
|
|
76
|
+
quantity: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CartLineUpdateInput {
|
|
80
|
+
id: string;
|
|
81
|
+
quantity: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CartUserError {
|
|
85
|
+
field?: string[] | null;
|
|
86
|
+
message: string;
|
|
87
|
+
code?: string | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CartResult<TCart = any> {
|
|
91
|
+
cart: TCart | null;
|
|
92
|
+
userErrors: CartUserError[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CartHandler<TCart = any> {
|
|
96
|
+
get(): Promise<TCart | null>;
|
|
97
|
+
getCartId(): string | null;
|
|
98
|
+
setCartId(cartId: string): void;
|
|
99
|
+
create(input?: { lines?: CartLineInput[] }): Promise<CartResult<TCart>>;
|
|
100
|
+
addLines(lines: CartLineInput[]): Promise<CartResult<TCart>>;
|
|
101
|
+
updateLines(lines: CartLineUpdateInput[]): Promise<CartResult<TCart>>;
|
|
102
|
+
removeLines(lineIds: string[]): Promise<CartResult<TCart>>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================
|
|
106
|
+
// Helium Context
|
|
107
|
+
// ============================================================
|
|
108
|
+
|
|
109
|
+
export interface HeliumContextOptions {
|
|
110
|
+
request: Request;
|
|
111
|
+
env: Record<string, any>;
|
|
112
|
+
executionContext: ExecutionContext;
|
|
113
|
+
storefront: Omit<StorefrontClientOptions, 'request' | 'cache' | 'waitUntil'> & {
|
|
114
|
+
request?: Request;
|
|
115
|
+
cache?: Cache;
|
|
116
|
+
waitUntil?: (p: Promise<any>) => void;
|
|
117
|
+
};
|
|
118
|
+
cart?: {
|
|
119
|
+
getId: CartIdGetter;
|
|
120
|
+
setId: (cartId: string, headers: Headers) => void;
|
|
121
|
+
/** Optional custom GraphQL fragment for the Cart return type */
|
|
122
|
+
cartFragment?: string;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface HeliumContext {
|
|
127
|
+
env: Record<string, any>;
|
|
128
|
+
request: Request;
|
|
129
|
+
storefront: StorefrontClient;
|
|
130
|
+
cart: CartHandler;
|
|
131
|
+
/** Headers that should be merged into the outgoing response (e.g. Set-Cookie) */
|
|
132
|
+
responseHeaders: Headers;
|
|
133
|
+
}
|