@reactive-contracts/server 0.1.0-beta

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mariano Álvarez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @reactive-contracts/server
2
+
3
+ Server-side implementation utilities for Reactive Contracts.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @reactive-contracts/server @reactive-contracts/core
9
+ # or
10
+ yarn add @reactive-contracts/server @reactive-contracts/core
11
+ # or
12
+ pnpm add @reactive-contracts/server @reactive-contracts/core
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Implementing a Contract
18
+
19
+ ```typescript
20
+ import { implementContract } from '@reactive-contracts/server';
21
+ import { UserProfileContract } from './contracts/user-profile.contract';
22
+
23
+ export const UserProfileResolver = implementContract(UserProfileContract, {
24
+ async resolve({ userId }, context) {
25
+ const user = await db.users.findById(userId);
26
+ const activity = await db.activity.getForUser(userId);
27
+
28
+ return {
29
+ user: {
30
+ id: user.id,
31
+ name: user.name,
32
+ avatar: user.avatarUrl,
33
+ joinedAt: user.createdAt,
34
+ },
35
+ activity: {
36
+ postsCount: activity.posts,
37
+ lastActive: activity.lastSeen,
38
+ },
39
+ };
40
+ },
41
+
42
+ cache: {
43
+ ttl: '5m',
44
+ staleWhileRevalidate: '1h',
45
+ tags: (params) => [`user:${params.userId}`],
46
+ },
47
+ });
48
+ ```
49
+
50
+ ### With Validation
51
+
52
+ ```typescript
53
+ export const CreateUserResolver = implementContract(CreateUserContract, {
54
+ async resolve(params, context) {
55
+ const user = await db.users.create(params);
56
+ return user;
57
+ },
58
+
59
+ validate: (params) => {
60
+ return params.email.includes('@') && params.name.length > 0;
61
+ },
62
+
63
+ onError: (error, params, context) => {
64
+ console.error('Failed to create user:', error);
65
+ },
66
+ });
67
+ ```
68
+
69
+ ### Using in an API Endpoint
70
+
71
+ ```typescript
72
+ import { UserProfileResolver } from './resolvers/user-profile.resolver';
73
+
74
+ // Express example
75
+ app.get('/api/users/:userId', async (req, res) => {
76
+ try {
77
+ const result = await UserProfileResolver.execute(
78
+ { userId: req.params.userId },
79
+ { user: req.user }
80
+ );
81
+
82
+ res.json(result);
83
+ } catch (error) {
84
+ res.status(500).json({ error: error.message });
85
+ }
86
+ });
87
+ ```
88
+
89
+ ## API
90
+
91
+ ### `implementContract(contract, implementation)`
92
+
93
+ Creates a contract resolver for server-side implementation.
94
+
95
+ **Parameters:**
96
+ - `contract`: Contract definition
97
+ - `implementation`: Implementation config
98
+ - `resolve`: Resolver function `(params, context) => data | Promise<data>`
99
+ - `validate?`: Validation function `(params) => boolean | Promise<boolean>`
100
+ - `cache?`: Caching configuration
101
+ - `ttl?`: Time to live (e.g., '5m', '1h')
102
+ - `staleWhileRevalidate?`: Stale cache duration
103
+ - `tags?`: Cache tag generator function
104
+ - `onError?`: Error handler function
105
+
106
+ **Returns:** ContractResolver object with:
107
+ - `contract`: The original contract
108
+ - `implementation`: The implementation config
109
+ - `execute`: Function to execute the resolver
110
+
111
+ ### Resolver Context
112
+
113
+ The context object passed to resolvers can include:
114
+ - `request?`: HTTP request object
115
+ - `headers?`: Request headers
116
+ - `user?`: Authenticated user
117
+ - Any custom properties you add
118
+
119
+ ### Cache Configuration
120
+
121
+ - `ttl`: How long to cache the result (e.g., '5m' for 5 minutes)
122
+ - `staleWhileRevalidate`: How long to serve stale data while revalidating
123
+ - `tags`: Function that returns cache tags for invalidation
124
+
125
+ ### Latency Monitoring
126
+
127
+ The server automatically monitors latency and logs warnings when constraints are exceeded:
128
+
129
+ ```typescript
130
+ // If contract has: latency: max('100ms')
131
+ // And execution takes 350ms, a warning will be logged
132
+ ```
133
+
134
+ ## TypeScript Support
135
+
136
+ Full TypeScript support with type inference:
137
+ - Resolver functions are typed based on contract shape
138
+ - Context is fully typed
139
+ - Parameters are validated at compile time
140
+
141
+ ## License
142
+
143
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ // src/implementContract.ts
4
+ function implementContract(contract, implementation) {
5
+ if (!implementation.resolve || typeof implementation.resolve !== "function") {
6
+ throw new Error(`Contract ${contract.definition.name} must have a resolve function`);
7
+ }
8
+ const execute = async (params, context = {}) => {
9
+ try {
10
+ if (implementation.validate) {
11
+ const isValid = await implementation.validate(params);
12
+ if (!isValid) {
13
+ throw new Error("Parameter validation failed");
14
+ }
15
+ }
16
+ const startTime = Date.now();
17
+ const result = await implementation.resolve(params, context);
18
+ const executionTime = Date.now() - startTime;
19
+ if (contract.definition.constraints?.latency) {
20
+ const maxLatency = parseLatency(contract.definition.constraints.latency.max);
21
+ if (executionTime > maxLatency) {
22
+ console.warn(
23
+ `Contract ${contract.definition.name} exceeded latency constraint: ${executionTime}ms > ${maxLatency}ms`
24
+ );
25
+ }
26
+ }
27
+ return result;
28
+ } catch (error) {
29
+ const err = error instanceof Error ? error : new Error("Unknown error");
30
+ implementation.onError?.(err, params, context);
31
+ throw err;
32
+ }
33
+ };
34
+ return {
35
+ contract,
36
+ implementation,
37
+ execute
38
+ };
39
+ }
40
+ function parseLatency(latency) {
41
+ const match = latency.match(/^(\d+)(ms|s|m)$/);
42
+ if (!match || !match[1] || !match[2]) {
43
+ throw new Error(`Invalid latency format: ${latency}`);
44
+ }
45
+ const value = parseInt(match[1], 10);
46
+ const unit = match[2];
47
+ switch (unit) {
48
+ case "ms":
49
+ return value;
50
+ case "s":
51
+ return value * 1e3;
52
+ case "m":
53
+ return value * 60 * 1e3;
54
+ default:
55
+ throw new Error(`Unknown latency unit: ${unit}`);
56
+ }
57
+ }
58
+
59
+ // src/express.ts
60
+ function createContractHandler(resolver) {
61
+ return async (req, res, next) => {
62
+ try {
63
+ const startTime = Date.now();
64
+ const { params = {}, contract: contractName } = req.body;
65
+ if (contractName && contractName !== resolver.contract.definition.name) {
66
+ res.status(400).json({
67
+ error: "Contract name mismatch",
68
+ expected: resolver.contract.definition.name,
69
+ received: contractName
70
+ });
71
+ return;
72
+ }
73
+ const reqWithUser = req;
74
+ const userValue = reqWithUser.user;
75
+ const result = await resolver.execute(params, {
76
+ user: typeof userValue === "object" && userValue !== null ? userValue : void 0,
77
+ // If using auth middleware
78
+ headers: req.headers,
79
+ ip: req.ip
80
+ });
81
+ const executionTime = Date.now() - startTime;
82
+ const latencyStatus = evaluateLatencyStatus(resolver.contract, executionTime);
83
+ res.json({
84
+ data: result,
85
+ status: {
86
+ latency: latencyStatus,
87
+ freshness: "fresh",
88
+ availability: "available"
89
+ },
90
+ metadata: {
91
+ executionTime,
92
+ cacheHit: false,
93
+ derivedAt: "origin"
94
+ }
95
+ });
96
+ } catch (error) {
97
+ next(error);
98
+ }
99
+ };
100
+ }
101
+ function createContractRouter(resolvers) {
102
+ return async (req, res, next) => {
103
+ try {
104
+ const contractName = req.params.contract || req.body.contract;
105
+ if (!contractName) {
106
+ res.status(400).json({
107
+ error: "Contract name is required"
108
+ });
109
+ return;
110
+ }
111
+ const resolver = resolvers[contractName];
112
+ if (!resolver) {
113
+ res.status(404).json({
114
+ error: "Contract not found",
115
+ contract: contractName,
116
+ available: Object.keys(resolvers)
117
+ });
118
+ return;
119
+ }
120
+ const handler = createContractHandler(resolver);
121
+ await handler(req, res, next);
122
+ } catch (error) {
123
+ next(error);
124
+ }
125
+ };
126
+ }
127
+ function evaluateLatencyStatus(contract, actualLatency) {
128
+ const latencyConstraint = contract.definition.constraints?.latency;
129
+ if (!latencyConstraint) {
130
+ return "normal";
131
+ }
132
+ const maxLatency = parseLatencyToMs(latencyConstraint.max);
133
+ if (maxLatency === null) {
134
+ return "normal";
135
+ }
136
+ if (actualLatency <= maxLatency) {
137
+ return "normal";
138
+ } else if (actualLatency <= maxLatency * 1.5) {
139
+ return "degraded";
140
+ } else {
141
+ return "violated";
142
+ }
143
+ }
144
+ function parseLatencyToMs(latency) {
145
+ const match = latency.match(/^(\d+)(ms|s|m)$/);
146
+ if (!match || !match[1] || !match[2]) {
147
+ return null;
148
+ }
149
+ const value = parseInt(match[1], 10);
150
+ const unit = match[2];
151
+ switch (unit) {
152
+ case "ms":
153
+ return value;
154
+ case "s":
155
+ return value * 1e3;
156
+ case "m":
157
+ return value * 60 * 1e3;
158
+ default:
159
+ return null;
160
+ }
161
+ }
162
+
163
+ exports.createContractHandler = createContractHandler;
164
+ exports.createContractRouter = createContractRouter;
165
+ exports.implementContract = implementContract;
166
+ //# sourceMappingURL=index.cjs.map
167
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/implementContract.ts","../src/express.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Contract } from '@reactive-contracts/core';\nimport type { ContractImplementation, ContractResolver, ResolverContext } from './types.js';\n\n/**\n * Implement a contract resolver on the server side\n *\n * @example\n * ```typescript\n * const UserProfileResolver = implementContract<\n * { userId: string },\n * { user: { id: string; name: string } }\n * >(UserProfileContract, {\n * async resolve({ userId }, context) {\n * const user = await db.users.findById(userId);\n * return {\n * user: {\n * id: user.id,\n * name: user.name,\n * avatar: user.avatarUrl,\n * },\n * };\n * },\n * cache: {\n * ttl: '5m',\n * tags: (params) => [`user:${params.userId}`],\n * },\n * });\n * ```\n */\nexport function implementContract<TParams, TData>(\n contract: Contract,\n implementation: ContractImplementation<TParams, TData>\n): ContractResolver<TParams, TData> {\n // Validate implementation\n if (!implementation.resolve || typeof implementation.resolve !== 'function') {\n throw new Error(`Contract ${contract.definition.name} must have a resolve function`);\n }\n\n const execute = async (params: TParams, context: ResolverContext = {}): Promise<TData> => {\n try {\n // Validate params if validator provided\n if (implementation.validate) {\n const isValid = await implementation.validate(params);\n if (!isValid) {\n throw new Error('Parameter validation failed');\n }\n }\n\n // Execute the resolver\n const startTime = Date.now();\n const result = await implementation.resolve(params, context);\n const executionTime = Date.now() - startTime;\n\n // Check latency constraints\n if (contract.definition.constraints?.latency) {\n const maxLatency = parseLatency(contract.definition.constraints.latency.max);\n if (executionTime > maxLatency) {\n console.warn(\n `Contract ${contract.definition.name} exceeded latency constraint: ${executionTime}ms > ${maxLatency}ms`\n );\n }\n }\n\n return result;\n } catch (error) {\n const err = error instanceof Error ? error : new Error('Unknown error');\n\n // Call error handler if provided\n implementation.onError?.(err, params, context);\n\n throw err;\n }\n };\n\n return {\n contract,\n implementation,\n execute,\n };\n}\n\n/**\n * Parse latency string to milliseconds\n */\nfunction parseLatency(latency: string): number {\n const match = latency.match(/^(\\d+)(ms|s|m)$/);\n if (!match || !match[1] || !match[2]) {\n throw new Error(`Invalid latency format: ${latency}`);\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2];\n\n switch (unit) {\n case 'ms':\n return value;\n case 's':\n return value * 1000;\n case 'm':\n return value * 60 * 1000;\n default:\n throw new Error(`Unknown latency unit: ${unit}`);\n }\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport type { Contract } from '@reactive-contracts/core';\nimport type { ContractResolver } from './types.js';\n\n/**\n * Create an Express handler for a contract resolver\n */\nexport function createContractHandler<TParams, TData>(\n resolver: ContractResolver<TParams, TData>\n): (req: Request, res: Response, next: NextFunction) => Promise<void> {\n return async (req: Request, res: Response, next: NextFunction) => {\n try {\n const startTime = Date.now();\n\n // Extract params from request body\n const { params = {}, contract: contractName } = req.body;\n\n // Validate contract name matches\n if (contractName && contractName !== resolver.contract.definition.name) {\n res.status(400).json({\n error: 'Contract name mismatch',\n expected: resolver.contract.definition.name,\n received: contractName,\n });\n return;\n }\n\n // Execute the contract\n const reqWithUser = req as unknown as Record<string, unknown>;\n const userValue = reqWithUser.user;\n const result = await resolver.execute(params, {\n user:\n typeof userValue === 'object' && userValue !== null\n ? (userValue as Record<string, unknown>)\n : undefined, // If using auth middleware\n headers: req.headers as Record<string, string>,\n ip: req.ip,\n });\n\n const executionTime = Date.now() - startTime;\n\n // Evaluate latency status\n const latencyStatus = evaluateLatencyStatus(resolver.contract, executionTime);\n\n // Send response\n res.json({\n data: result,\n status: {\n latency: latencyStatus,\n freshness: 'fresh',\n availability: 'available',\n },\n metadata: {\n executionTime,\n cacheHit: false,\n derivedAt: 'origin',\n },\n });\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Create an Express router for multiple contract resolvers\n * Uses Record with generic resolver type for flexibility\n */\nexport function createContractRouter<\n TResolvers extends Record<string, ContractResolver<unknown, unknown>>,\n>(resolvers: TResolvers): (req: Request, res: Response, next: NextFunction) => Promise<void> {\n return async (req: Request, res: Response, next: NextFunction) => {\n try {\n // Extract contract name from URL or body\n const contractName = req.params.contract || req.body.contract;\n\n if (!contractName) {\n res.status(400).json({\n error: 'Contract name is required',\n });\n return;\n }\n\n const resolver = resolvers[contractName];\n\n if (!resolver) {\n res.status(404).json({\n error: 'Contract not found',\n contract: contractName,\n available: Object.keys(resolvers),\n });\n return;\n }\n\n // Delegate to contract handler\n const handler = createContractHandler(resolver);\n await handler(req, res, next);\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Evaluate latency status based on contract constraints\n */\nfunction evaluateLatencyStatus(\n contract: Contract,\n actualLatency: number\n): 'normal' | 'degraded' | 'violated' {\n const latencyConstraint = contract.definition.constraints?.latency;\n\n if (!latencyConstraint) {\n return 'normal';\n }\n\n const maxLatency = parseLatencyToMs(latencyConstraint.max);\n if (maxLatency === null) {\n return 'normal';\n }\n\n if (actualLatency <= maxLatency) {\n return 'normal';\n } else if (actualLatency <= maxLatency * 1.5) {\n return 'degraded';\n } else {\n return 'violated';\n }\n}\n\n/**\n * Parse latency string to milliseconds\n */\nfunction parseLatencyToMs(latency: string): number | null {\n const match = latency.match(/^(\\d+)(ms|s|m)$/);\n if (!match || !match[1] || !match[2]) {\n return null;\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2];\n\n switch (unit) {\n case 'ms':\n return value;\n case 's':\n return value * 1000;\n case 'm':\n return value * 60 * 1000;\n default:\n return null;\n }\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { Contract } from '@reactive-contracts/core';
2
+ import { Request as Request$1, Response, NextFunction } from 'express';
3
+
4
+ interface ResolverContext {
5
+ request?: Request;
6
+ headers?: Record<string, string>;
7
+ user?: Record<string, unknown>;
8
+ [key: string]: unknown;
9
+ }
10
+ type ResolverFn<TParams, TData, TContext extends ResolverContext = ResolverContext> = (params: TParams, context: TContext) => Promise<TData> | TData;
11
+ interface CacheConfig<TParams> {
12
+ ttl?: string;
13
+ staleWhileRevalidate?: string;
14
+ tags?: (params: TParams) => string[];
15
+ }
16
+ interface ContractImplementation<TParams, TData, TContext extends ResolverContext = ResolverContext> {
17
+ resolve: ResolverFn<TParams, TData, TContext>;
18
+ cache?: CacheConfig<TParams>;
19
+ validate?: (params: TParams) => boolean | Promise<boolean>;
20
+ onError?: (error: Error, params: TParams, context: TContext) => void;
21
+ }
22
+ interface ContractResolver<TParams, TData> {
23
+ contract: Contract;
24
+ implementation: ContractImplementation<TParams, TData>;
25
+ execute: (params: TParams, context?: ResolverContext) => Promise<TData>;
26
+ }
27
+
28
+ declare function implementContract<TParams, TData>(contract: Contract, implementation: ContractImplementation<TParams, TData>): ContractResolver<TParams, TData>;
29
+
30
+ declare function createContractHandler<TParams, TData>(resolver: ContractResolver<TParams, TData>): (req: Request$1, res: Response, next: NextFunction) => Promise<void>;
31
+ declare function createContractRouter<TResolvers extends Record<string, ContractResolver<unknown, unknown>>>(resolvers: TResolvers): (req: Request$1, res: Response, next: NextFunction) => Promise<void>;
32
+
33
+ export { type CacheConfig, type ContractImplementation, type ContractResolver, type ResolverContext, type ResolverFn, createContractHandler, createContractRouter, implementContract };
@@ -0,0 +1,33 @@
1
+ import { Contract } from '@reactive-contracts/core';
2
+ import { Request as Request$1, Response, NextFunction } from 'express';
3
+
4
+ interface ResolverContext {
5
+ request?: Request;
6
+ headers?: Record<string, string>;
7
+ user?: Record<string, unknown>;
8
+ [key: string]: unknown;
9
+ }
10
+ type ResolverFn<TParams, TData, TContext extends ResolverContext = ResolverContext> = (params: TParams, context: TContext) => Promise<TData> | TData;
11
+ interface CacheConfig<TParams> {
12
+ ttl?: string;
13
+ staleWhileRevalidate?: string;
14
+ tags?: (params: TParams) => string[];
15
+ }
16
+ interface ContractImplementation<TParams, TData, TContext extends ResolverContext = ResolverContext> {
17
+ resolve: ResolverFn<TParams, TData, TContext>;
18
+ cache?: CacheConfig<TParams>;
19
+ validate?: (params: TParams) => boolean | Promise<boolean>;
20
+ onError?: (error: Error, params: TParams, context: TContext) => void;
21
+ }
22
+ interface ContractResolver<TParams, TData> {
23
+ contract: Contract;
24
+ implementation: ContractImplementation<TParams, TData>;
25
+ execute: (params: TParams, context?: ResolverContext) => Promise<TData>;
26
+ }
27
+
28
+ declare function implementContract<TParams, TData>(contract: Contract, implementation: ContractImplementation<TParams, TData>): ContractResolver<TParams, TData>;
29
+
30
+ declare function createContractHandler<TParams, TData>(resolver: ContractResolver<TParams, TData>): (req: Request$1, res: Response, next: NextFunction) => Promise<void>;
31
+ declare function createContractRouter<TResolvers extends Record<string, ContractResolver<unknown, unknown>>>(resolvers: TResolvers): (req: Request$1, res: Response, next: NextFunction) => Promise<void>;
32
+
33
+ export { type CacheConfig, type ContractImplementation, type ContractResolver, type ResolverContext, type ResolverFn, createContractHandler, createContractRouter, implementContract };
package/dist/index.js ADDED
@@ -0,0 +1,163 @@
1
+ // src/implementContract.ts
2
+ function implementContract(contract, implementation) {
3
+ if (!implementation.resolve || typeof implementation.resolve !== "function") {
4
+ throw new Error(`Contract ${contract.definition.name} must have a resolve function`);
5
+ }
6
+ const execute = async (params, context = {}) => {
7
+ try {
8
+ if (implementation.validate) {
9
+ const isValid = await implementation.validate(params);
10
+ if (!isValid) {
11
+ throw new Error("Parameter validation failed");
12
+ }
13
+ }
14
+ const startTime = Date.now();
15
+ const result = await implementation.resolve(params, context);
16
+ const executionTime = Date.now() - startTime;
17
+ if (contract.definition.constraints?.latency) {
18
+ const maxLatency = parseLatency(contract.definition.constraints.latency.max);
19
+ if (executionTime > maxLatency) {
20
+ console.warn(
21
+ `Contract ${contract.definition.name} exceeded latency constraint: ${executionTime}ms > ${maxLatency}ms`
22
+ );
23
+ }
24
+ }
25
+ return result;
26
+ } catch (error) {
27
+ const err = error instanceof Error ? error : new Error("Unknown error");
28
+ implementation.onError?.(err, params, context);
29
+ throw err;
30
+ }
31
+ };
32
+ return {
33
+ contract,
34
+ implementation,
35
+ execute
36
+ };
37
+ }
38
+ function parseLatency(latency) {
39
+ const match = latency.match(/^(\d+)(ms|s|m)$/);
40
+ if (!match || !match[1] || !match[2]) {
41
+ throw new Error(`Invalid latency format: ${latency}`);
42
+ }
43
+ const value = parseInt(match[1], 10);
44
+ const unit = match[2];
45
+ switch (unit) {
46
+ case "ms":
47
+ return value;
48
+ case "s":
49
+ return value * 1e3;
50
+ case "m":
51
+ return value * 60 * 1e3;
52
+ default:
53
+ throw new Error(`Unknown latency unit: ${unit}`);
54
+ }
55
+ }
56
+
57
+ // src/express.ts
58
+ function createContractHandler(resolver) {
59
+ return async (req, res, next) => {
60
+ try {
61
+ const startTime = Date.now();
62
+ const { params = {}, contract: contractName } = req.body;
63
+ if (contractName && contractName !== resolver.contract.definition.name) {
64
+ res.status(400).json({
65
+ error: "Contract name mismatch",
66
+ expected: resolver.contract.definition.name,
67
+ received: contractName
68
+ });
69
+ return;
70
+ }
71
+ const reqWithUser = req;
72
+ const userValue = reqWithUser.user;
73
+ const result = await resolver.execute(params, {
74
+ user: typeof userValue === "object" && userValue !== null ? userValue : void 0,
75
+ // If using auth middleware
76
+ headers: req.headers,
77
+ ip: req.ip
78
+ });
79
+ const executionTime = Date.now() - startTime;
80
+ const latencyStatus = evaluateLatencyStatus(resolver.contract, executionTime);
81
+ res.json({
82
+ data: result,
83
+ status: {
84
+ latency: latencyStatus,
85
+ freshness: "fresh",
86
+ availability: "available"
87
+ },
88
+ metadata: {
89
+ executionTime,
90
+ cacheHit: false,
91
+ derivedAt: "origin"
92
+ }
93
+ });
94
+ } catch (error) {
95
+ next(error);
96
+ }
97
+ };
98
+ }
99
+ function createContractRouter(resolvers) {
100
+ return async (req, res, next) => {
101
+ try {
102
+ const contractName = req.params.contract || req.body.contract;
103
+ if (!contractName) {
104
+ res.status(400).json({
105
+ error: "Contract name is required"
106
+ });
107
+ return;
108
+ }
109
+ const resolver = resolvers[contractName];
110
+ if (!resolver) {
111
+ res.status(404).json({
112
+ error: "Contract not found",
113
+ contract: contractName,
114
+ available: Object.keys(resolvers)
115
+ });
116
+ return;
117
+ }
118
+ const handler = createContractHandler(resolver);
119
+ await handler(req, res, next);
120
+ } catch (error) {
121
+ next(error);
122
+ }
123
+ };
124
+ }
125
+ function evaluateLatencyStatus(contract, actualLatency) {
126
+ const latencyConstraint = contract.definition.constraints?.latency;
127
+ if (!latencyConstraint) {
128
+ return "normal";
129
+ }
130
+ const maxLatency = parseLatencyToMs(latencyConstraint.max);
131
+ if (maxLatency === null) {
132
+ return "normal";
133
+ }
134
+ if (actualLatency <= maxLatency) {
135
+ return "normal";
136
+ } else if (actualLatency <= maxLatency * 1.5) {
137
+ return "degraded";
138
+ } else {
139
+ return "violated";
140
+ }
141
+ }
142
+ function parseLatencyToMs(latency) {
143
+ const match = latency.match(/^(\d+)(ms|s|m)$/);
144
+ if (!match || !match[1] || !match[2]) {
145
+ return null;
146
+ }
147
+ const value = parseInt(match[1], 10);
148
+ const unit = match[2];
149
+ switch (unit) {
150
+ case "ms":
151
+ return value;
152
+ case "s":
153
+ return value * 1e3;
154
+ case "m":
155
+ return value * 60 * 1e3;
156
+ default:
157
+ return null;
158
+ }
159
+ }
160
+
161
+ export { createContractHandler, createContractRouter, implementContract };
162
+ //# sourceMappingURL=index.js.map
163
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/implementContract.ts","../src/express.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Contract } from '@reactive-contracts/core';\nimport type { ContractImplementation, ContractResolver, ResolverContext } from './types.js';\n\n/**\n * Implement a contract resolver on the server side\n *\n * @example\n * ```typescript\n * const UserProfileResolver = implementContract<\n * { userId: string },\n * { user: { id: string; name: string } }\n * >(UserProfileContract, {\n * async resolve({ userId }, context) {\n * const user = await db.users.findById(userId);\n * return {\n * user: {\n * id: user.id,\n * name: user.name,\n * avatar: user.avatarUrl,\n * },\n * };\n * },\n * cache: {\n * ttl: '5m',\n * tags: (params) => [`user:${params.userId}`],\n * },\n * });\n * ```\n */\nexport function implementContract<TParams, TData>(\n contract: Contract,\n implementation: ContractImplementation<TParams, TData>\n): ContractResolver<TParams, TData> {\n // Validate implementation\n if (!implementation.resolve || typeof implementation.resolve !== 'function') {\n throw new Error(`Contract ${contract.definition.name} must have a resolve function`);\n }\n\n const execute = async (params: TParams, context: ResolverContext = {}): Promise<TData> => {\n try {\n // Validate params if validator provided\n if (implementation.validate) {\n const isValid = await implementation.validate(params);\n if (!isValid) {\n throw new Error('Parameter validation failed');\n }\n }\n\n // Execute the resolver\n const startTime = Date.now();\n const result = await implementation.resolve(params, context);\n const executionTime = Date.now() - startTime;\n\n // Check latency constraints\n if (contract.definition.constraints?.latency) {\n const maxLatency = parseLatency(contract.definition.constraints.latency.max);\n if (executionTime > maxLatency) {\n console.warn(\n `Contract ${contract.definition.name} exceeded latency constraint: ${executionTime}ms > ${maxLatency}ms`\n );\n }\n }\n\n return result;\n } catch (error) {\n const err = error instanceof Error ? error : new Error('Unknown error');\n\n // Call error handler if provided\n implementation.onError?.(err, params, context);\n\n throw err;\n }\n };\n\n return {\n contract,\n implementation,\n execute,\n };\n}\n\n/**\n * Parse latency string to milliseconds\n */\nfunction parseLatency(latency: string): number {\n const match = latency.match(/^(\\d+)(ms|s|m)$/);\n if (!match || !match[1] || !match[2]) {\n throw new Error(`Invalid latency format: ${latency}`);\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2];\n\n switch (unit) {\n case 'ms':\n return value;\n case 's':\n return value * 1000;\n case 'm':\n return value * 60 * 1000;\n default:\n throw new Error(`Unknown latency unit: ${unit}`);\n }\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport type { Contract } from '@reactive-contracts/core';\nimport type { ContractResolver } from './types.js';\n\n/**\n * Create an Express handler for a contract resolver\n */\nexport function createContractHandler<TParams, TData>(\n resolver: ContractResolver<TParams, TData>\n): (req: Request, res: Response, next: NextFunction) => Promise<void> {\n return async (req: Request, res: Response, next: NextFunction) => {\n try {\n const startTime = Date.now();\n\n // Extract params from request body\n const { params = {}, contract: contractName } = req.body;\n\n // Validate contract name matches\n if (contractName && contractName !== resolver.contract.definition.name) {\n res.status(400).json({\n error: 'Contract name mismatch',\n expected: resolver.contract.definition.name,\n received: contractName,\n });\n return;\n }\n\n // Execute the contract\n const reqWithUser = req as unknown as Record<string, unknown>;\n const userValue = reqWithUser.user;\n const result = await resolver.execute(params, {\n user:\n typeof userValue === 'object' && userValue !== null\n ? (userValue as Record<string, unknown>)\n : undefined, // If using auth middleware\n headers: req.headers as Record<string, string>,\n ip: req.ip,\n });\n\n const executionTime = Date.now() - startTime;\n\n // Evaluate latency status\n const latencyStatus = evaluateLatencyStatus(resolver.contract, executionTime);\n\n // Send response\n res.json({\n data: result,\n status: {\n latency: latencyStatus,\n freshness: 'fresh',\n availability: 'available',\n },\n metadata: {\n executionTime,\n cacheHit: false,\n derivedAt: 'origin',\n },\n });\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Create an Express router for multiple contract resolvers\n * Uses Record with generic resolver type for flexibility\n */\nexport function createContractRouter<\n TResolvers extends Record<string, ContractResolver<unknown, unknown>>,\n>(resolvers: TResolvers): (req: Request, res: Response, next: NextFunction) => Promise<void> {\n return async (req: Request, res: Response, next: NextFunction) => {\n try {\n // Extract contract name from URL or body\n const contractName = req.params.contract || req.body.contract;\n\n if (!contractName) {\n res.status(400).json({\n error: 'Contract name is required',\n });\n return;\n }\n\n const resolver = resolvers[contractName];\n\n if (!resolver) {\n res.status(404).json({\n error: 'Contract not found',\n contract: contractName,\n available: Object.keys(resolvers),\n });\n return;\n }\n\n // Delegate to contract handler\n const handler = createContractHandler(resolver);\n await handler(req, res, next);\n } catch (error) {\n next(error);\n }\n };\n}\n\n/**\n * Evaluate latency status based on contract constraints\n */\nfunction evaluateLatencyStatus(\n contract: Contract,\n actualLatency: number\n): 'normal' | 'degraded' | 'violated' {\n const latencyConstraint = contract.definition.constraints?.latency;\n\n if (!latencyConstraint) {\n return 'normal';\n }\n\n const maxLatency = parseLatencyToMs(latencyConstraint.max);\n if (maxLatency === null) {\n return 'normal';\n }\n\n if (actualLatency <= maxLatency) {\n return 'normal';\n } else if (actualLatency <= maxLatency * 1.5) {\n return 'degraded';\n } else {\n return 'violated';\n }\n}\n\n/**\n * Parse latency string to milliseconds\n */\nfunction parseLatencyToMs(latency: string): number | null {\n const match = latency.match(/^(\\d+)(ms|s|m)$/);\n if (!match || !match[1] || !match[2]) {\n return null;\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2];\n\n switch (unit) {\n case 'ms':\n return value;\n case 's':\n return value * 1000;\n case 'm':\n return value * 60 * 1000;\n default:\n return null;\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@reactive-contracts/server",
3
+ "version": "0.1.0-beta",
4
+ "description": "Server-side implementation utilities for Reactive Contracts",
5
+ "keywords": [
6
+ "contracts",
7
+ "server",
8
+ "backend",
9
+ "api"
10
+ ],
11
+ "homepage": "https://github.com/creativoma/reactive-contracts",
12
+ "bugs": {
13
+ "url": "https://github.com/creativoma/reactive-contracts/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/creativoma/reactive-contracts.git",
18
+ "directory": "packages/server"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Reactive Contracts Contributors",
22
+ "type": "module",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "require": "./dist/index.cjs"
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md"
36
+ ],
37
+ "dependencies": {
38
+ "@reactive-contracts/core": "0.1.0-beta"
39
+ },
40
+ "devDependencies": {
41
+ "@types/express": "^5.0.6",
42
+ "@types/node": "^25.0.3",
43
+ "@typescript-eslint/eslint-plugin": "^8.52.0",
44
+ "@typescript-eslint/parser": "^8.52.0",
45
+ "@vitest/coverage-v8": "^4.0.16",
46
+ "eslint": "^9.39.2",
47
+ "tsup": "^8.5.1",
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^4.0.16"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "lint": "eslint src --ext .ts",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "vitest run",
60
+ "test:coverage": "vitest run --coverage",
61
+ "clean": "rm -rf dist"
62
+ }
63
+ }