Skip to content

Commit 9632607

Browse files
committed
feat(@unimolecule/canon): ✨ generate d.ts for status-codes
1 parent f9d5251 commit 9632607

22 files changed

Lines changed: 1026 additions & 10 deletions

.changeset/sparkly-clouds-think.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@unimolecule/canon": patch
3+
---
4+
5+
generate d.ts for status-codes

internal/cache/AGENTS.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Cache Package Instructions
2+
3+
## Scope
4+
5+
`@unimolecule/cache` owns the shared cache contract and default in-memory implementation.
6+
7+
## Boundary Rules
8+
9+
- Keep this package runtime-neutral enough for shared packages, Node-like runtimes, and web-compatible runtimes.
10+
- Do not add Redis, Cloudflare KV, R2, D1, or app-specific storage clients here.
11+
- Platform-specific cache stores belong in the application layer and should extend the shared `Cache` contract.
12+
- Reuse JSON helpers from `@unimolecule/utils` for serialization boundaries.
13+
14+
## Implementation Rules
15+
16+
- Keep the `Cache` base contract small and explicit.
17+
- TTL values are always milliseconds.
18+
- `maxSize` values are always bytes.
19+
- Preserve key-prefix normalization behavior.
20+
- Keep implementation helpers in `utils.ts` and shared public types in `types.ts`.
21+
- Prefer fail-fast errors for unimplemented methods or invalid cache values.
22+
23+
## Documentation
24+
25+
- README must describe this as a library package: contract, memory driver, inputs/outputs, examples, and runtime notes.
26+
- Include examples for default factory usage, direct `MemoryCache` usage, and custom store implementation when behavior changes.
27+
28+
## Verification
29+
30+
- Run `pnpm -F @unimolecule/cache test` for behavior changes.
31+
- Run `pnpm -F @unimolecule/cache build` for export or build changes.
32+
- Run `pnpm -F @unimolecule/cache lint` after broad TypeScript or Markdown edits.

internal/cache/README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# @shamt/cache
2+
3+
<p><strong>English</strong> | <a href="./README.zh-CN.md">中文</a></p>
4+
5+
## Table of Contents
6+
7+
- [Overview](#overview)
8+
- [Design and Architecture](#design-and-architecture)
9+
- [Inputs and Outputs](#inputs-and-outputs)
10+
- [Usage](#usage)
11+
- [Implementation Notes](#implementation-notes)
12+
13+
## Overview
14+
15+
`@shamt/cache` defines the shared cache abstraction for the workspace and provides the default in-memory implementation. It is designed to stay runtime-neutral enough for shared packages, Node-like runtimes, and web-compatible runtimes.
16+
17+
The package currently includes:
18+
19+
- `Cache`: abstract cache contract.
20+
- `MemoryCache`: LRU-based in-memory cache implementation.
21+
- `createCache`: factory that creates the default `MemoryCache`.
22+
- Utility functions for cache key normalization and value serialization.
23+
24+
Platform-specific stores such as Redis and Cloudflare KV should live in the application layer and extend the same `Cache` contract.
25+
26+
## Design and Architecture
27+
28+
`@shamt/cache` separates the cache contract from concrete runtime storage:
29+
30+
- The `Cache` base class stores the backing client in `this.store` and declares the core cache methods every implementation must provide: `set`, `get`, `del`, and `has`.
31+
- Lifecycle methods such as `connect` and `dispose` are implementation-specific and are not part of the base `Cache` contract.
32+
- Base methods throw `CacheMethodNotImplementedError` by default, so missing implementations fail early during development.
33+
- TTL values passed into the package are always handled as milliseconds.
34+
- Values are serialized at the cache boundary with JSON helpers from `@unimolecule/utils`, making the memory driver behave closer to string-based stores such as Redis and KV.
35+
- Keys can be namespaced with a prefix. `MemoryCache` defaults to `cache:`;
36+
non-empty prefixes are normalized to end with `:`, and an empty prefix disables
37+
namespacing.
38+
39+
`MemoryCache` uses `lru-cache` for eviction, TTL, and max-size accounting. It is suitable for local development, tests, short-lived in-process caching, and runtime-neutral default behavior.
40+
41+
## Inputs and Outputs
42+
43+
Inputs:
44+
45+
- Logical cache keys as strings.
46+
- JSON-serializable values.
47+
- Per-write options, such as `{ ttl }`.
48+
- Store-level options, such as `{ ttl, keyPrefix, maxSize }`.
49+
50+
Outputs:
51+
52+
- Write methods return `Promise<void>`.
53+
- Read methods return `Promise<T | undefined>`.
54+
- Existence checks return `Promise<boolean>`.
55+
- Invalid TTL values, non-serializable values, or missing required methods throw errors.
56+
57+
Units:
58+
59+
- `ttl` is always in milliseconds.
60+
- `maxSize` is always in bytes.
61+
- `keyPrefix` defaults to `cache:` for `MemoryCache` and `createCache`.
62+
63+
## Usage
64+
65+
Use the default factory:
66+
67+
```ts
68+
import { createCache } from "@shamt/cache";
69+
70+
const cache = createCache({
71+
ttl: 60_000,
72+
keyPrefix: "shop",
73+
});
74+
75+
await cache.set("settings", { currency: "USD" });
76+
77+
const settings = await cache.get<{ currency: string }>("settings");
78+
const exists = await cache.has("settings");
79+
80+
await cache.del("settings");
81+
```
82+
83+
Use `MemoryCache` directly:
84+
85+
```ts
86+
import { MemoryCache } from "@shamt/cache";
87+
88+
const cache = new MemoryCache({
89+
ttl: 5 * 60_000,
90+
keyPrefix: "session",
91+
maxSize: 1024 * 1024,
92+
});
93+
94+
await cache.connect();
95+
await cache.set("offline:shop.myshopify.com", {
96+
accessToken: "token",
97+
});
98+
99+
const session = await cache.get<{ accessToken: string }>(
100+
"offline:shop.myshopify.com",
101+
);
102+
103+
await cache.dispose();
104+
```
105+
106+
Implement a platform-specific store:
107+
108+
```ts
109+
import {
110+
Cache,
111+
deserializeCacheValue,
112+
serializeCacheValue,
113+
type CacheSetOptions,
114+
} from "@shamt/cache";
115+
116+
interface RedisClient {
117+
set: (key: string, value: string, options?: { px: number }) => Promise<void>;
118+
get: (key: string) => Promise<string | null>;
119+
del: (key: string) => Promise<void>;
120+
exists: (key: string) => Promise<number>;
121+
quit: () => Promise<void>;
122+
}
123+
124+
class RedisCache extends Cache<RedisClient> {
125+
async connect() {
126+
// Open Redis connection.
127+
}
128+
129+
override async set<T>(key: string, value: T, options: CacheSetOptions = {}) {
130+
const ttl = this.resolveTtl(options.ttl);
131+
await this.store.set(
132+
key,
133+
serializeCacheValue(value),
134+
ttl === undefined ? undefined : { px: ttl },
135+
);
136+
}
137+
138+
override async get<T>(key: string): Promise<T | undefined> {
139+
return deserializeCacheValue<T>((await this.store.get(key)) ?? undefined);
140+
}
141+
142+
override async del(key: string) {
143+
await this.store.del(key);
144+
}
145+
146+
override async has(key: string) {
147+
return (await this.store.exists(key)) > 0;
148+
}
149+
150+
async dispose() {
151+
await this.store.quit();
152+
}
153+
}
154+
```
155+
156+
## Implementation Notes
157+
158+
`@shamt/cache` currently only includes the memory driver. This avoids forcing Redis, Cloudflare KV, or other platform SDKs into the shared package dependency graph, keeping the package easier to reuse in node, web, and serverless/isolate environments.
159+
160+
Applications can choose runtime-specific stores at the application layer while still reusing the same `Cache` abstraction.

internal/cache/README.zh-CN.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# @shamt/cache
2+
3+
<p><a href="./README.md">English</a> | <strong>中文</strong></p>
4+
5+
## 目录
6+
7+
- [介绍](#介绍)
8+
- [设计与架构](#设计与架构)
9+
- [输入与输出](#输入与输出)
10+
- [使用方式](#使用方式)
11+
- [实现说明](#实现说明)
12+
13+
## 介绍
14+
15+
`@shamt/cache` 定义 workspace 共享的 cache 抽象,并提供默认的内存实现。这个包希望保持足够 runtime-neutral,方便在共享包、Node-like runtime、以及 web-compatible runtime 中使用。
16+
17+
当前包内包含:
18+
19+
- `Cache`: 抽象 cache contract。
20+
- `MemoryCache`: 基于 LRU 的内存 cache 实现。
21+
- `createCache`: 创建默认 `MemoryCache` 的工厂函数。
22+
- cache key 规范化、value 序列化等工具函数。
23+
24+
Redis、Cloudflare KV 等平台相关 store 不放在这个包内,应该由应用层自行实现并继承同一个 `Cache` contract。
25+
26+
## 设计与架构
27+
28+
`@shamt/cache` 将 cache contract 与具体运行时存储分离:
29+
30+
- `Cache` 基类把底层客户端保存在 `this.store`,并声明所有实现类必须具备的核心 cache 方法:`set``get``del``has`
31+
- `connect``dispose` 等生命周期方法属于具体实现自己的能力,不放在 `Cache` 基类 contract 中。
32+
- 基类方法默认抛出 `CacheMethodNotImplementedError`,让未实现的方法在开发阶段尽早暴露。
33+
- 包内传入的 TTL 均按毫秒处理。
34+
- value 在 cache 边界统一使用 `@unimolecule/utils` 的 JSON helper 序列化,让 memory driver 的行为更接近 Redis、KV 这类字符串存储。
35+
- key 可以使用 prefix 做命名空间隔离。`MemoryCache` 默认使用 `cache:`;非空 prefix 会被规范化为以 `:` 结尾,空字符串会关闭命名空间。
36+
37+
`MemoryCache` 使用 `lru-cache` 负责淘汰、TTL 与 max-size 统计。它适合本地开发、测试、短生命周期进程内缓存,以及 runtime-neutral 的默认行为。
38+
39+
## 输入与输出
40+
41+
输入:
42+
43+
- 字符串形式的逻辑 cache key。
44+
- JSON-serializable value。
45+
- 单次写入选项,例如 `{ ttl }`
46+
- store 级配置,例如 `{ ttl, keyPrefix, maxSize }`
47+
48+
输出:
49+
50+
- 写操作返回 `Promise<void>`
51+
- 读取操作返回 `Promise<T | undefined>`
52+
- 存在性检查返回 `Promise<boolean>`
53+
- TTL 非法、value 无法序列化、实现类缺少必需方法时抛出错误。
54+
55+
单位:
56+
57+
- `ttl` 始终为毫秒。
58+
- `maxSize` 始终为字节。
59+
- `MemoryCache``createCache``keyPrefix` 默认是 `cache:`
60+
61+
## 使用方式
62+
63+
使用默认工厂函数:
64+
65+
```ts
66+
import { createCache } from "@shamt/cache";
67+
68+
const cache = createCache({
69+
ttl: 60_000,
70+
keyPrefix: "shop",
71+
});
72+
73+
await cache.set("settings", { currency: "USD" });
74+
75+
const settings = await cache.get<{ currency: string }>("settings");
76+
const exists = await cache.has("settings");
77+
78+
await cache.del("settings");
79+
```
80+
81+
直接使用 `MemoryCache`
82+
83+
```ts
84+
import { MemoryCache } from "@shamt/cache";
85+
86+
const cache = new MemoryCache({
87+
ttl: 5 * 60_000,
88+
keyPrefix: "session",
89+
maxSize: 1024 * 1024,
90+
});
91+
92+
await cache.connect();
93+
await cache.set("offline:shop.myshopify.com", {
94+
accessToken: "token",
95+
});
96+
97+
const session = await cache.get<{ accessToken: string }>(
98+
"offline:shop.myshopify.com",
99+
);
100+
101+
await cache.dispose();
102+
```
103+
104+
实现平台相关 store:
105+
106+
```ts
107+
import {
108+
Cache,
109+
deserializeCacheValue,
110+
serializeCacheValue,
111+
type CacheSetOptions,
112+
} from "@shamt/cache";
113+
114+
interface RedisClient {
115+
set: (key: string, value: string, options?: { px: number }) => Promise<void>;
116+
get: (key: string) => Promise<string | null>;
117+
del: (key: string) => Promise<void>;
118+
exists: (key: string) => Promise<number>;
119+
quit: () => Promise<void>;
120+
}
121+
122+
class RedisCache extends Cache<RedisClient> {
123+
async connect() {
124+
// Open Redis connection.
125+
}
126+
127+
override async set<T>(key: string, value: T, options: CacheSetOptions = {}) {
128+
const ttl = this.resolveTtl(options.ttl);
129+
await this.store.set(
130+
key,
131+
serializeCacheValue(value),
132+
ttl === undefined ? undefined : { px: ttl },
133+
);
134+
}
135+
136+
override async get<T>(key: string): Promise<T | undefined> {
137+
return deserializeCacheValue<T>((await this.store.get(key)) ?? undefined);
138+
}
139+
140+
override async del(key: string) {
141+
await this.store.del(key);
142+
}
143+
144+
override async has(key: string) {
145+
return (await this.store.exists(key)) > 0;
146+
}
147+
148+
async dispose() {
149+
await this.store.quit();
150+
}
151+
}
152+
```
153+
154+
## 实现说明
155+
156+
`@shamt/cache` 当前只内置 memory driver。这样可以避免把 Redis、Cloudflare KV 或其他平台 SDK 强行带入共享包依赖图,保证这个包在 node、web、serverless/isolate 等环境中更容易复用。
157+
158+
应用层可以根据部署 runtime 选择自己的 store,但仍然复用同一个 `Cache` 抽象。

internal/cache/build.config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import process from "node:process";
2+
import { defineConfig } from "tsdown";
3+
4+
export default defineConfig([
5+
{
6+
entry: ["./src/index.ts"],
7+
format: ["esm", "cjs"],
8+
platform: "node",
9+
dts: true,
10+
tsconfig: "./tsconfig.json",
11+
watch: process.env.NODE_ENV === "development",
12+
},
13+
]);

0 commit comments

Comments
 (0)