@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 +21 -0
- package/README.md +143 -0
- package/dist/index.cjs +167 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
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":";;;AA6BO,SAAS,iBAAA,CACd,UACA,cAAA,EACkC;AAElC,EAAA,IAAI,CAAC,cAAA,CAAe,OAAA,IAAW,OAAO,cAAA,CAAe,YAAY,UAAA,EAAY;AAC3E,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,SAAA,EAAY,QAAA,CAAS,UAAA,CAAW,IAAI,CAAA,6BAAA,CAA+B,CAAA;AAAA,EACrF;AAEA,EAAA,MAAM,OAAA,GAAU,OAAO,MAAA,EAAiB,OAAA,GAA2B,EAAC,KAAsB;AACxF,IAAA,IAAI;AAEF,MAAA,IAAI,eAAe,QAAA,EAAU;AAC3B,QAAA,MAAM,OAAA,GAAU,MAAM,cAAA,CAAe,QAAA,CAAS,MAAM,CAAA;AACpD,QAAA,IAAI,CAAC,OAAA,EAAS;AACZ,UAAA,MAAM,IAAI,MAAM,6BAA6B,CAAA;AAAA,QAC/C;AAAA,MACF;AAGA,MAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,MAAA,MAAM,MAAA,GAAS,MAAM,cAAA,CAAe,OAAA,CAAQ,QAAQ,OAAO,CAAA;AAC3D,MAAA,MAAM,aAAA,GAAgB,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAGnC,MAAA,IAAI,QAAA,CAAS,UAAA,CAAW,WAAA,EAAa,OAAA,EAAS;AAC5C,QAAA,MAAM,aAAa,YAAA,CAAa,QAAA,CAAS,UAAA,CAAW,WAAA,CAAY,QAAQ,GAAG,CAAA;AAC3E,QAAA,IAAI,gBAAgB,UAAA,EAAY;AAC9B,UAAA,OAAA,CAAQ,IAAA;AAAA,YACN,YAAY,QAAA,CAAS,UAAA,CAAW,IAAI,CAAA,8BAAA,EAAiC,aAAa,QAAQ,UAAU,CAAA,EAAA;AAAA,WACtG;AAAA,QACF;AAAA,MACF;AAEA,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,MAAM,KAAA,YAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,MAAM,eAAe,CAAA;AAGtE,MAAA,cAAA,CAAe,OAAA,GAAU,GAAA,EAAK,MAAA,EAAQ,OAAO,CAAA;AAE7C,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,QAAA;AAAA,IACA,cAAA;AAAA,IACA;AAAA,GACF;AACF;AAKA,SAAS,aAAa,OAAA,EAAyB;AAC7C,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,iBAAiB,CAAA;AAC7C,EAAA,IAAI,CAAC,SAAS,CAAC,KAAA,CAAM,CAAC,CAAA,IAAK,CAAC,KAAA,CAAM,CAAC,CAAA,EAAG;AACpC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,OAAO,CAAA,CAAE,CAAA;AAAA,EACtD;AAEA,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,CAAC,GAAG,EAAE,CAAA;AACnC,EAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AAEpB,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,IAAA;AACH,MAAA,OAAO,KAAA;AAAA,IACT,KAAK,GAAA;AACH,MAAA,OAAO,KAAA,GAAQ,GAAA;AAAA,IACjB,KAAK,GAAA;AACH,MAAA,OAAO,QAAQ,EAAA,GAAK,GAAA;AAAA,IACtB;AACE,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,IAAI,CAAA,CAAE,CAAA;AAAA;AAErD;;;AChGO,SAAS,sBACd,QAAA,EACoE;AACpE,EAAA,OAAO,OAAO,GAAA,EAAc,GAAA,EAAe,IAAA,KAAuB;AAChE,IAAA,IAAI;AACF,MAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAG3B,MAAA,MAAM,EAAE,MAAA,GAAS,IAAI,QAAA,EAAU,YAAA,KAAiB,GAAA,CAAI,IAAA;AAGpD,MAAA,IAAI,YAAA,IAAgB,YAAA,KAAiB,QAAA,CAAS,QAAA,CAAS,WAAW,IAAA,EAAM;AACtE,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK;AAAA,UACnB,KAAA,EAAO,wBAAA;AAAA,UACP,QAAA,EAAU,QAAA,CAAS,QAAA,CAAS,UAAA,CAAW,IAAA;AAAA,UACvC,QAAA,EAAU;AAAA,SACX,CAAA;AACD,QAAA;AAAA,MACF;AAGA,MAAA,MAAM,WAAA,GAAc,GAAA;AACpB,MAAA,MAAM,YAAY,WAAA,CAAY,IAAA;AAC9B,MAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,OAAA,CAAQ,MAAA,EAAQ;AAAA,QAC5C,MACE,OAAO,SAAA,KAAc,QAAA,IAAY,SAAA,KAAc,OAC1C,SAAA,GACD,KAAA,CAAA;AAAA;AAAA,QACN,SAAS,GAAA,CAAI,OAAA;AAAA,QACb,IAAI,GAAA,CAAI;AAAA,OACT,CAAA;AAED,MAAA,MAAM,aAAA,GAAgB,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAGnC,MAAA,MAAM,aAAA,GAAgB,qBAAA,CAAsB,QAAA,CAAS,QAAA,EAAU,aAAa,CAAA;AAG5E,MAAA,GAAA,CAAI,IAAA,CAAK;AAAA,QACP,IAAA,EAAM,MAAA;AAAA,QACN,MAAA,EAAQ;AAAA,UACN,OAAA,EAAS,aAAA;AAAA,UACT,SAAA,EAAW,OAAA;AAAA,UACX,YAAA,EAAc;AAAA,SAChB;AAAA,QACA,QAAA,EAAU;AAAA,UACR,aAAA;AAAA,UACA,QAAA,EAAU,KAAA;AAAA,UACV,SAAA,EAAW;AAAA;AACb,OACD,CAAA;AAAA,IACH,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,KAAK,CAAA;AAAA,IACZ;AAAA,EACF,CAAA;AACF;AAMO,SAAS,qBAEd,SAAA,EAA2F;AAC3F,EAAA,OAAO,OAAO,GAAA,EAAc,GAAA,EAAe,IAAA,KAAuB;AAChE,IAAA,IAAI;AAEF,MAAA,MAAM,YAAA,GAAe,GAAA,CAAI,MAAA,CAAO,QAAA,IAAY,IAAI,IAAA,CAAK,QAAA;AAErD,MAAA,IAAI,CAAC,YAAA,EAAc;AACjB,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK;AAAA,UACnB,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,QAAA,GAAW,UAAU,YAAY,CAAA;AAEvC,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK;AAAA,UACnB,KAAA,EAAO,oBAAA;AAAA,UACP,QAAA,EAAU,YAAA;AAAA,UACV,SAAA,EAAW,MAAA,CAAO,IAAA,CAAK,SAAS;AAAA,SACjC,CAAA;AACD,QAAA;AAAA,MACF;AAGA,MAAA,MAAM,OAAA,GAAU,sBAAsB,QAAQ,CAAA;AAC9C,MAAA,MAAM,OAAA,CAAQ,GAAA,EAAK,GAAA,EAAK,IAAI,CAAA;AAAA,IAC9B,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,KAAK,CAAA;AAAA,IACZ;AAAA,EACF,CAAA;AACF;AAKA,SAAS,qBAAA,CACP,UACA,aAAA,EACoC;AACpC,EAAA,MAAM,iBAAA,GAAoB,QAAA,CAAS,UAAA,CAAW,WAAA,EAAa,OAAA;AAE3D,EAAA,IAAI,CAAC,iBAAA,EAAmB;AACtB,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,MAAM,UAAA,GAAa,gBAAA,CAAiB,iBAAA,CAAkB,GAAG,CAAA;AACzD,EAAA,IAAI,eAAe,IAAA,EAAM;AACvB,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,IAAI,iBAAiB,UAAA,EAAY;AAC/B,IAAA,OAAO,QAAA;AAAA,EACT,CAAA,MAAA,IAAW,aAAA,IAAiB,UAAA,GAAa,GAAA,EAAK;AAC5C,IAAA,OAAO,UAAA;AAAA,EACT,CAAA,MAAO;AACL,IAAA,OAAO,UAAA;AAAA,EACT;AACF;AAKA,SAAS,iBAAiB,OAAA,EAAgC;AACxD,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,iBAAiB,CAAA;AAC7C,EAAA,IAAI,CAAC,SAAS,CAAC,KAAA,CAAM,CAAC,CAAA,IAAK,CAAC,KAAA,CAAM,CAAC,CAAA,EAAG;AACpC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,CAAC,GAAG,EAAE,CAAA;AACnC,EAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AAEpB,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,IAAA;AACH,MAAA,OAAO,KAAA;AAAA,IACT,KAAK,GAAA;AACH,MAAA,OAAO,KAAA,GAAQ,GAAA;AAAA,IACjB,KAAK,GAAA;AACH,MAAA,OAAO,QAAQ,EAAA,GAAK,GAAA;AAAA,IACtB;AACE,MAAA,OAAO,IAAA;AAAA;AAEb","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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|