@shaferllc/keel 0.66.0 → 0.68.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.
Files changed (132) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +2 -2
  6. package/dist/core/cache.d.ts +82 -5
  7. package/dist/core/cache.js +181 -23
  8. package/dist/core/cli/stubs.d.ts +12 -0
  9. package/dist/core/cli/stubs.js +120 -0
  10. package/dist/core/events.d.ts +129 -5
  11. package/dist/core/events.js +165 -7
  12. package/dist/core/health.d.ts +141 -0
  13. package/dist/core/health.js +226 -0
  14. package/dist/core/helpers.d.ts +9 -3
  15. package/dist/core/helpers.js +11 -3
  16. package/dist/core/index.d.ts +15 -10
  17. package/dist/core/index.js +7 -4
  18. package/dist/core/lock.d.ts +139 -0
  19. package/dist/core/lock.js +215 -0
  20. package/dist/core/logger.d.ts +82 -4
  21. package/dist/core/logger.js +141 -23
  22. package/dist/core/mail.d.ts +128 -7
  23. package/dist/core/mail.js +264 -16
  24. package/dist/core/queue.d.ts +134 -9
  25. package/dist/core/queue.js +304 -14
  26. package/dist/core/storage.d.ts +159 -6
  27. package/dist/core/storage.js +287 -7
  28. package/dist/mcp/server.d.ts +19 -0
  29. package/dist/mcp/server.js +355 -0
  30. package/docs/ai-manifest.json +2472 -0
  31. package/docs/ai.md +128 -0
  32. package/docs/architecture.md +331 -0
  33. package/docs/authentication.md +453 -0
  34. package/docs/authorization.md +167 -0
  35. package/docs/broadcasting.md +137 -0
  36. package/docs/broker.md +500 -0
  37. package/docs/cache.md +558 -0
  38. package/docs/configuration.md +311 -0
  39. package/docs/console.md +356 -0
  40. package/docs/container.md +467 -0
  41. package/docs/controllers.md +265 -0
  42. package/docs/cors.md +51 -0
  43. package/docs/database.md +530 -0
  44. package/docs/debugging.md +129 -0
  45. package/docs/decorators.md +127 -0
  46. package/docs/errors.md +395 -0
  47. package/docs/events.md +496 -0
  48. package/docs/examples/architecture-app.ts +27 -0
  49. package/docs/examples/authentication.ts +61 -0
  50. package/docs/examples/authorization.ts +79 -0
  51. package/docs/examples/broadcasting.ts +60 -0
  52. package/docs/examples/broker-cache-validate.ts +34 -0
  53. package/docs/examples/broker-fault-tolerance.ts +29 -0
  54. package/docs/examples/broker-middleware.ts +27 -0
  55. package/docs/examples/broker.ts +203 -0
  56. package/docs/examples/cache.ts +222 -0
  57. package/docs/examples/configuration.ts +81 -0
  58. package/docs/examples/container.ts +134 -0
  59. package/docs/examples/controllers.ts +86 -0
  60. package/docs/examples/database.ts +118 -0
  61. package/docs/examples/debugging.ts +41 -0
  62. package/docs/examples/decorators.ts +40 -0
  63. package/docs/examples/errors.ts +121 -0
  64. package/docs/examples/events.ts +204 -0
  65. package/docs/examples/factories.ts +84 -0
  66. package/docs/examples/hashing.ts +71 -0
  67. package/docs/examples/health.ts +94 -0
  68. package/docs/examples/helpers.ts +171 -0
  69. package/docs/examples/hooks.ts +54 -0
  70. package/docs/examples/inertia.ts +81 -0
  71. package/docs/examples/locks.ts +120 -0
  72. package/docs/examples/logger.ts +92 -0
  73. package/docs/examples/mail.ts +160 -0
  74. package/docs/examples/middleware.ts +119 -0
  75. package/docs/examples/migrations.ts +126 -0
  76. package/docs/examples/models.ts +239 -0
  77. package/docs/examples/notification.ts +124 -0
  78. package/docs/examples/providers.ts +123 -0
  79. package/docs/examples/queues.ts +254 -0
  80. package/docs/examples/rate-limiting.ts +42 -0
  81. package/docs/examples/redis.ts +99 -0
  82. package/docs/examples/request-response.ts +197 -0
  83. package/docs/examples/routing.ts +186 -0
  84. package/docs/examples/scheduling.ts +62 -0
  85. package/docs/examples/sessions.ts +102 -0
  86. package/docs/examples/static-files.ts +63 -0
  87. package/docs/examples/storage.ts +132 -0
  88. package/docs/examples/templates.ts +58 -0
  89. package/docs/examples/testing.ts +66 -0
  90. package/docs/examples/transformer.ts +141 -0
  91. package/docs/examples/transformers.ts +49 -0
  92. package/docs/examples/url-builder.ts +86 -0
  93. package/docs/examples/validation.ts +102 -0
  94. package/docs/examples/views.tsx +62 -0
  95. package/docs/examples/vite.ts +106 -0
  96. package/docs/factories.md +166 -0
  97. package/docs/getting-started.md +290 -0
  98. package/docs/hashing.md +259 -0
  99. package/docs/health.md +225 -0
  100. package/docs/helpers.md +347 -0
  101. package/docs/hono.md +186 -0
  102. package/docs/hooks.md +118 -0
  103. package/docs/inertia.md +241 -0
  104. package/docs/locks.md +323 -0
  105. package/docs/logger.md +290 -0
  106. package/docs/mail.md +678 -0
  107. package/docs/middleware.md +425 -0
  108. package/docs/migrations.md +476 -0
  109. package/docs/models.md +810 -0
  110. package/docs/notifications.md +474 -0
  111. package/docs/providers.md +363 -0
  112. package/docs/queues.md +679 -0
  113. package/docs/rate-limiting.md +155 -0
  114. package/docs/redis.md +178 -0
  115. package/docs/request-response.md +953 -0
  116. package/docs/routing.md +804 -0
  117. package/docs/scheduling.md +110 -0
  118. package/docs/security.md +85 -0
  119. package/docs/sessions.md +354 -0
  120. package/docs/social-auth.md +174 -0
  121. package/docs/static-files.md +211 -0
  122. package/docs/storage.md +450 -0
  123. package/docs/templates.md +315 -0
  124. package/docs/testing.md +125 -0
  125. package/docs/transformers.md +381 -0
  126. package/docs/url-builder.md +295 -0
  127. package/docs/validation.md +288 -0
  128. package/docs/views.md +267 -0
  129. package/docs/vite.md +434 -0
  130. package/llms-full.txt +17694 -0
  131. package/llms.txt +116 -0
  132. package/package.json +26 -7
@@ -0,0 +1,60 @@
1
+ // Type-check harness for docs/broadcasting.md. Compile-only — never executed.
2
+ import {
3
+ broadcast,
4
+ setBroadcaster,
5
+ getBroadcaster,
6
+ MemoryBroadcaster,
7
+ channelAuth,
8
+ authorizeChannel,
9
+ json,
10
+ request,
11
+ response,
12
+ auth,
13
+ type Broadcaster,
14
+ } from "@shaferllc/keel/core";
15
+
16
+ declare const sha: string;
17
+ declare const socket: { send(data: string): void };
18
+
19
+ type User = { id: number; name: string };
20
+
21
+ export async function broadcasting() {
22
+ await broadcast("orders.42", "status", { state: "shipped" });
23
+ await broadcast(["team.7", "admins"], "deploy", { sha });
24
+ return getBroadcaster();
25
+ }
26
+
27
+ export function channels() {
28
+ channelAuth("orders.{orderId}", (user, params) => (user as User).id === Number(params.orderId));
29
+ channelAuth("presence.room.{room}", (user, params) => {
30
+ const u = user as User;
31
+ return { id: u.id, name: u.name, room: params.room };
32
+ });
33
+ }
34
+
35
+ export async function authEndpoint() {
36
+ const { channel } = (await request.all()) as { channel: string };
37
+ const ok = await authorizeChannel(channel, await auth().user());
38
+ if (!ok) response.abort("Forbidden", 403);
39
+ return json(ok);
40
+ }
41
+
42
+ export function fanOut() {
43
+ const bus = new MemoryBroadcaster();
44
+ setBroadcaster(bus);
45
+ const off = bus.subscribe("orders.42", (event, payload) =>
46
+ socket.send(JSON.stringify({ event, payload })),
47
+ );
48
+ off();
49
+ }
50
+
51
+ // A Pusher-style fetch driver
52
+ export const pusher = (url: string, authorization: string): Broadcaster => ({
53
+ async publish(channels, event, payload) {
54
+ await fetch(url, {
55
+ method: "POST",
56
+ headers: { "content-type": "application/json", authorization },
57
+ body: JSON.stringify({ channels, name: event, data: JSON.stringify(payload) }),
58
+ });
59
+ },
60
+ });
@@ -0,0 +1,34 @@
1
+ // Type-check harness for the validating & caching sections of docs/broker.md.
2
+ // Compile-only — never executed.
3
+ import { Broker, Cache } from "@shaferllc/keel/core";
4
+ import { z } from "zod";
5
+
6
+ declare function createUser(p: unknown): unknown;
7
+ declare function computeDaily(day: string): unknown;
8
+
9
+ export function validating(broker: Broker) {
10
+ broker.createService({
11
+ name: "users",
12
+ actions: {
13
+ create: {
14
+ params: z.object({ email: z.string().email(), age: z.coerce.number().min(18) }),
15
+ handler: (ctx) => createUser(ctx.params),
16
+ },
17
+ },
18
+ });
19
+ }
20
+
21
+ export function caching(): Broker {
22
+ const broker = new Broker({ cacher: new Cache() });
23
+ broker.createService({
24
+ name: "stats",
25
+ actions: {
26
+ daily: {
27
+ cache: { ttl: 300, keys: ["day"] },
28
+ handler: (ctx: { params: { day: string } }) => computeDaily(ctx.params.day),
29
+ },
30
+ forever: { cache: true, handler: () => 1 },
31
+ },
32
+ });
33
+ return broker;
34
+ }
@@ -0,0 +1,29 @@
1
+ // Type-check harness for the fault-tolerance & registry sections of
2
+ // docs/broker.md. Compile-only — never executed.
3
+ import { Broker } from "@shaferllc/keel/core";
4
+
5
+ declare const id: string;
6
+ declare const cart: unknown;
7
+
8
+ export async function faultTolerance(broker: Broker) {
9
+ await broker.call("orders.get", { id }, {
10
+ retries: 3,
11
+ fallback: { id, status: "unknown" },
12
+ });
13
+ await broker.call("pricing.quote", cart, {
14
+ timeout: 500,
15
+ fallback: (err: Error) => ({ error: err.message, price: null }),
16
+ });
17
+ }
18
+
19
+ export function defaults(): Broker {
20
+ return new Broker({ requestTimeout: 1000, retries: 2 });
21
+ }
22
+
23
+ export function introspection(broker: Broker) {
24
+ const has: boolean = broker.hasAction("users.find");
25
+ const actions: string[] = broker.listActions();
26
+ const services: string[] = broker.listServices();
27
+ const svc = broker.getService("users");
28
+ return { has, actions, services, name: svc?.name };
29
+ }
@@ -0,0 +1,27 @@
1
+ // Type-check harness for the Middlewares section of docs/broker.md.
2
+ // Compile-only — never executed.
3
+ import { Broker, logger, type BrokerMiddleware } from "@shaferllc/keel/core";
4
+
5
+ const timing: BrokerMiddleware = {
6
+ name: "timing",
7
+ localAction(next, action) {
8
+ return async (ctx) => {
9
+ const start = performance.now();
10
+ try {
11
+ return await next(ctx);
12
+ } finally {
13
+ logger().debug("action", { action, ms: performance.now() - start });
14
+ }
15
+ };
16
+ },
17
+ started(broker) {
18
+ logger().info("broker up", { nodeID: broker.nodeID });
19
+ },
20
+ stopped() {
21
+ /* flush metrics, close connections */
22
+ },
23
+ };
24
+
25
+ export function withMiddleware(): Broker {
26
+ return new Broker({ middlewares: [timing, { name: "noop" }] });
27
+ }
@@ -0,0 +1,203 @@
1
+ // Type-check harness for docs/broker.md. Compile-only — never executed.
2
+ import {
3
+ Broker,
4
+ setBroker,
5
+ broker,
6
+ type Context,
7
+ type Transporter,
8
+ } from "@shaferllc/keel/core";
9
+
10
+ export function defineService() {
11
+ broker().createService({
12
+ name: "users",
13
+ settings: { defaultRole: "member" },
14
+ metadata: { region: "us-east" },
15
+ actions: {
16
+ async get(this: any, ctx: Context<{ id: number }>) {
17
+ return { id: ctx.params.id, role: this.settings.defaultRole };
18
+ },
19
+ async create(ctx: Context<{ email: string }>) {
20
+ const user = { id: 1, email: ctx.params.email };
21
+ await ctx.emit("user.created", user);
22
+ return user;
23
+ },
24
+ },
25
+ events: {
26
+ "user.created": (_ctx: Context) => {},
27
+ },
28
+ });
29
+ }
30
+
31
+ export async function calling() {
32
+ await broker().call("users.create", { email: "ada@keel.dev" });
33
+ await broker().call("users.get", { id: 1 }, { meta: { locale: "en" } });
34
+ await broker().call("reports.build", {}, { timeout: 5000 });
35
+
36
+ const [a, b] = await broker().mcall<[unknown, unknown]>([
37
+ { action: "users.get", params: { id: 1 } },
38
+ { action: "users.get", params: { id: 2 } },
39
+ ]);
40
+ const { profile, posts } = await broker().mcall<{ profile: unknown; posts: unknown }>({
41
+ profile: { action: "users.get", params: { id: 1 } },
42
+ posts: { action: "posts.byUser", params: { id: 1 } },
43
+ });
44
+ return { a, b, profile, posts };
45
+ }
46
+
47
+ export async function contextSlots() {
48
+ await broker().call("reports.build", {}, {
49
+ headers: { "x-trace": "abc" },
50
+ requestID: "req-42",
51
+ });
52
+ }
53
+
54
+ export function contextTree() {
55
+ broker().createService({
56
+ name: "orders",
57
+ actions: {
58
+ place(ctx: Context) {
59
+ void ctx.id;
60
+ void ctx.parentID;
61
+ void ctx.level;
62
+ void ctx.caller;
63
+ void ctx.action?.name;
64
+ void ctx.toJSON();
65
+ return ctx.call("orders.notify");
66
+ },
67
+ notify: (_ctx: Context) => {},
68
+ },
69
+ events: {
70
+ "order.placed": (ctx: Context) => {
71
+ void ctx.eventName;
72
+ void ctx.eventType;
73
+ void ctx.eventGroups;
74
+ },
75
+ },
76
+ });
77
+ }
78
+
79
+ export async function events() {
80
+ await broker().emit("user.created", { id: 1 });
81
+ await broker().emit("user.created", { id: 1 }, { groups: ["notify"] });
82
+ await broker().broadcast("cache.flush");
83
+ await broker().broadcastLocal("cache.warm");
84
+ broker().hasEventListener("user.created");
85
+
86
+ broker().createService({
87
+ name: "mailer",
88
+ events: { "user.created": { group: "notify", handler: (_ctx: Context) => {} } },
89
+ });
90
+
91
+ broker().createService({
92
+ name: "registry",
93
+ events: {
94
+ "user.??eated": (_ctx: Context) => {},
95
+ "$services.changed": (ctx: Context) => void (ctx.params as { service: string }).service,
96
+ "$broker.started": (_ctx: Context) => {},
97
+ "$broker.stopped": (_ctx: Context) => {},
98
+ },
99
+ });
100
+ }
101
+
102
+ export function fullActionDefs() {
103
+ broker().createService({
104
+ name: "billing",
105
+ actions: {
106
+ quote: (_ctx: Context) => ({ cents: 999 }),
107
+ charge: {
108
+ visibility: "private",
109
+ timeout: 3000,
110
+ hooks: {
111
+ before: (_ctx) => {},
112
+ after: (_ctx, res) => res,
113
+ },
114
+ handler: (ctx: Context<{ cents: number }>) => ctx.params.cents,
115
+ },
116
+ checkout(this: any, ctx: Context<{ cents: number }>) {
117
+ return this.actions.charge({ cents: ctx.params.cents });
118
+ },
119
+ },
120
+ });
121
+ }
122
+
123
+ export function serviceHooks() {
124
+ broker().createService({
125
+ name: "users",
126
+ hooks: {
127
+ before: {
128
+ "*": (_ctx) => {},
129
+ "create|update": (_ctx) => {},
130
+ remove: (_ctx) => {},
131
+ },
132
+ after: {
133
+ get: (_ctx, res: any) => ({ ...res, fetchedAt: Date.now() }),
134
+ },
135
+ error: {
136
+ "*": (_ctx, err) => {
137
+ throw err;
138
+ },
139
+ },
140
+ },
141
+ actions: {
142
+ get: (_ctx: Context) => ({}),
143
+ create: (_ctx: Context) => ({}),
144
+ update: (_ctx: Context) => ({}),
145
+ remove: (_ctx: Context) => ({}),
146
+ },
147
+ });
148
+ }
149
+
150
+ const Timestamps = {
151
+ name: "timestamps",
152
+ settings: { softDelete: false },
153
+ methods: {
154
+ touch(this: any) {},
155
+ },
156
+ };
157
+
158
+ export function mixins() {
159
+ broker().createService({
160
+ mixins: [Timestamps],
161
+ name: "articles",
162
+ settings: { perPage: 10 },
163
+ actions: { list: () => [] },
164
+ merged(schema) {
165
+ void schema.name;
166
+ },
167
+ });
168
+ }
169
+
170
+ export function dependencies() {
171
+ broker().createService({
172
+ name: "api",
173
+ dependencies: ["db", "cache"],
174
+ async started(this: any) {
175
+ await this.waitForServices("mailer", 5000);
176
+ },
177
+ });
178
+ }
179
+
180
+ export async function lifecycleAndCluster() {
181
+ const b = broker();
182
+ b.createService({
183
+ name: "clock",
184
+ async started(this: any) {
185
+ this.timer = setInterval(() => this.broker.broadcast("tick"), 1000);
186
+ },
187
+ async stopped(this: any) {
188
+ clearInterval(this.timer);
189
+ },
190
+ });
191
+ await b.start();
192
+ await b.stop();
193
+
194
+ await broker().emit("user.created", { id: 1 });
195
+ await broker().broadcast("cache.flush");
196
+ broker().hasEventListener("user.created");
197
+
198
+ const nats: Transporter = {
199
+ async connect(_broker) {},
200
+ async disconnect() {},
201
+ };
202
+ setBroker(new Broker({ nodeID: "api-1", transporter: nats, requestTimeout: 10_000 }));
203
+ }
@@ -0,0 +1,222 @@
1
+ // Type-check harness for docs/cache.md. Every snippet in the reference is
2
+ // exercised here against the real exports, so a renamed method or wrong argument
3
+ // type fails `npm run typecheck:docs`. Compile-only — never executed.
4
+ import {
5
+ cache,
6
+ Cache,
7
+ MemoryStore,
8
+ singleton,
9
+ type CacheStore,
10
+ type RememberOptions,
11
+ type PutOptions,
12
+ } from "@shaferllc/keel/core";
13
+ import { db } from "@shaferllc/keel/core";
14
+
15
+ type User = { id: number; name: string };
16
+
17
+ declare const user: User;
18
+ declare const code: string;
19
+ declare const cfg: unknown;
20
+ declare const config: unknown;
21
+ declare const id: number;
22
+ declare const name: string;
23
+ declare const fallback: string;
24
+
25
+ declare function computeExpensiveStats(): Promise<{ total: number }>;
26
+ declare function loadConfig(): { url: string };
27
+ declare function fetchRates(): Promise<Record<string, number>>;
28
+ declare function runJobOnce(): Promise<void>;
29
+ declare function warmProfile(id: number): Promise<void>;
30
+ declare const requests: unknown[];
31
+ declare function runExpensiveReport(): Promise<{ rows: number }>;
32
+
33
+ export async function basics() {
34
+ await cache().put("user:1", user);
35
+ await cache().put("otp", code, 300);
36
+ await cache().add("otp", code, 300);
37
+ await cache().get("user:1");
38
+ await cache().get("missing", fallback);
39
+ await cache().has("otp");
40
+ await cache().missing("otp");
41
+ await cache().forget("otp");
42
+ await cache().forgetMany(["otp", "user:1"]);
43
+ await cache().pull("otp");
44
+ await cache().flush();
45
+ }
46
+
47
+ export async function stampedeAndGrace() {
48
+ await Promise.all(
49
+ requests.map(() => cache().remember("report", 300, runExpensiveReport)),
50
+ );
51
+
52
+ const rates = await cache().remember("fx.rates", 60, fetchRates, { grace: 3600 });
53
+ return rates;
54
+ }
55
+
56
+ export async function newMethods() {
57
+ if (await cache().add("job:lock", 1, 30)) {
58
+ await runJobOnce();
59
+ }
60
+ if (await cache().missing("profile:1")) await warmProfile(1);
61
+ await cache().forgetMany(["user:1", "user:1:posts", "user:1:stats"]);
62
+ }
63
+
64
+ export async function tags() {
65
+ await cache().put("post:1", user, 600, { tags: ["posts"] });
66
+ await cache().remember("feed:home", 300, computeExpensiveStats, { tags: ["posts"] });
67
+ await cache().put("post:2", user, 600, { tags: ["posts", "featured"] });
68
+ await cache().deleteByTag(["posts"]);
69
+ }
70
+
71
+ export async function namespaces() {
72
+ const users = cache().namespace("users");
73
+ const posts = cache().namespace("posts");
74
+ await users.put("1", user); // "users:1"
75
+ await posts.put("1", user); // "posts:1"
76
+ await users.flush(); // scoped
77
+ await posts.get("1");
78
+
79
+ // Nested, with the full API.
80
+ const team = cache().namespace("org").namespace("team");
81
+ await team.remember("count", 60, () => 1, { grace: 30, tags: ["team"] });
82
+ await team.flush();
83
+ }
84
+
85
+ // Type seams for the options bags.
86
+ const graceOpts: RememberOptions = { grace: 3600, tags: ["posts"] };
87
+ const putOpts: PutOptions = { tags: ["posts"] };
88
+ export { graceOpts, putOpts };
89
+
90
+ export async function remember() {
91
+ const stats = await cache().remember("dashboard.stats", 60, async () => {
92
+ return computeExpensiveStats();
93
+ });
94
+ const conf = await cache().rememberForever("app.config", () => loadConfig());
95
+ return { stats, conf };
96
+ }
97
+
98
+ export async function readThroughThenInvalidate() {
99
+ const token = await cache().pull<string>("reset:jane", "");
100
+
101
+ await db("users").where("id", id).update({ name });
102
+ await cache().forget(`user:${id}`);
103
+ return token;
104
+ }
105
+
106
+ export async function ttls() {
107
+ await cache().put("otp", code, 300);
108
+ await cache().put("app.config", cfg);
109
+ }
110
+
111
+ // --- Custom store (narrative) ---
112
+ class RedisStore implements CacheStore {
113
+ async get(key: string) {
114
+ return key;
115
+ }
116
+ async set(key: string, value: unknown, ttlMs?: number) {
117
+ void key;
118
+ void value;
119
+ void ttlMs;
120
+ }
121
+ async delete(key: string) {
122
+ void key;
123
+ }
124
+ async clear() {}
125
+ }
126
+
127
+ export function customStore() {
128
+ singleton(Cache, () => new Cache(new RedisStore()));
129
+ }
130
+
131
+ // --- API reference: cache() / Cache construction ---
132
+ export async function cacheHelper() {
133
+ await cache().put("user:1", user);
134
+ }
135
+
136
+ export function construction() {
137
+ const c = new Cache();
138
+ const r = new Cache(new MemoryStore());
139
+ return { c, r };
140
+ }
141
+
142
+ export async function cacheGet() {
143
+ const u = await cache().get<User>("user:1");
144
+ const port = await cache().get("app.port", 3000);
145
+ return { u, port };
146
+ }
147
+
148
+ export async function cachePut() {
149
+ await cache().put("otp", code, 300);
150
+ await cache().put("user:1", user);
151
+ }
152
+
153
+ export async function cacheHas() {
154
+ if (await cache().has("otp")) {
155
+ /* still valid */
156
+ }
157
+ }
158
+
159
+ export async function cacheForget() {
160
+ await cache().forget("user:1");
161
+ }
162
+
163
+ export async function cachePull() {
164
+ const token = await cache().pull<string>("reset:jane", "");
165
+ return token;
166
+ }
167
+
168
+ export async function cacheFlush() {
169
+ await cache().flush();
170
+ }
171
+
172
+ export async function cacheRemember() {
173
+ const stats = await cache().remember("dashboard.stats", 60, () =>
174
+ computeExpensiveStats(),
175
+ );
176
+ return stats;
177
+ }
178
+
179
+ export async function cacheRememberForever() {
180
+ const conf = await cache().rememberForever("app.config", () => loadConfig());
181
+ return conf;
182
+ }
183
+
184
+ // --- MemoryStore methods ---
185
+ export function memoryStore() {
186
+ const store = new MemoryStore();
187
+ store.set("k", 1, 1000);
188
+ const v = store.get("k");
189
+ store.set("otp", code, 300_000);
190
+ store.set("cfg", config);
191
+ store.delete("otp");
192
+ store.clear();
193
+ return v;
194
+ }
195
+
196
+ // --- Interfaces & types: CacheStore (KV example) ---
197
+ type KV = {
198
+ get(key: string): Promise<string | null>;
199
+ put(key: string, value: string, ttlMs?: number): Promise<void>;
200
+ delete(key: string): Promise<void>;
201
+ };
202
+ declare const kv: KV;
203
+
204
+ class KVStore implements CacheStore {
205
+ constructor(private kv: KV) {}
206
+ async get(key: string) {
207
+ return (await this.kv.get(key)) ?? undefined;
208
+ }
209
+ async set(key: string, value: unknown, ttlMs?: number) {
210
+ await this.kv.put(key, JSON.stringify(value), ttlMs);
211
+ }
212
+ async delete(key: string) {
213
+ await this.kv.delete(key);
214
+ }
215
+ async clear() {
216
+ /* KV has no bulk clear — list + delete, or skip */
217
+ }
218
+ }
219
+
220
+ export function kvStore() {
221
+ singleton(Cache, () => new Cache(new KVStore(kv)));
222
+ }
@@ -0,0 +1,81 @@
1
+ // Type-check harness for docs/configuration.md. Every type-checkable snippet in
2
+ // the guide is exercised here against the real exports, so a renamed method or
3
+ // wrong argument type fails `npm run typecheck:docs`. Compile-only — never run.
4
+ import {
5
+ env,
6
+ config,
7
+ app,
8
+ Config,
9
+ type ConfigData,
10
+ } from "@shaferllc/keel/core";
11
+
12
+ // A stand-in for a user's service, used only for `app().make(...)`.
13
+ class SomeService {}
14
+
15
+ export function envHelper() {
16
+ const name = env("APP_NAME"); // string
17
+ const debug = env("APP_DEBUG", false); // boolean
18
+ const port = env("APP_PORT", 3000); // number
19
+ const fallback = env("MISSING", "default"); // string
20
+
21
+ const portStr = env("APP_PORT"); // string (no numeric fallback)
22
+ const portNum = env("APP_PORT", 0); // number
23
+ const debugBool = env("APP_DEBUG"); // string (declared) / boolean (runtime)
24
+
25
+ return { name, debug, port, fallback, portStr, portNum, debugBool };
26
+ }
27
+
28
+ // config/app.ts — a config file's default export.
29
+ export function configFile() {
30
+ return {
31
+ name: env("APP_NAME", "Keel"),
32
+ env: env("APP_ENV", "local"),
33
+ debug: env("APP_DEBUG", true),
34
+ url: env("APP_URL", "http://localhost:3000"),
35
+ port: env("APP_PORT", 3000),
36
+ };
37
+ }
38
+
39
+ export function readingConfig() {
40
+ const a = config("app.name"); // unknown
41
+ const b = config("app.port", 3000); // number
42
+ const c = config("services.stripe.key"); // unknown
43
+ const d = config<number>("app.port", 3000);
44
+ const e = config("services.stripe.key", ""); // string
45
+ const f = config("nope.at.all"); // unknown | undefined
46
+ return { a, b, c, d, e, f };
47
+ }
48
+
49
+ export function containerHelper() {
50
+ app().make(SomeService);
51
+ app().config().get("app.name");
52
+ }
53
+
54
+ export function longForm() {
55
+ const cfg = app().make(Config);
56
+
57
+ cfg.get("app.name");
58
+ cfg.get("app.port", 3000);
59
+ cfg.get("services.stripe.key");
60
+ cfg.get<string>("services.stripe.key");
61
+ cfg.set("app.debug", false);
62
+ cfg.all();
63
+
64
+ if (cfg.get("services.stripe.key") !== undefined) {
65
+ // configured
66
+ }
67
+ }
68
+
69
+ export function configClass() {
70
+ const repo = new Config({ app: { name: "Keel", port: 3000 } });
71
+ const port = repo.get("app.port"); // unknown
72
+ repo.set("services.stripe.key", "sk_test_…"); // creates `services`
73
+ const tree: ConfigData = repo.all();
74
+ return { port, tree };
75
+ }
76
+
77
+ // Interface / type seams
78
+ const data: ConfigData = {
79
+ app: { name: "Keel", port: 3000 },
80
+ };
81
+ export { data };