cooper-stack 0.5.2

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 (87) hide show
  1. package/dist/ai.d.ts +60 -0
  2. package/dist/ai.d.ts.map +1 -0
  3. package/dist/ai.js +66 -0
  4. package/dist/ai.js.map +1 -0
  5. package/dist/api.d.ts +31 -0
  6. package/dist/api.d.ts.map +1 -0
  7. package/dist/api.js +40 -0
  8. package/dist/api.js.map +1 -0
  9. package/dist/auth.d.ts +13 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +16 -0
  12. package/dist/auth.js.map +1 -0
  13. package/dist/bridge.d.ts +15 -0
  14. package/dist/bridge.d.ts.map +1 -0
  15. package/dist/bridge.js +217 -0
  16. package/dist/bridge.js.map +1 -0
  17. package/dist/cache.d.ts +27 -0
  18. package/dist/cache.d.ts.map +1 -0
  19. package/dist/cache.js +69 -0
  20. package/dist/cache.js.map +1 -0
  21. package/dist/cron.d.ts +22 -0
  22. package/dist/cron.d.ts.map +1 -0
  23. package/dist/cron.js +26 -0
  24. package/dist/cron.js.map +1 -0
  25. package/dist/db.d.ts +30 -0
  26. package/dist/db.d.ts.map +1 -0
  27. package/dist/db.js +75 -0
  28. package/dist/db.js.map +1 -0
  29. package/dist/error.d.ts +16 -0
  30. package/dist/error.d.ts.map +1 -0
  31. package/dist/error.js +28 -0
  32. package/dist/error.js.map +1 -0
  33. package/dist/index.d.ts +16 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +16 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/islands.d.ts +16 -0
  38. package/dist/islands.d.ts.map +1 -0
  39. package/dist/islands.js +23 -0
  40. package/dist/islands.js.map +1 -0
  41. package/dist/middleware.d.ts +20 -0
  42. package/dist/middleware.d.ts.map +1 -0
  43. package/dist/middleware.js +26 -0
  44. package/dist/middleware.js.map +1 -0
  45. package/dist/pubsub.d.ts +24 -0
  46. package/dist/pubsub.d.ts.map +1 -0
  47. package/dist/pubsub.js +48 -0
  48. package/dist/pubsub.js.map +1 -0
  49. package/dist/queue.d.ts +36 -0
  50. package/dist/queue.d.ts.map +1 -0
  51. package/dist/queue.js +100 -0
  52. package/dist/queue.js.map +1 -0
  53. package/dist/registry.d.ts +75 -0
  54. package/dist/registry.d.ts.map +1 -0
  55. package/dist/registry.js +41 -0
  56. package/dist/registry.js.map +1 -0
  57. package/dist/secrets.d.ts +10 -0
  58. package/dist/secrets.d.ts.map +1 -0
  59. package/dist/secrets.js +35 -0
  60. package/dist/secrets.js.map +1 -0
  61. package/dist/ssr.d.ts +53 -0
  62. package/dist/ssr.d.ts.map +1 -0
  63. package/dist/ssr.js +39 -0
  64. package/dist/ssr.js.map +1 -0
  65. package/dist/storage.d.ts +28 -0
  66. package/dist/storage.d.ts.map +1 -0
  67. package/dist/storage.js +61 -0
  68. package/dist/storage.js.map +1 -0
  69. package/package.json +38 -0
  70. package/src/ai.ts +99 -0
  71. package/src/api.ts +56 -0
  72. package/src/auth.ts +16 -0
  73. package/src/bridge.ts +267 -0
  74. package/src/cache.ts +86 -0
  75. package/src/cron.ts +32 -0
  76. package/src/db.ts +109 -0
  77. package/src/error.ts +42 -0
  78. package/src/index.ts +15 -0
  79. package/src/islands.ts +28 -0
  80. package/src/middleware.ts +27 -0
  81. package/src/pubsub.ts +69 -0
  82. package/src/queue.ts +133 -0
  83. package/src/registry.ts +98 -0
  84. package/src/secrets.ts +40 -0
  85. package/src/ssr.ts +58 -0
  86. package/src/storage.ts +79 -0
  87. package/tsconfig.json +17 -0
package/src/bridge.ts ADDED
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cooper JS Worker Bridge
4
+ *
5
+ * Spawned by the Rust runtime as a subprocess. Communicates via
6
+ * JSON-RPC over stdin/stdout. Loads user TypeScript modules and
7
+ * executes handler functions on demand.
8
+ *
9
+ * Protocol:
10
+ * → { "id": 1, "method": "call", "params": { "source": "services/users/api.ts", "export": "getUser", "input": {...}, "auth": {...} } }
11
+ * ← { "id": 1, "result": {...} }
12
+ * ← { "id": 1, "error": { "code": "NOT_FOUND", "message": "User not found" } }
13
+ */
14
+
15
+ import { registry } from "./registry.js";
16
+ import { CooperError } from "./error.js";
17
+ import { createRequire } from "node:module";
18
+ import { createInterface } from "node:readline";
19
+ import path from "node:path";
20
+ import { pathToFileURL } from "node:url";
21
+
22
+ const projectRoot = process.env.COOPER_PROJECT_ROOT ?? process.cwd();
23
+
24
+ // Module cache — avoid re-importing on every request
25
+ const moduleCache = new Map<string, any>();
26
+
27
+ async function loadModule(sourcePath: string): Promise<any> {
28
+ if (moduleCache.has(sourcePath)) return moduleCache.get(sourcePath)!;
29
+
30
+ const fullPath = path.resolve(projectRoot, sourcePath);
31
+ const fileUrl = pathToFileURL(fullPath).href;
32
+
33
+ try {
34
+ const mod = await import(fileUrl);
35
+ moduleCache.set(sourcePath, mod);
36
+ return mod;
37
+ } catch (err: any) {
38
+ throw new Error(`Failed to load module "${sourcePath}": ${err.message}`);
39
+ }
40
+ }
41
+
42
+ interface RPCRequest {
43
+ id: number;
44
+ method: string;
45
+ params: any;
46
+ }
47
+
48
+ interface RPCResponse {
49
+ id: number;
50
+ result?: any;
51
+ error?: { code: string; message: string; statusCode?: number };
52
+ }
53
+
54
+ async function handleCall(params: {
55
+ source: string;
56
+ export: string;
57
+ input: any;
58
+ auth?: any;
59
+ headers?: Record<string, string>;
60
+ }): Promise<any> {
61
+ const mod = await loadModule(params.source);
62
+ const exported = mod[params.export];
63
+
64
+ if (!exported) {
65
+ throw new CooperError("NOT_FOUND", `Export "${params.export}" not found in ${params.source}`);
66
+ }
67
+
68
+ // Get the handler function
69
+ let handler: Function;
70
+ let routeConfig: any = null;
71
+
72
+ if (exported._cooper_type === "api") {
73
+ handler = exported.handler;
74
+ routeConfig = exported.config;
75
+ } else if (typeof exported === "function") {
76
+ handler = exported;
77
+ } else {
78
+ throw new CooperError("INTERNAL", `Export "${params.export}" is not callable`);
79
+ }
80
+
81
+ // Validation — run Zod schema if present.
82
+ // Use passthrough() to preserve path params that aren't in the schema.
83
+ let validatedInput = params.input;
84
+ if (routeConfig?.validate) {
85
+ const schema = routeConfig.validate;
86
+ const lenient = typeof schema.passthrough === "function" ? schema.passthrough() : schema;
87
+ const result = lenient.safeParse(params.input);
88
+ if (!result.success) {
89
+ throw new CooperError(
90
+ "VALIDATION_FAILED",
91
+ result.error.issues.map((i: any) => `${i.path.join(".")}: ${i.message}`).join("; ")
92
+ );
93
+ }
94
+ validatedInput = result.data;
95
+ }
96
+
97
+ // Auth — verify token and inject principal
98
+ let principal: any = undefined;
99
+ if (routeConfig?.auth) {
100
+ if (!params.auth?.token) {
101
+ throw new CooperError("UNAUTHORIZED", "Authentication required");
102
+ }
103
+ if (registry.authHandler) {
104
+ principal = await registry.authHandler(params.auth.token);
105
+ } else {
106
+ throw new CooperError("INTERNAL", "No auth handler registered");
107
+ }
108
+ }
109
+
110
+ // Middleware chain
111
+ const middlewares = [
112
+ ...registry.globalMiddleware,
113
+ ...(routeConfig?.middleware ?? []),
114
+ ];
115
+
116
+ const req = {
117
+ ...validatedInput,
118
+ headers: params.headers ?? {},
119
+ ip: params.headers?.["x-forwarded-for"] ?? "127.0.0.1",
120
+ method: routeConfig?.method ?? "GET",
121
+ path: routeConfig?.path ?? "/",
122
+ };
123
+
124
+ // Build the middleware chain
125
+ let idx = 0;
126
+ const runMiddleware = async (currentReq: any): Promise<any> => {
127
+ if (idx < middlewares.length) {
128
+ const mw = middlewares[idx++];
129
+ return mw(currentReq, runMiddleware);
130
+ }
131
+ // End of chain — call the actual handler
132
+ if (routeConfig?.auth) {
133
+ return handler(validatedInput, principal);
134
+ }
135
+ return handler(validatedInput);
136
+ };
137
+
138
+ return runMiddleware(req);
139
+ }
140
+
141
+ async function handleCron(params: { source: string; export: string }): Promise<any> {
142
+ const mod = await loadModule(params.source);
143
+ const exported = mod[params.export];
144
+
145
+ if (!exported) {
146
+ throw new CooperError("NOT_FOUND", `Cron "${params.export}" not found in ${params.source}`);
147
+ }
148
+
149
+ if (exported._cooper_type === "cron") {
150
+ // The cron was registered via the SDK, find it in the registry
151
+ const cronEntry = registry.crons.get(exported.name);
152
+ if (cronEntry) {
153
+ await cronEntry.handler();
154
+ return { ok: true };
155
+ }
156
+ }
157
+
158
+ throw new CooperError("INTERNAL", `Cannot execute cron "${params.export}"`);
159
+ }
160
+
161
+ async function handlePubSub(params: {
162
+ topic: string;
163
+ subscriber: string;
164
+ data: any;
165
+ }): Promise<any> {
166
+ const topicEntry = registry.topics.get(params.topic);
167
+ if (!topicEntry) {
168
+ throw new CooperError("NOT_FOUND", `Topic "${params.topic}" not registered`);
169
+ }
170
+
171
+ const sub = topicEntry.subscribers.get(params.subscriber);
172
+ if (!sub) {
173
+ throw new CooperError("NOT_FOUND", `Subscriber "${params.subscriber}" not found on topic "${params.topic}"`);
174
+ }
175
+
176
+ await sub.handler(params.data);
177
+ return { ok: true };
178
+ }
179
+
180
+ async function handleRequest(req: RPCRequest): Promise<RPCResponse> {
181
+ try {
182
+ let result: any;
183
+
184
+ switch (req.method) {
185
+ case "call":
186
+ result = await handleCall(req.params);
187
+ break;
188
+ case "cron":
189
+ result = await handleCron(req.params);
190
+ break;
191
+ case "pubsub":
192
+ result = await handlePubSub(req.params);
193
+ break;
194
+ case "ping":
195
+ result = { pong: true, pid: process.pid };
196
+ break;
197
+ case "invalidate":
198
+ // Clear module cache for hot reload
199
+ moduleCache.clear();
200
+ result = { ok: true };
201
+ break;
202
+ default:
203
+ throw new CooperError("INVALID_ARGUMENT", `Unknown method: ${req.method}`);
204
+ }
205
+
206
+ return { id: req.id, result };
207
+ } catch (err: any) {
208
+ if (err instanceof CooperError) {
209
+ return {
210
+ id: req.id,
211
+ error: { code: err.code, message: err.message, statusCode: err.statusCode },
212
+ };
213
+ }
214
+ return {
215
+ id: req.id,
216
+ error: { code: "INTERNAL", message: err.message ?? "Unknown error", statusCode: 500 },
217
+ };
218
+ }
219
+ }
220
+
221
+ // Main loop — read JSON lines from stdin, write responses to stdout
222
+ const rl = createInterface({ input: process.stdin });
223
+
224
+ rl.on("line", async (line) => {
225
+ if (!line.trim()) return;
226
+
227
+ try {
228
+ const req: RPCRequest = JSON.parse(line);
229
+ const res = await handleRequest(req);
230
+ process.stdout.write(JSON.stringify(res) + "\n");
231
+ } catch (err: any) {
232
+ process.stdout.write(
233
+ JSON.stringify({ id: 0, error: { code: "INTERNAL", message: `Bridge parse error: ${err.message}` } }) + "\n"
234
+ );
235
+ }
236
+ });
237
+
238
+ // Preload auth handlers and middleware — these register via side effects
239
+ async function preloadSideEffects() {
240
+ const fs = await import("node:fs");
241
+ const servicesDir = path.join(projectRoot, "services");
242
+ if (!fs.existsSync(servicesDir)) return;
243
+
244
+ const scanDir = (dir: string) => {
245
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
246
+ const fullPath = path.join(dir, entry.name);
247
+ if (entry.isDirectory()) {
248
+ scanDir(fullPath);
249
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
250
+ try {
251
+ const content = fs.readFileSync(fullPath, "utf-8");
252
+ if (content.includes("authHandler") || content.includes("middleware(")) {
253
+ const relative = path.relative(projectRoot, fullPath);
254
+ loadModule(relative).catch(() => {});
255
+ }
256
+ } catch {}
257
+ }
258
+ }
259
+ };
260
+
261
+ scanDir(servicesDir);
262
+ }
263
+
264
+ await preloadSideEffects();
265
+
266
+ // Signal ready
267
+ process.stdout.write(JSON.stringify({ id: 0, result: { ready: true, pid: process.pid } }) + "\n");
package/src/cache.ts ADDED
@@ -0,0 +1,86 @@
1
+ export interface CacheConfig {
2
+ ttl?: string; // e.g. "10m", "1h", "30s"
3
+ }
4
+
5
+ export interface CacheClient<T> {
6
+ get(key: string): Promise<T | null>;
7
+ set(key: string, value: T, opts?: { ttl?: string }): Promise<void>;
8
+ getOrSet(key: string, factory: () => Promise<T>, opts?: { ttl?: string }): Promise<T>;
9
+ delete(key: string): Promise<void>;
10
+ invalidatePrefix(prefix: string): Promise<void>;
11
+ increment(key: string, opts?: { ttl?: string }): Promise<number>;
12
+ }
13
+
14
+ function parseTTL(ttl: string): number {
15
+ const match = ttl.match(/^(\d+)(s|m|h|d)$/);
16
+ if (!match) return 600;
17
+ const [, num, unit] = match;
18
+ const multipliers: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400 };
19
+ return parseInt(num) * (multipliers[unit] ?? 60);
20
+ }
21
+
22
+ /**
23
+ * Declare a typed cache.
24
+ *
25
+ * ```ts
26
+ * export const userCache = cache<User>("users", { ttl: "10m" });
27
+ * const user = await userCache.getOrSet(userId, () => db.queryRow(...));
28
+ * ```
29
+ */
30
+ export function cache<T = any>(name: string, config?: CacheConfig): CacheClient<T> {
31
+ const defaultTTL = config?.ttl ?? "10m";
32
+ let redis: any = null;
33
+
34
+ const ensureRedis = async () => {
35
+ if (redis) return redis;
36
+ const Redis = (await import("ioredis")).default;
37
+ const url = process.env.COOPER_VALKEY_URL ?? "redis://localhost:6379";
38
+ redis = new Redis(url);
39
+ return redis;
40
+ };
41
+
42
+ const prefixed = (key: string) => `cooper:${name}:${key}`;
43
+
44
+ return {
45
+ async get(key: string): Promise<T | null> {
46
+ const r = await ensureRedis();
47
+ const val = await r.get(prefixed(key));
48
+ return val ? JSON.parse(val) : null;
49
+ },
50
+
51
+ async set(key: string, value: T, opts?: { ttl?: string }): Promise<void> {
52
+ const r = await ensureRedis();
53
+ const ttlSec = parseTTL(opts?.ttl ?? defaultTTL);
54
+ await r.setex(prefixed(key), ttlSec, JSON.stringify(value));
55
+ },
56
+
57
+ async getOrSet(key: string, factory: () => Promise<T>, opts?: { ttl?: string }): Promise<T> {
58
+ const existing = await this.get(key);
59
+ if (existing !== null) return existing;
60
+ const value = await factory();
61
+ await this.set(key, value, opts);
62
+ return value;
63
+ },
64
+
65
+ async delete(key: string): Promise<void> {
66
+ const r = await ensureRedis();
67
+ await r.del(prefixed(key));
68
+ },
69
+
70
+ async invalidatePrefix(prefix: string): Promise<void> {
71
+ const r = await ensureRedis();
72
+ const keys = await r.keys(prefixed(prefix) + "*");
73
+ if (keys.length > 0) await r.del(...keys);
74
+ },
75
+
76
+ async increment(key: string, opts?: { ttl?: string }): Promise<number> {
77
+ const r = await ensureRedis();
78
+ const k = prefixed(key);
79
+ const val = await r.incr(k);
80
+ if (val === 1 && opts?.ttl) {
81
+ await r.expire(k, parseTTL(opts.ttl));
82
+ }
83
+ return val;
84
+ },
85
+ };
86
+ }
package/src/cron.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { registry } from "./registry.js";
2
+
3
+ export interface CronConfig {
4
+ schedule: string;
5
+ handler: () => Promise<void>;
6
+ }
7
+
8
+ /**
9
+ * Declare a cron job.
10
+ *
11
+ * ```ts
12
+ * export const cleanup = cron("cleanup", {
13
+ * schedule: "every 1 hour",
14
+ * handler: async () => {
15
+ * await db.query("DELETE FROM sessions WHERE expires_at < NOW()");
16
+ * },
17
+ * });
18
+ * ```
19
+ */
20
+ export function cron(name: string, config: CronConfig) {
21
+ registry.registerCron(name, {
22
+ name,
23
+ schedule: config.schedule,
24
+ handler: config.handler,
25
+ });
26
+
27
+ return {
28
+ _cooper_type: "cron" as const,
29
+ name,
30
+ schedule: config.schedule,
31
+ };
32
+ }
package/src/db.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { registry } from "./registry.js";
2
+
3
+ export interface DatabaseConfig {
4
+ engine?: "postgres" | "mysql" | "mongodb" | "dynamodb";
5
+ migrations?: string;
6
+ partitionKey?: string;
7
+ sortKey?: string;
8
+ }
9
+
10
+ export interface DatabaseClient {
11
+ /** Run a query returning multiple rows */
12
+ query<T = any>(sql: string, params?: any[]): Promise<T[]>;
13
+ /** Run a query returning a single row or null */
14
+ queryRow<T = any>(sql: string, params?: any[]): Promise<T | null>;
15
+ /** Run a query returning affected row count */
16
+ exec(sql: string, params?: any[]): Promise<{ rowCount: number }>;
17
+ /** Insert and return the inserted row */
18
+ insert<T = any>(table: string, data: Record<string, any>): Promise<T>;
19
+ /** Access the underlying connection pool (for ORMs like Drizzle) */
20
+ pool: any;
21
+ }
22
+
23
+ /**
24
+ * Declare a database.
25
+ *
26
+ * ```ts
27
+ * export const db = database("main", { migrations: "./migrations" });
28
+ * const user = await db.queryRow<User>("SELECT * FROM users WHERE id = $1", [id]);
29
+ * ```
30
+ */
31
+ export function database(name: string, config?: DatabaseConfig): DatabaseClient {
32
+ const engine = config?.engine ?? "postgres";
33
+
34
+ // Connection details injected by the Rust runtime via env vars
35
+ const connStr = process.env[`COOPER_DB_${name.toUpperCase()}_URL`]
36
+ ?? `postgres://cooper:cooper@localhost:5432/cooper_${name}`;
37
+
38
+ let pool: any = null;
39
+
40
+ const ensurePool = async () => {
41
+ if (pool) return pool;
42
+
43
+ if (engine === "postgres") {
44
+ const pg = await import("pg");
45
+ pool = new pg.default.Pool({ connectionString: connStr });
46
+ return pool;
47
+ }
48
+
49
+ if (engine === "mysql") {
50
+ const mysql = await import("mysql2/promise");
51
+ pool = await mysql.createPool(connStr);
52
+ return pool;
53
+ }
54
+
55
+ throw new Error(`Database engine "${engine}" not yet supported in JS runtime`);
56
+ };
57
+
58
+ const client: DatabaseClient = {
59
+ async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
60
+ const p = await ensurePool();
61
+ if (engine === "postgres") {
62
+ const res = await p.query(sql, params);
63
+ return res.rows as T[];
64
+ }
65
+ if (engine === "mysql") {
66
+ const [rows] = await p.execute(sql, params);
67
+ return rows as T[];
68
+ }
69
+ return [];
70
+ },
71
+
72
+ async queryRow<T = any>(sql: string, params?: any[]): Promise<T | null> {
73
+ const rows = await client.query<T>(sql, params);
74
+ return rows[0] ?? null;
75
+ },
76
+
77
+ async exec(sql: string, params?: any[]): Promise<{ rowCount: number }> {
78
+ const p = await ensurePool();
79
+ if (engine === "postgres") {
80
+ const res = await p.query(sql, params);
81
+ return { rowCount: res.rowCount ?? 0 };
82
+ }
83
+ if (engine === "mysql") {
84
+ const [result] = await p.execute(sql, params);
85
+ return { rowCount: (result as any).affectedRows ?? 0 };
86
+ }
87
+ return { rowCount: 0 };
88
+ },
89
+
90
+ async insert<T = any>(table: string, data: Record<string, any>): Promise<T> {
91
+ const keys = Object.keys(data);
92
+ const values = Object.values(data);
93
+ const placeholders = keys.map((_, i) =>
94
+ engine === "postgres" ? `$${i + 1}` : "?"
95
+ );
96
+ const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders.join(", ")}) RETURNING *`;
97
+ const row = await client.queryRow<T>(sql, values);
98
+ return row!;
99
+ },
100
+
101
+ get pool() {
102
+ return pool;
103
+ },
104
+ };
105
+
106
+ registry.registerDatabase(name, { name, engine, pool: client });
107
+
108
+ return client;
109
+ }
package/src/error.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Structured error codes with automatic HTTP status mapping.
3
+ */
4
+ export type ErrorCode =
5
+ | "NOT_FOUND"
6
+ | "UNAUTHORIZED"
7
+ | "PERMISSION_DENIED"
8
+ | "RATE_LIMITED"
9
+ | "INVALID_ARGUMENT"
10
+ | "VALIDATION_FAILED"
11
+ | "INTERNAL";
12
+
13
+ const STATUS_MAP: Record<ErrorCode, number> = {
14
+ NOT_FOUND: 404,
15
+ UNAUTHORIZED: 401,
16
+ PERMISSION_DENIED: 403,
17
+ RATE_LIMITED: 429,
18
+ INVALID_ARGUMENT: 400,
19
+ VALIDATION_FAILED: 422,
20
+ INTERNAL: 500,
21
+ };
22
+
23
+ export class CooperError extends Error {
24
+ public readonly code: ErrorCode;
25
+ public readonly statusCode: number;
26
+
27
+ constructor(code: ErrorCode, message: string) {
28
+ super(message);
29
+ this.name = "CooperError";
30
+ this.code = code;
31
+ this.statusCode = STATUS_MAP[code] ?? 500;
32
+ }
33
+
34
+ toJSON() {
35
+ return {
36
+ error: {
37
+ code: this.code,
38
+ message: this.message,
39
+ },
40
+ };
41
+ }
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { api } from "./api.js";
2
+ export { CooperError, type ErrorCode } from "./error.js";
3
+ export { database, type DatabaseClient } from "./db.js";
4
+ export { middleware, cooper } from "./middleware.js";
5
+ export { authHandler } from "./auth.js";
6
+ export { topic, type Topic } from "./pubsub.js";
7
+ export { cron } from "./cron.js";
8
+ export { cache, type CacheClient } from "./cache.js";
9
+ export { bucket, type BucketClient } from "./storage.js";
10
+ export { secret } from "./secrets.js";
11
+ export { queue, type QueueClient } from "./queue.js";
12
+ export { page, layout, pageLoader, Suspense } from "./ssr.js";
13
+ export { island } from "./islands.js";
14
+ export { vectorStore, llmGateway } from "./ai.js";
15
+ export { registry } from "./registry.js";
package/src/islands.ts ADDED
@@ -0,0 +1,28 @@
1
+ export type HydrationStrategy = "load" | "visible" | "idle" | "interaction" | "none";
2
+
3
+ /**
4
+ * Mark a component as an island — it will be hydrated on the client.
5
+ *
6
+ * ```tsx
7
+ * // islands/LikeButton.island.tsx
8
+ * export default island(function LikeButton({ userId, initialCount }) {
9
+ * const [count, setCount] = useState(initialCount);
10
+ * return <button onClick={...}>Like ({count})</button>;
11
+ * });
12
+ * ```
13
+ */
14
+ export function island<P = any>(
15
+ component: (props: P) => any
16
+ ): (props: P & { hydrate?: HydrationStrategy }) => any {
17
+ // Mark the component for the bundler
18
+ const wrapper = (props: P & { hydrate?: HydrationStrategy }) => {
19
+ // Server-side: render the component to HTML
20
+ // Client-side: hydrate based on strategy
21
+ return component(props);
22
+ };
23
+
24
+ (wrapper as any)._cooper_island = true;
25
+ (wrapper as any)._cooper_hydrate = "load"; // default strategy
26
+
27
+ return wrapper;
28
+ }
@@ -0,0 +1,27 @@
1
+ import { registry, type MiddlewareFn } from "./registry.js";
2
+
3
+ /**
4
+ * Define a middleware function.
5
+ *
6
+ * ```ts
7
+ * const rateLimiter = middleware(async (req, next) => {
8
+ * const count = await userCache.increment(`rate:${req.ip}`, { ttl: "1m" });
9
+ * if (count > 100) throw new CooperError("RATE_LIMITED");
10
+ * return next(req);
11
+ * });
12
+ * ```
13
+ */
14
+ export function middleware(fn: MiddlewareFn): MiddlewareFn {
15
+ return fn;
16
+ }
17
+
18
+ /**
19
+ * Cooper instance for global middleware registration.
20
+ */
21
+ export const cooper = {
22
+ use(...middlewares: MiddlewareFn[]) {
23
+ for (const mw of middlewares) {
24
+ registry.addGlobalMiddleware(mw);
25
+ }
26
+ },
27
+ };
package/src/pubsub.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { registry } from "./registry.js";
2
+
3
+ export interface TopicConfig {
4
+ deliveryGuarantee?: "at-least-once" | "exactly-once";
5
+ orderBy?: string;
6
+ }
7
+
8
+ export interface SubscribeConfig {
9
+ concurrency?: number;
10
+ handler: (data: any) => Promise<void>;
11
+ }
12
+
13
+ export interface Topic<T> {
14
+ publish(data: T): Promise<void>;
15
+ subscribe(name: string, config: SubscribeConfig): any;
16
+ }
17
+
18
+ /**
19
+ * Declare a typed Pub/Sub topic.
20
+ *
21
+ * ```ts
22
+ * export const UserSignedUp = topic<{ userId: string; email: string }>(
23
+ * "user-signed-up",
24
+ * { deliveryGuarantee: "at-least-once" }
25
+ * );
26
+ * ```
27
+ */
28
+ export function topic<T = any>(name: string, config?: TopicConfig): Topic<T> {
29
+ const subscribers = new Map<string, { handler: Function; options: any }>();
30
+
31
+ const t: Topic<T> = {
32
+ async publish(data: T) {
33
+ // In local dev, deliver directly to subscribers
34
+ // In production, publish to NATS/SNS/Pub/Sub
35
+ const natsUrl = process.env.COOPER_NATS_URL ?? "nats://localhost:4222";
36
+
37
+ // Direct local delivery for development
38
+ for (const [subName, sub] of subscribers) {
39
+ try {
40
+ await sub.handler(data);
41
+ } catch (err) {
42
+ console.error(`[cooper] Subscriber "${subName}" failed:`, err);
43
+ }
44
+ }
45
+ },
46
+
47
+ subscribe(subName: string, subConfig: SubscribeConfig) {
48
+ subscribers.set(subName, {
49
+ handler: subConfig.handler,
50
+ options: subConfig,
51
+ });
52
+
53
+ registry.registerTopic(name, {
54
+ name,
55
+ subscribers,
56
+ });
57
+
58
+ return {
59
+ _cooper_type: "subscription",
60
+ topic: name,
61
+ name: subName,
62
+ };
63
+ },
64
+ };
65
+
66
+ registry.registerTopic(name, { name, subscribers });
67
+
68
+ return t;
69
+ }